당니의 개발자 스토리

새로운 할인 정책 적용과 문제점 본문

스프링/스프링 핵심 원리 - 기본편

새로운 할인 정책 적용과 문제점

clainy 2024. 1. 18. 02:24

새로운 할인 정책 적용과 문제점

새로운 할인 정책을 한번 적용을 해보겠습니다.

제목이 할인 정책 적용과 문제점이죠. 뭔가 적용하려는데 문제가 생기는 거에요.

먼저, 코드로 가서 RateDiscountPolicy을 적용 하려면, OrderServiceImpl로 가야 됩니다.

여기서 할인 정책을 FixDiscountPolicy을 썼습니다. 이거를 RateDiscountPolicy로 바꿔줘야 합니다.

단순하게 이렇게 바꿔주면 끝납니다.

이 정도만 해도 훌륭한데 지금 중요한 게 있어요.

할인 정책을 애플리케이션에 적용을 한 건데, 할인 정책을 변경하려면 할인 정책의 클라이언트인 OrderServiceImpl에 대한 코드를 고쳐야 됩니다.

여기서 문제점이 발견이 된 거죠.

먼저 우리는 역할과 구현을 충실하게 분리했죠. DiscountPolicy를 인터페이스로 만들고, FixDiscountPolicy과 RateDiscountPolicy으로 구현체를 잘 분리해서 구현을 완성했습니다.

그 다음에 다형성도 활용하고, 인터페이스와 구현 객체를 분리했죠. 이것도 OK 입니다.

그리고 OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다? 그렇게 보이지만 사실은 그게 아니에요. 준수를 안 한 거에요.

DIP부터 살펴보면, 주문 서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데? 그런데 잘 보세요. 한번 클래스 의존 관계를 분석해봐야 돼요.

클라이언트는 추상(인터페이스) 뿐만 아니라, 지금 구체(구현) 클래스에도 의존하고 있습니다.


그림으로 설명해 보겠습니다.

OrderServiceImpl(클라이언트)은 분명히 DiscountPolicy에 의존하고 있습니다.

지금까지 단순히 DiscountPolicy 인터페이스에만 의존한다고 생각했어요.

그런데 실제 까보면,

OrderServiceImplDiscountPolicy 인터페이스 뿐만 아니라, FixDiscountPolicy 라는 구체적인 클래스도 의존하고 있습니다.

실제 코드를 보면, FixDiscountPolicy도 의존하고 있습니다. 코드를 보면, 추상에만 의존을 해야 되는데, 추상에도 의존하고 구체적인 거에도 의존해서 둘 다 의존하고 있는 거에요.

이거는 DIP를 위반하는 거예요. DIP는 의존 관계를 inversion(역전)해서 해야 되는 의존 관계고, 그래서 구체에 의존하지 말고, 항상 "추상화"에만 의존하라는 건데, 추상이 바로 인터페이스죠.

그래서 DIP를 위반하기 때문에 문제가 생긴 거에요.

무슨 문제가 생기냐면, 지금 정책을 바꿨단 말이에요.

OrderServiceImpl이 DiscountPolicy만 보는게 아니라, X친 선을 따라가 보면, FixDiscountPolicy도 의존했기 때문에, 이 FixDiscountPolicy를 빼고, RateDiscountPolicy로 바꿔야 되잖아요.

그래서 FixDiscountPolicy를 RateDiscountPolicy 로 딱 변경하는 순간, OrderServiceImpl의 소스 코드도 함께 변경해야 한다! OCP 위반하게 되는거죠.

어쩔 수 없습니다. 이게 바로 의존성이에요. 의존 관계가 있으면, 결국 내가 의존하는 애를 건드리는 순간, 내 코드를 바꿔야 되는 거에요. 우리가 설계를 잘해놔서 조금만 변경하면 됐지만,

그래도 어쨌든 이건 OCP 위반이에요. 우리가 기대했던 거랑은 완전 다른 거에요.

그럼 코드를 안 고치고 구현체를 변경하는 게 가능한가? 가능합니다. 이 문제를 어떻게 해결할 수 있을까요?

클라이언트 코드인 OrderServiceImpl은 DiscountPolicy 인터페이스 뿐만 아니라 구체 클래스도 함께 의존하죠. 그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 되는 문제가 있었습니다. 그래서 DIP도 위반을 했던 거고요.

DIP를 위반하지 않으려면, 추상에만 의존하도록 해야 돼요. 쉽게 얘기해서 인터페이스에만 의존해야 돼요.

결론적으로, DIP를 위반하지 않게하기 위해선, 인터페이스에만 의존하도록 의존관계를 변경하면 되겠죠.

그럼 어떻게 하면 되는가?

일단 이걸 다 지우고,

이렇게 바꾸면 됩니다.

final을 지운 이유는, final을 쓰면 무조건 값이 할당되어야 되기 때문입니다.

이렇게만 하면, 이제 OrderServiceImplDiscountPolicy 인터페이스에만 의존을 하죠?

그래서 구체에 의존하지 않고, 추상화인 인터페이스에만 의존합니다.


이렇게 인터페이스만 의존하도록 설계와 코드를 변경했습니다. 실행하면 어떻게 될까요?

기존에 만들었던

OrderServiceTest를 돌려보겠습니다. 어떻게 될까요?

당연히 NullPointerException 이 납니다. null에다가 점찍으면, 그니까 null.discount() 이런식으로 하면 NullPointerException이 터지는 겁니다.

왜 그럴까요?

여길 보면, 테스트가 이 createOrder를 호출하는데,

이 discount 메서드를 호출할 때, discountPolicy에는 아무것도 할당이 안 된, NULL인 상태인데 빈깡통에다가 뭘 달라고 요구할 수가 없는거에요. 그러니까 NullPointerException이 터지는 거죠.

그럼 대체 DIP를 어떻게 지킬 수 있는 걸까요? 아니 구체적인 게 있어야 뭐가 돌아가지 인터페이스가 안 돌아가잖아요.

해결방안이 있습니다.

이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImplDiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 합니다. OrderServiceImpl는 코드를 변경할 수가 없잖아요.

OrderServiceImpl 대신 파라미터로 전달하든, 어떻게든 넣어줘야 됩니다. 그렇게 하면 해결이 됩니다.


어쨌든 DIP를 지키긴 했습니다.

그런데 DIP를 지키고나니깐 돌아가지 않습니다. 구현 객체가 할당된 게 없잖아요.

그래서 다음 시간부터 이거를 하나씩 해보겠습니다.