2.4 새로운 할인 정책 개발

새로운 할인 정책 개발

  • 정말 객체지향 설계 원칙을 잘 준수 했는지 확인해보자. 이번에는 주문한 금액의 %를 할인해주는 새로운 정률 할인 정책을 추가하자.

RateDiscountPolicy 추가

Pasted image 20250317114543.png


control + shift + T 테스트 클래스 자동 생성

  • JUnit5 사용 Pasted image 20250317114814.png Pasted image 20250317114932.png
  • 테스트코드 작성
class RateDiscountPolicyTest {  
  
    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();  
  
    @Test  
    @DisplayName("VIP는 10% 할인이 적용되어야 함")  
    void vip_o() {  
        //given  
        Member member = new Member(1L, "memberVIP", Grade.VIP);  
        //when  
        int discount = discountPolicy.discount(member, 10000);  
        //then  
        Assertions.assertThat(discount).isEqualTo(1000); // VIP의 할인금액 1000원  
  
    }  
  
    @Test  
    @DisplayName("VIP가 아니라면 할인이 적용되면 안됨")  
    void vip_x() {  
        //given  
        Member member = new Member(1L, "memberBASIC", Grade.BASIC);  
        //when  
        int discount = discountPolicy.discount(member, 10000);  
        //then  
        Assertions.assertThat(discount).isEqualTo(1000); // VIP의 할인금액 1000원  
  
    }  
}

Pasted image 20250317115640.png

Expected :1000 // 기대값
Actual   :0    // 실제값

Static import 하기

  • Add on-demand static import for ... Pasted image 20250317120025.png
import static org.assertj.core.api.Assertions.*;
  • 스테틱 임포트 후엔, 함수명만 작성해서 사용할 수 있다.
@Test  
@DisplayName("VIP는 10% 할인이 적용되어야 함")  
void vip_o() {  
    //given  
    Member member = new Member(1L, "memberVIP", Grade.VIP);  
    //when  
    int discount = discountPolicy.discount(member, 10000);  
    //then  
    assertThat(discount).isEqualTo(1000); // VIP의 할인금액 1000원  
}

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

  • OrderServiceImpl
public class OrderServiceImpl implements OrderService {  
  
    private final MemberRepository memberRepository = new MemoryMemberRepository();  
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();  
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();  
  
    @Override  
    public Order createOrder(Long memberId, String itemName, int itemPrice) {  
        Member member = memberRepository.findById(memberId);  
        int discountPrice = discountPolicy.discount(member, itemPrice);  
  
        return new Order(memberId, itemName, itemPrice, discountPrice);  
    }  
}

기존 할인정책 (new FixDiscountPolicy(); 구현체) 를 -> 신규 할인정책 (new RateDiscountPolicy(); 구현체)로 교체

할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.

문제점 발견

  • 우리는 역할과 구현을 충실하게 분리했다. OK

  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. OK

  • OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다? 그렇게 보이지만 사실은 아니다.

  • DIP: 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데? -> 클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다. 추상(인터페이스) 의존: DiscountPolicy 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy OCP: 변경하지 않고 확장할 수 있다고 했는데! -> 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다.

클라이언트인 OrderServiceImpl은 추상에만 의존해야하지만, 구체(구현)클래스에도 의존하고있다. -> DIP(의존관계 역전, 인터페이스에 의존할것)를 위반하고있다. Pasted image 20250317120814.png


어떻게 문제를 해결할 수 있을까?

  • DIP 위반 추상에만 의존하도록 변경(인터페이스에만 의존)
  • DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.

인터페이스에만 의존하도록 설계를 변경하자.

	private final DiscountPolicy discountPolicy = new RateDiscountPolicy();  //구현체
	-> 변경
    private final DiscountPolicy discountPolicy; // 추상화
  • 하지만 구현 객체가 없어 오류 발생. Pasted image 20250317121138.png

해결방안

  • 이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.

관심사의 분리

  • 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 한다.
  • 디카프리오는 어떤 여자 주인공이 선택되더라도 똑같이 공연을 할 수 있어야 한다.
  • 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 나 올시점이다.
  • 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리하자.

즉, OrderServiceImpl이 구현 객체를 생성하고, 연결하는 책임을 지게 하면 안된다.

AppConfig 등장

  • 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, (구성 == 설정) 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들자.

AppConfig

public class AppConfig {  
  
    public MemberService memberService() {  
        return new MemberServiceImpl(new MemoryMemberRepository());  
    }  
}

MemberServiceImpl

public class MemberServiceImpl implements MemberService {
	// 레파지토리 구현체 생성  
	private final MemberRepository memberRepository;  
	  
	//생성자로 의존성 주입  
	// MemberRepository 객체를 인자로 받아, 삽입  
	public MemberServiceImpl(MemberRepository memberRepository) {  
	    this.memberRepository = memberRepository;  
	}
}
  • 생성자 주입

  • 다음과 같이 구현하면 MemberServiceImpl는 구현객체와 무관하게 코드를 작성할 수 있다. -> DIP를 지킬 수 있다.

appConfig Pasted image 20250317122417.png

  • appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl 을 생성하면서 생성자로 전달한다.
  • 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다.
  • 이제 Service는 실행에만 집중하면 된다.

테스트 코드 변경

public static void main(String[] args) {  
    AppConfig appConfig = new AppConfig();  
    MemberService memberService = appConfig.memberService();  
    OrderService orderService = appConfig.orderService();
}

beforeEach로 서비스 초기화

//테스트를 실행하기 전, 무조건 실행됨 (테스트 케이스마다 각각)  
@BeforeEach  
public void beforeEach() {  
    AppConfig appConfig = new AppConfig();  
    memberService = appConfig.memberService();  
}
  • 동일한 로직 Test 성공 Pasted image 20250317134652.png

정리

  • AppConfig를 통해서 관심사를 확실하게 분리했다.
  • 배역, 배우를 생각해보자. AppConfig는 공연 기획자다.
  • AppConfig는 구체 클래스를 선택한다. 배역에 맞는 담당 배우를 선택한다.
  • 애플리케이션이 어떻게 동작해야 할 지 전체 구성을 책임진다.
  • OrderServiceImpl 은 기능을 실행하는 책임만 지면 된다.

AppConfig리펙토링

현재 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 안보인다. 중복을 제거하고, 역할에 따른 구현이 보이도록 리펙터링

control + alt + M

Pasted image 20250317135331.png

public MemberService memberService() {  
    return new MemberServiceImpl(new MemoryMemberRepository());  
}

----->

public MemberService memberService() {  
    return new MemberServiceImpl(memberRepository());  
}  
  
private MemoryMemberRepository memberRepository() {  
    return new MemoryMemberRepository();  
}

new MemoryMemberRepository() 이 부분이 중복 제거되었다. 이제 MemoryMemberRepository 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.

  • AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다.
  • 애플리케이션 전체 구성이 어떻게 되어있는지 빠르 게 파악할 수 있다

새로운 구조에서 할인정책 적용

  • 처음으로 돌아가서 정액 할인 정책을 정률% 할인 정책으로 변경해보자.
  • FixDiscountPolicy RateDiscountPolicy
  • 어떤 부분만 변경하면 되겠는가? -> AppConfig

AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분 리되었다

Pasted image 20250317135544.png

appConfig

public class AppConfig {  
  
...
  
    private DiscountPolicy discountPolicy() {  
//        return new FixDiscountPolicy();  //기존 고정할인
        return new RateDiscountPolicy();  // 신규 정률할인
    }  
}

한 줄만 바꾸면 된다.

출력

order = Order{memberId=1, itemName='itemA', itemPrice=20000, discountPrice=2000}
order.calculatePrice = 18000

정률 할인 정책이 성공적으로 적용된 모습.

  • FixDiscountPolicy -> RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다.