800===Dev Docs and License/이론 문서

객체지향 심화 학습 5편: 의존성 역전 원칙 (DIP) 완전정복 🎯

블로글러 2024. 11. 3. 21:15

오늘은 SOLID의 마지막 원칙인 의존성 역전 원칙(DIP)을 자세히 알아볼게요!

1. 의존성 역전 원칙이란? 💡

핵심: "고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 되며, 둘 다 추상화에 의존해야 한다"

Bad Case: DIP 위반 사례

// ❌ 이렇게 하면 안돼요!
public class OrderService {
    // 구체 클래스에 직접 의존
    private final MySQLOrderRepository orderRepository;
    private final SmtpEmailService emailService;

    public OrderService() {
        // 직접 생성하여 의존성 발생
        this.orderRepository = new MySQLOrderRepository();
        this.emailService = new SmtpEmailService();
    }

    public void createOrder(Order order) {
        orderRepository.save(order);
        emailService.sendEmail(order.getCustomerEmail());
    }
}

Good Case: DIP 준수 사례

// ✅ 이렇게 추상화에 의존하게 만들어요!
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(String orderId);
}

public interface NotificationService {
    void notify(String recipient, String message);
}

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final NotificationService notificationService;

    // 생성자 주입으로 의존성 주입
    public OrderService(
        OrderRepository orderRepository,
        NotificationService notificationService
    ) {
        this.orderRepository = orderRepository;
        this.notificationService = notificationService;
    }

    public void createOrder(Order order) {
        orderRepository.save(order);
        notificationService.notify(
            order.getCustomerEmail(),
            "주문이 완료되었습니다."
        );
    }
}

2. 실전 적용 예시: 결제 시스템 💳

2-1. 결제 시스템 구조

// 결제 처리 인터페이스
public interface PaymentGateway {
    PaymentResult process(Payment payment);
    RefundResult refund(String paymentId);
}

// 결제 검증 인터페이스
public interface PaymentValidator {
    void validate(Payment payment);
}

// 결제 이벤트 발행 인터페이스
public interface PaymentEventPublisher {
    void publishPaymentCompleted(Payment payment);
    void publishPaymentFailed(Payment payment, Exception e);
}

// 고수준 모듈: 결제 서비스
@Service
@RequiredArgsConstructor
public class PaymentService {
    private final PaymentGateway paymentGateway;
    private final PaymentValidator paymentValidator;
    private final PaymentEventPublisher eventPublisher;

    @Transactional
    public PaymentResult processPayment(Payment payment) {
        try {
            // 결제 검증
            paymentValidator.validate(payment);

            // 결제 처리
            PaymentResult result = paymentGateway.process(payment);

            // 이벤트 발행
            eventPublisher.publishPaymentCompleted(payment);

            return result;

        } catch (Exception e) {
            eventPublisher.publishPaymentFailed(payment, e);
            throw e;
        }
    }
}

// 저수준 모듈: 실제 구현체들
@Component
public class TossPaymentGateway implements PaymentGateway {
    private final TossPayClient tossPayClient;

    @Override
    public PaymentResult process(Payment payment) {
        return tossPayClient.processPayment(
            convertToTossPayRequest(payment)
        );
    }
}

@Component
public class DefaultPaymentValidator implements PaymentValidator {
    @Override
    public void validate(Payment payment) {
        validateAmount(payment.getAmount());
        validatePaymentMethod(payment.getMethod());
    }
}

@Component
public class KafkaPaymentEventPublisher implements PaymentEventPublisher {
    private final KafkaTemplate<String, PaymentEvent> kafkaTemplate;

    @Override
    public void publishPaymentCompleted(Payment payment) {
        kafkaTemplate.send(
            "payment.completed",
            new PaymentCompletedEvent(payment)
        );
    }
}

2-2. 테스트 용이성

@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
    @Mock private PaymentGateway paymentGateway;
    @Mock private PaymentValidator validator;
    @Mock private PaymentEventPublisher eventPublisher;

    @InjectMocks
    private PaymentService paymentService;

    @Test
    void processPayment_Success() {
        // Given
        Payment payment = createTestPayment();
        PaymentResult expectedResult = createSuccessResult();

        given(paymentGateway.process(payment))
            .willReturn(expectedResult);

        // When
        PaymentResult result = paymentService.processPayment(payment);

        // Then
        assertThat(result).isEqualTo(expectedResult);
        verify(eventPublisher).publishPaymentCompleted(payment);
    }
}

3. DIP를 지원하는 Spring의 기능들 🌱

3-1. 의존성 주입

// 생성자 주입
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentService paymentService;
}

// 필드 주입 (권장하지 않음)
@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
}

// 수정자 주입
@Service
public class OrderService {
    private OrderRepository orderRepository;

    @Autowired
    public void setOrderRepository(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

3-2. Configuration을 통한 의존성 설정

@Configuration
public class PaymentConfig {
    @Bean
    public PaymentGateway tossPaymentGateway(
        @Value("${toss.api.key}") String apiKey
    ) {
        return new TossPaymentGateway(
            new TossPayClient(apiKey)
        );
    }

    @Bean
    @Profile("prod")
    public PaymentEventPublisher kafkaPaymentEventPublisher(
        KafkaTemplate kafkaTemplate
    ) {
        return new KafkaPaymentEventPublisher(kafkaTemplate);
    }

    @Bean
    @Profile("test")
    public PaymentEventPublisher mockPaymentEventPublisher() {
        return new MockPaymentEventPublisher();
    }
}

4. DIP를 활용한 유연한 설계 💪

4-1. 전략 패턴과 함께 사용

// 결제 방식별 전략
public interface PaymentStrategy {
    boolean supports(PaymentMethod method);
    PaymentResult process(Payment payment);
}

@Component
public class CreditCardStrategy implements PaymentStrategy {
    @Override
    public boolean supports(PaymentMethod method) {
        return method == PaymentMethod.CREDIT_CARD;
    }

    @Override
    public PaymentResult process(Payment payment) {
        // 신용카드 결제 처리
    }
}

@Component
public class KakaoPayStrategy implements PaymentStrategy {
    @Override
    public boolean supports(PaymentMethod method) {
        return method == PaymentMethod.KAKAO_PAY;
    }

    @Override
    public PaymentResult process(Payment payment) {
        // 카카오페이 결제 처리
    }
}

// 전략 선택 및 실행
@Service
@RequiredArgsConstructor
public class PaymentProcessor {
    private final List<PaymentStrategy> paymentStrategies;

    public PaymentResult processPayment(Payment payment) {
        return paymentStrategies.stream()
            .filter(strategy -> strategy.supports(payment.getMethod()))
            .findFirst()
            .orElseThrow(() -> new UnsupportedPaymentMethodException())
            .process(payment);
    }
}

4-2. 어댑터 패턴과 함께 사용

// 외부 결제 시스템 어댑터
public interface PaymentSystemAdapter {
    PaymentResult processPayment(Payment payment);
}

@Component
public class TossPaymentAdapter implements PaymentSystemAdapter {
    private final TossPayClient tossPayClient;

    @Override
    public PaymentResult processPayment(Payment payment) {
        TossPayRequest request = convertToTossPayRequest(payment);
        TossPayResponse response = tossPayClient.executePayment(request);
        return convertToPaymentResult(response);
    }
}

@Component
public class NaverPayAdapter implements PaymentSystemAdapter {
    private final NaverPayApi naverPayApi;

    @Override
    public PaymentResult processPayment(Payment payment) {
        NaverPaymentDto dto = convertToNaverPayDto(payment);
        NaverPayResult result = naverPayApi.pay(dto);
        return convertToPaymentResult(result);
    }
}

5. DIP 적용 시 주의사항 ⚠️

5-1. 적절한 추상화 수준 선택

// ❌ 너무 상세한 추상화
public interface OrderRepository {
    void saveToMySQL(Order order);
    void updateInMySQL(Order order);
}

// ✅ 적절한 추상화
public interface OrderRepository {
    void save(Order order);
    void update(Order order);
}

5-2. 순환 의존성 피하기

// ❌ 순환 의존성 발생
public class ServiceA {
    private final ServiceB serviceB;
}

public class ServiceB {
    private final ServiceA serviceA;  // 순환 의존!
}

// ✅ 이벤트를 통한 의존성 제거
public class ServiceA {
    private final ServiceB serviceB;

    public void process() {
        // B 호출 후 이벤트 발행
        serviceB.process();
        eventPublisher.publish(new ProcessCompletedEvent());
    }
}

public class ServiceB {
    @EventListener
    public void onProcessCompleted(ProcessCompletedEvent event) {
        // A의 처리 완료를 이벤트로 수신
    }
}

6. 실무 적용 전략 🔨

6-1. 계층별 의존성 관리

// 도메인 계층: 순수한 비즈니스 로직
public class Order {
    private OrderStatus status;

    public void cancel() {
        validateCancellable();
        this.status = OrderStatus.CANCELLED;
    }
}

// 응용 계층: 인터페이스를 통한 의존성 주입
@Service
public class OrderService {
    private final OrderRepository repository;
    private final PaymentService paymentService;

    public void cancelOrder(String orderId) {
        Order order = repository.findById(orderId)
            .orElseThrow(OrderNotFoundException::new);

        order.cancel();
        paymentService.refund(order.getPaymentId());
        repository.save(order);
    }
}

// 인프라 계층: 구체적인 구현
@Repository
public class JpaOrderRepository implements OrderRepository {
    private final OrderJpaRepository jpaRepository;

    @Override
    public Optional<Order> findById(String orderId) {
        return jpaRepository.findById(orderId)
            .map(this::convertToOrder);
    }
}

6-2. 모듈화와 패키지 구조

com.company.payment
├── domain
│   ├── Payment.java
│   ├── PaymentResult.java
│   └── PaymentStatus.java
├── application
│   ├── PaymentService.java
│   ├── port
│   │   ├── PaymentGateway.java
│   │   └── PaymentEventPublisher.java
│   └── dto
│       ├── PaymentRequest.java
│       └── PaymentResponse.java
└── infrastructure
    ├── persistence
    │   └── JpaPaymentRepository.java
    ├── payment
    │   ├── TossPaymentGateway.java
    │   └── KakaoPaymentGateway.java
    └── event
        └── KafkaPaymentEventPublisher.java

7. DIP의 장점 🌟

  1. 유연성과 확장성

    • 구현체 변경이 용이
    • 새로운 기능 추가가 쉬움
  2. 테스트 용이성

    • 목(Mock) 객체를 통한 테스트 가능
    • 단위 테스트 작성이 쉬움
  3. 유지보수성

    • 변경의 영향 범위가 제한적
    • 코드의 재사용성 향상
  4. 디커플링

    • 모듈 간 결합도 감소
    • 독립적인 개발과 배포 가능

마치며 🎁

DIP는 객체지향 설계의 핵심 원칙 중 하나로, 유연하고 확장 가능한 시스템을 만드는 데 매우 중요합니다. Spring Framework는 DIP를 구현하기 위한 다양한 기능을 제공하며, 이를 잘 활용하면 견고한 애플리케이션을 만들 수 있습니다.


SOLID 원칙 시리즈의 마지막 편이었습니다!
다음에는 디자인 패턴에 대해 알아볼게요! 😊

728x90