본문 바로가기

카테고리 없음

인터페이스를 통해 구현을 하는 이유

인터페이스를 만들고 그에 따른 구현을 하는 이유는 주로 유연성, 확장성, 테스트 용이성을 높이기 위해서입니다.

아래에서 각 이유를 자세히 설명하겠습니다:

  1. 유연성과 변경 용이성
    인터페이스를 사용하면 시스템의 구현을 바꾸거나 변경할 때 의존성을 최소화할 수 있습니다. 클래스들이 인터페이스에 의존하게 되면, 해당 인터페이스를 구현한 클래스만 변경하면 되므로 시스템 전체에 미치는 영향을 줄일 수 있습니다.

예를 들어, 데이터베이스 저장 방식을 변경해야 한다면, 데이터베이스 관련 코드의 구현체만 바꾸면 됩니다. 인터페이스를 구현한 코드만 변경하고, 나머지 시스템은 여전히 동일한 인터페이스를 통해 접근할 수 있기 때문입니다. 이를 통해 기존 코드 변경을 최소화하고, 새로운 구현을 도입할 수 있는 유연성을 제공합니다.


// 인터페이스 정의
public interface Storage {
    void save(String data);
}

// DB 저장소 구현
public class DatabaseStorage implements Storage {
    @Override
    public void save(String data) {
        // DB 저장 구현
    }
}

// 파일 저장소 구현
public class FileStorage implements Storage {
    @Override
    public void save(String data) {
        // 파일에 저장 구현
    }
}

// 클라이언트 클래스
public class DataService {
    private Storage storage;

    public DataService(Storage storage) {
        this.storage = storage;
    }

    public void saveData(String data) {
        storage.save(data);  // 다양한 구현체로 바꿀 수 있음
    }
}

위 예시에서 DataService 클래스는 Storage 인터페이스에 의존하고 있기 때문에, 저장 방식을 바꾸고 싶다면 구현체만 변경하면 됩니다.

  1. 추상화와 의존성 역전
    인터페이스는 추상화를 제공합니다. 클래스는 특정 기능을 수행하지만, 이 기능이 어떻게 구현되는지에 대해서는 몰라도 됩니다. 즉, 구현 세부 사항을 숨기고, 시스템 간의 의존성을 낮추고 각 모듈을 독립적으로 설계할 수 있습니다.

의존성 역전 원칙(DIP, Dependency Inversion Principle) 은 고수준 모듈이 저수준 모듈에 의존하지 않도록 해야 한다는 원칙입니다. 인터페이스를 사용하면 고수준 모듈이 저수준 모듈에 의존하는 문제를 해결할 수 있습니다. 고수준 모듈은 구현이 아닌 인터페이스에 의존하게 되므로, 구현을 변경하거나 추가할 때 고수준 모듈을 수정할 필요가 없습니다.


// 고수준 모듈 (비즈니스 로직)
public class OrderService {
    private PaymentProcessor paymentProcessor;

    public OrderService(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public void processOrder(Order order) {
        paymentProcessor.processPayment(order);  // PaymentProcessor 인터페이스에 의존
    }
}

// 저수준 모듈 (구현체)
public interface PaymentProcessor {
    void processPayment(Order order);
}

public class CreditCardPaymentProcessor implements PaymentProcessor {
    @Override
    public void processPayment(Order order) {
        // 신용카드 결제 처리 구현
    }
}

public class PayPalPaymentProcessor implements PaymentProcessor {
    @Override
    public void processPayment(Order order) {
        // PayPal 결제 처리 구현
    }
}

위 예시에서 OrderService는 PaymentProcessor 인터페이스에 의존하고 있으며, 구체적인 결제 처리 방식은 CreditCardPaymentProcessor나 PayPalPaymentProcessor와 같은 클래스로 처리됩니다. 새로운 결제 방식을 추가하려면, PaymentProcessor 인터페이스만 구현한 클래스를 추가하면 됩니다. OrderService는 그대로 두고, 변경이 필요하지 않습니다.

  1. 테스트 용이성
    인터페이스를 사용하면 단위 테스트가 훨씬 용이해집니다. 구현체를 인터페이스로 추상화하면, 실제 구현체가 아닌 목(Mocking) 객체를 사용하여 테스트할 수 있습니다. 테스트 환경에서 실제 구현체를 사용하지 않고, 의존성을 주입받는 방식으로 코드가 동작하게 되므로, 테스트가 빠르고 독립적입니다.

public class OrderServiceTest {
    @Test
    public void testProcessOrder() {
        // PaymentProcessor를 Mock으로 설정
        PaymentProcessor mockPaymentProcessor = mock(PaymentProcessor.class);
        OrderService orderService = new OrderService(mockPaymentProcessor);

        Order order = new Order();
        orderService.processOrder(order);

        // 결제 처리 메서드가 호출되었는지 확인
        verify(mockPaymentProcessor).processPayment(order);
    }
}

이렇게 PaymentProcessor 인터페이스를 사용하면 실제 결제 로직을 테스트할 때, 실제 결제 처리 시스템을 호출하는 대신, mock 객체를 사용하여 빠르게 테스트할 수 있습니다.

  1. 확장성
    인터페이스를 사용하면 기능 추가 및 확장이 용이합니다. 기존 코드를 수정할 필요 없이 인터페이스를 구현하는 새로운 클래스를 추가하여 시스템에 기능을 확장할 수 있습니다. 예를 들어, 결제 처리 시스템에 새로운 결제 수단을 추가하려면, 해당 결제 수단을 처리할 클래스를 인터페이스만 따르도록 구현하면 됩니다.

public class NewPaymentProcessor implements PaymentProcessor {
    @Override
    public void processPayment(Order order) {
        // 새로운 결제 방식 처리 구현
    }
}

이와 같이, 시스템에 새로운 기능을 추가하거나 변경할 때 기존 시스템의 구조를 그대로 유지하면서 쉽게 확장할 수 있습니다.

  1. 인터페이스를 통한 다양한 구현
    인터페이스를 사용하면 여러 가지 다양한 구현체를 하나의 유니폼한 방식으로 처리할 수 있습니다. 예를 들어, 결제 시스템에서 여러 결제 방식이 필요하다면, 각 결제 방식을 하나의 인터페이스로 추상화하여 동일한 방식으로 처리할 수 있습니다.

PaymentProcessor processor = new CreditCardPaymentProcessor();
processor.processPayment(order);

processor = new PayPalPaymentProcessor();
processor.processPayment(order);

이와 같이 인터페이스는 다양한 구현체를 동일한 방식으로 처리할 수 있도록 해줍니다.

결론

인터페이스를 사용하고 그에 맞는 구현을 하는 이유는 유연성, 확장성, 테스트 용이성, 구현의 분리 등 여러 이점을 제공하기 때문입니다. 시스템이 커지고 복잡해질수록 이러한 추상화는 필수적이며, 변경에 강하고, 확장 가능한 시스템을 설계하는 데 매우 중요한 역할을 합니다.