스프링

스프링 기본원리 - 객체지향 원리 적용

ho코딩 2024. 2. 13. 13:30

이번에는 기존에 작성한 '정액 할인 정책'에서 -> '정률 할인 정책'을 적용하기 위해

'정률 할인 정책' 구현체를 작성하고, 지금까지 작성된 코드 중 SOLID 원칙에 위배되는 점을 찾아 고쳐보겠습니다. 

 

 

 

DiscountPolicy라는 인터페이스에 '정액 할인 정책' / '정률 할인 정책' 구현체를 만들어 놓고 

적용되는 정책에 따라 조립하듯 끼워넣기만 하면 됩니다. 

 

 

RateDiscountPolicy

 

 

discountPercent라는 할인율은 10%으로 정해두고 

 

DiscountPolicy 인터페이스에서 선언한 메서드인 discount를 재정의하기 위해 Overide 어노테이션을 붙입니다. 

 

이 역시도 VIP인 경우 10%할인 그 외 BASIC인 경우 할인 적용 X를 하기 위해 IF문을 작성해주고 

discountPercent는 정수로 입력했기 때문에 백분율 적용을 위해 /100을 해줍니다.

 

- 이제 정률할인 정책 구현체까지 작성했으니 OrderService에서 끼워넣어봅시다. 

 

 

 

 

 

-기존 FIxDiscountPolicy를 주석처리 해주고 RateDiscountPolicy를

DiscountPolicy 인터페이스의 객체인 discountPolicy에 적용해줍니다. 

 

이렇게 하면 정액할인 -> 정률할인으로 잘 바뀌게 됩니다. 

 

하지만 문제점이 있습니다. 

 

SOLID 관련 문제점

 

동작은 잘 수행되고 있으나, 객체지향 원칙인 SOLID 원칙을 모두 잘 준수하고 있을까요? 

 

1. 역할과 구현을 충실하게 분리했나? -> 인터페이스와 구현체를 잘 분리했습니다 O

2. OCP/ DIP 원칙은 잘 준수했나? -> 그렇게 보이긴 하지만 그렇지 않습니다. 

 

바로 위 코드에서 

private final DiscountPolicy discountPolicy = new RateDiscountPolicy() ; 부분을 보면 

DiscountPolicy라는 인터페이스에도 의존하고 있고, RateDiscountPolicy라는 구현체에도 의존해있습니다. 

 

다시말해서, OrderServiceImpl 클래스는 추상(인터페이스)인 DiscountPolicy에도 의존하고 있고, 

구현 클래스인 FixDiscountPolicy와 Rate~에도 의존하고 있습니다. -> DIP 위반 (인터페이스에만 의존해야함)

 

더군다나, 각 정책을 바꿀 때마다 OrderServiceImpl 중 Fix와 Rate 정책이 변동 될때마다 코드를 바꿔야합니다. 

-->  OCP위반 (변경하지 않고 확장할 수 있다고 했는데,, 현재는 기능을 확장해서 변경하면 클라이언트 코드에 영향!)

 

 

그림으로 설명해보자면 

이렇게 DiscountPolicy에만 의존하길 기대했지만 

 

현실은 

 

인터페이스와 구현체 모두에 기대고 있는 상황입니다. 

 

그렇다면 어떻게 인터페이스에만 의존하도록 설계를 변경할 수 있을까요? 

 

 

이처럼 먼저 private DiscountPolicy discountPolicy; 만 작성하여 구현체 내용을 빼주면 됩니다. 

 

그럼 잘 작동될까요?

= 될리가 없습니다. 어떤 방법으로 할인 정책을 실행할지 OrderServiceImpl에게 안 알려줬으니까요.

-> 실행하게 되면 NullPointException 오류가 나타납니다. 

 

그럼 대체 OrderServiceImpl에 구현체를 의존하지 않으면서 어떻게 할인 정책을 적용시키나요 ,, 

-> 누군가가 이 구현체를 대신 주입시켜준다면 ..? 

 

 

 

DIP/OCP 해결방안 

 

인터페이스와 구현체를 각각 분리하려면 어떻게 해야할지 고민하던 중 누군가가 구현체를 대신 주입해주는 아이디어를 떠올리게 됩니다. (물론 제가 떠올린건 아닙니다 ;;) 

 

공연을 예로 들어보겠습니다. 

공연에는 여러 역할이 필요합니다. 

1. 배역 = 로미오 , 줄리엣 등등 
2. 배우 = 레오나르도 디카프리오 , 클레어 데인즈 등등 
3. 공연 기획자 = 감독, 작가 등등 

 

이를 애플리케이션에 적용시킨다면 

배역 = 인터페이스 
배우 = 구현체 
공연 기획자 = ?? 

 

만약 공연을 한다면 각 배역을 배우가 선택할까요? 보통 감독이나 여러 기획자들이 캐스팅을 하기 마련입니다. 

 

현재까지 작성한 애플리케이션은 , 구현체가 공연기획자 역할까지 하는 다양한 책임을 가지고 있습니다. 

 

때문에, 우리는 애플리케이션에서 공연 기획자의 역할이 필요합니다. 

 

 

AppConfig

 

config (구성) 이라는 뜻의 공연기획자 파일을 만듭니다. 

Appconfig에서는 애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가집니다.

 

 

MemberService 객체를 생성하여 MemberServiceImpl에 MemoryMemberRepository를 주입하고. 

OrderService 객체를 생성하여 OrderServiceImpl에 MemoryMemberRepository/ FixDiscountPolicy를 주입합니다. 

 

현재 Appconfi 파일에서 

MemberServiceImpl -> MemoryMemberRepository 

OrderServiceImpl -> MemoryMemberReposiotry, FIxDiscountPolicy 를 각각 주입해주었습니다 .

하지만 현재는 MemberServiceImpl과OrderServiceImpl에 각각 생성자를 만들지 않은 상태이므로 주입이 되지 않습니다. 

 

때문에  MemberServiceImpl에는   

public MemberServiceImpl (MemberRepository memberRepository){
     this.memberRepository = memberRepository;
}

를 작성하여 객체를 생성해줍니다. 

 

여기까지하면, 일단  MemberServiceImpl은 구현체를 의존하지 않고 인터페이스에만 의존하게 됩니다.

 

또한,  MemberServiceImpl입장에서는 생성자를 통해 어떤 구현 객체가 주입될지 모르게 됩니다. 

-  > 오직 Appconfig(공연기획자)에서 결정됩니다. 

->  MemberServiceImpl 은 이제 의존관계에 대한 고민은 외부(Appconfig)에 맡기고 실행에만 집중합니다.  

 

DIP 완성 (MemberServiceImpl은 인터페이스에만 의존)

 

 

 

이번에는 OrderServiceImpl에 생성자를 주입합니다 . 

public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {         this.memberRepository = memberRepository;
 this.discountPolicy = discountPolicy;
}

 

이제 OrderServiceImpl 에서도 FixDiscountPolicy를 의존하지 않고, 인터페이스에만 의존합니다. 

 

 

정리

 

-AppConfig를 통해서 관심사(의존/실행)을 확실하게 분리했습니다. 

-공연을 예로 들면 , 배역(인터페이스) , 배우(구현체), 공연기획자(AppConfig)

-AppConfig는 구체 클래스를 선택하고 애플리케이션의 전체적인 구성을 책임집니다.