오늘은 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의 장점 🌟
유연성과 확장성
- 구현체 변경이 용이
- 새로운 기능 추가가 쉬움
테스트 용이성
- 목(Mock) 객체를 통한 테스트 가능
- 단위 테스트 작성이 쉬움
유지보수성
- 변경의 영향 범위가 제한적
- 코드의 재사용성 향상
디커플링
- 모듈 간 결합도 감소
- 독립적인 개발과 배포 가능
마치며 🎁
DIP는 객체지향 설계의 핵심 원칙 중 하나로, 유연하고 확장 가능한 시스템을 만드는 데 매우 중요합니다. Spring Framework는 DIP를 구현하기 위한 다양한 기능을 제공하며, 이를 잘 활용하면 견고한 애플리케이션을 만들 수 있습니다.
SOLID 원칙 시리즈의 마지막 편이었습니다!
다음에는 디자인 패턴에 대해 알아볼게요! 😊
728x90
'800===Dev Docs and License > 이론 문서' 카테고리의 다른 글
함수형 프로그래밍 vs 명령형 프로그래밍 비교 가이드 🔄 (0) | 2024.11.03 |
---|---|
객체지향 심화 학습 4편: 인터페이스 분리 원칙 (ISP) 완전정복 🎯 (1) | 2024.11.03 |
객체지향 심화 학습 1편 SRP : SOLID 원칙 완전정복 🎯 (0) | 2024.11.03 |
객체지향의 핵심 개념 정복하기 🎯 (0) | 2024.11.03 |
What is the Internet (0) | 2024.06.07 |