(2-4) SOLID(객체 지향 설계): 의존성 역전의 원칙(DIP)

728x90

DIP

상위 모듈은 하위 모듈의 구현에 의존해서는 안되며, 하위의 모듈이 상위 모듈에 정의한 추상 타입에 의존 해야 한다.

  • 카드 결제 시스템을 구축
  • 현재 지원하는 카드는 신한 카드
  • 추후에 우리 카드가 추가되어 결제를 구현
  • 지속해서 카드가 추가
상위 모듈이란?
상위 모듈이란 상위 정책을 의미한다. 위의 요구사항에서는 카드 졀제라는 것이 상위 정책을 뜻한다.
하위 모듈이란?
하위 모듈이란 상위 정책에 따른 하위 정책을 말한다. 위의 요구사항에서 카드 결제라는 상위 정책이 있으면 신한 카드 결제라는 하위(세부) 정책이 있다.
추상 타입이란?
추상 타입은 인터페이스, 추상화 클래스를 의미한다. 상위 정책이 하위 정책에 의존하지 않고 추상 타입에 의존하라는 것은 카드 결제라는 상위 정책이 신한 카드 결제라는 하위 정책을 의존하지 말고 추상 클래스, 인터페이스를 의존하라는 뜻이다.

DIP 를 준수하지 않은 코드

위 그림은 카드 결제(상위 수준 정책) 가 신한 카드 결제(하위 수준) 에 의존하고 있다. 이 경우에는 아래와 같은 단점이 존재하게 된다.

 

1. 지나친 의존 관계

class PaymentController {
    @RequestMapping(value = "/dip/anti/payment", method = RequestMethod.POST)
    public void pay(@RequestBody ShinhanCardDto.PaymentRequest req){
        shinhanCardPaymentService.pay(req);
    }   
}
class ShinhanCardPaymentService {
    public void pay(ShinhanCardDto.PaymentRequest req) {
        shinhanCardApi.pay(req);
    }   
}
// RequestBody JSON 포멧
{
  "shinhanCardNumber":"4845-9005-9423-4452",  //만약 shinhanCardNumber -> cardNumber 으로 변경된다면 ?
  "cvc":"233"
}

카드 결제 기능(상취 정책) 이 신한 카드 결제(하위 정책) 에 지나치게 의존적이다. 그 결과 위처럼 신한 카드사의 카드 결제의 JSON 의 키값만 변경 시 컨트롤러, 또 그 값을 넘겨주는 프론트엔드 변경까지 영향을 미치게 된다. 이렇게 지나친 의존관계는 많은 변경 포인트를 유발한다.

 

2. 확장에 유연하지 못함

@RequestMapping(value = "/anti/payment", method = RequestMethod.POST)
public void pay(@RequestBody CardPaymentDto.PaymentRequest req){
    if(req.getType() == CardType.SHINHAN){
        shinhanCardPaymentService.pay(req);
    }else if(req.getType() == CardType.WOORI){
        wooriCardPaymentService.pay(req);
    }
}

우리 카드, 신한 카드 결제 요청을 받을 PaymentRequest DTO 클래스를 생성했고 CardType 으로 해당하는 카드 타입에 맞는 서비스를 호출하는 구조이다. 가장 쉽게 생각할 수 있는 구조이지만 절대 좋은 구조는 아니다. 카드가 지속해서 추가될 때 마다 해당 카드 결제를 위한 if 문을 지속해서 추가해야 하기 때문이며, 이런 반복적인 if 는 리팩토링 대상일 확률이 매우 높다.

 

그 밖에도 추가될 결제를 담당하는 XXXPaymentService 클래스들이 지속해서 컨트롤러에 의존하게 된다. 그 결과 PaymentController 는 컨트롤러 계층임에도 너무 많은 책임을 갖게 되며, 확장에 어렵고, 변경에 취약한 구조가 된다. OCP 에서도 언급했던 내용이다.

이렇듯 DIP 와 OCP 는 연관이 크며 SOLID 도 각기 다른 메커니즘이 아니라 서로 유기적으로 연결되어 있다.


DIP 준수

상위 모듈은 하위 모듈의 구현에 의존해서는 안 된다. 하위 모듈이 상위 모듈이 정의한 추상 타입에 의존해야 한다.

카드 결제(컨트롤러, 상위 모듈) 는 신한 카드 결제(서비스, 하위 모듈) 에 의존해서는 안된다. 신한 카드 결제(서비스, 하위 모듈) 은 카드 결제(컨트롤러, 상위 모듈) 에 정의한 카드 결제 인터페이스(추상 타입)에 의존해야 한다.

class PaymentController {
    @RequestMapping(value = "/payment", method = RequestMethod.POST)
    public void pay(@RequestBody CardPaymentDto.PaymentRequest req) {
        final CardPaymentService cardPaymentService = cardPaymentFactory.getType(req.getType());
        cardPaymentService.pay(req);
    }
}

public interface CardPaymentService {
    void pay(CardPaymentDto.PaymentRequest req);
}

public class ShinhanCardPaymentService implements CardPaymentService {
    @Override
    public void pay(CardPaymentDto.PaymentRequest req) {
        shinhanCardApi.pay(req);
    }
}

의존 관계를 인터페이스를 통해서 의존성 역전을 시켰다. 컴파일 단계에서는 PaymentController 는 PaymentService 인터페이스를 바라보지만 런타임에서는 cardPaymentFactory 를 통해 ShinhanCardPaymentService 를 바라보게 된다.

또 하위 정책의 신한 카드(서비스), 우리 카드(서비스) 가 변경되더라도 PaymentService 인터페이스를 의존하고 있으므로 확장에 열려 있고 변경에 닫혀 있는 OCP 를 준수하게 된다.


참고

728x90