현재는 정액 할인 정책을 채택하여 DiscountPolicy를 구현하고 있는데, 정률 할인 정책을 채택하여 DiscountPolicy를 변경하려한다.
다행히 객체지향적으로 설계해서 유연하게 변화에 대응가능하다.
RateDiscountPolicy 클래스 생성
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
public class RateDiscountPolicy implements DiscountPolicy{
private final int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price * discountPercent / 100;
}
else{
return 0;
}
}
}
테스트 코드 작성
테스트 코드 작성 팁 : command + shift + T 단축키 사용
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class RateDiscountPolicyTest {
RateDiscountPolicy rateDiscountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10프로 할인이 적용되어야 한다.")
void vip_o() {
//given
Member member = new Member(1L, "test", Grade.VIP);
//when
int discount = rateDiscountPolicy.discount(member, 10000);
//then
Assertions.assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("일반 회원은 비율할인이 적용되어야지 않아야 한다.")
void vip_x() {
//given
Member member = new Member(2L, "test2", Grade.BASIC);
//when
int discount = rateDiscountPolicy.discount(member, 10000);
//then
Assertions.assertThat(discount).isEqualTo(0);
}
}
새로운 할인 정책 적용과 문제점
RateDiscountPolicy를 적용하기위해 OrderServiceImpl의 코드를 아래와 같이 변경해야 한다.
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
해당 코드의 문제점 찾기
역할(interface)과 구현(class)는 충실하게 분리하여 구현하였다.
그러면 OCP,DIP를 지키고 있는가?
DIP : OrderServiceImpl는 DiscountPolicy 인터페이스에 의존하는 동시에 RateDiscountPolicy 구현체에도 의존하고 있기에 DIP를 위반한다.
OCP : 코드의 변경없이 기능의 확장이 가능해야하는데 RateDiscountPolicy를 확장하기 위해서 코드의 변경이 불가피하다.따라서 OCP도 위반한다.
의존성 문제해결
아래처럼 인터페이스만을 의존하도록 코드를 바꾸면, NPE(null point exception)이 발생한다. 그래서 옳은 방법은 아니다.
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
NPE 해결방안
관심사의 분리를 해야한다.
예를 들어, 해당 애플리케이션을 하나의 공연이라 하자. 그러면 크게 공연기획자, 배우, 배역이 존재 할 것이다.
공연기획자의 경우 어떠한 배역에 누구를 쓸지 결정한다. 즉 해당 배역을 맡을 배우를 결정할 수 있다.
하지만 위의 DIP와 OCP를 위반하는 코드의 경우 배우가 상대역 배우를 결정하는 구조이다.
코드로 설명하자면 OrderServiceImpl구현체가 RateDiscountPolicy 구현체를 선택하고 있다. 배우가 배우를 고르는 셈이다.
그래서 우리는 공연기획자, 즉 이러한 의존관계를 위한 새로운 클래스가 필요하다.
해당 역할을 바로 AppConfig 클래스가 수행한다.
애플리케이션과 공연을 빗대어서 매칭하면 공연기획자-AppConfig, 배우-구현체, 배역-인터페이스로 볼 수있다.
배우는 배우의 역할, 즉 배역만 수행하도록 하고 공연기획자는 배역에 누구를 쓸지를 정하면 서로의 관심사를 가장 효율적으로 분리한 구조라 볼 수 있다.
AppConfig 구현
hello.core 패키지에 AppConfig 클래스를 생성한다.
생성자를 통해 해당 인터페이스가 어떠한 구현체를 리턴 할지를 결정해준다.
즉, 배역에 들어갈 배우를 결정하는 과정이다.
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
MemberServiceImpl 생성자 주입
오직 인터페이스 memberRepository만 의존하도록 코드 수정이 가능하다.
MemberServiceImpl의 입장에서 본인이 어떠한 클래스를 구현체로 받아들이지는(주입 될지)전혀 모른다.
이는 오직 외부에서 결정되고 주입된다(AppConfig로부터!)
package hello.core.member;
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
OrderServiceImpl 생성자 주입
인터페이스를 두개를 의존하니, AppConfig의 생성자 설정에서도 구현체 두개를 리턴하도록 하면된다.
MemberRepository와 DiscountPolicy에 DI가 진행된다.
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@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);
}
}
테스트 코드 수정
@BeforeEach는 테스트 실행하기 전, 해당 메서드가 실행되도록 하는 어노테이션이다.
class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
}
class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
} }