public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
}else{
return 0;
}
}
}
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);
}
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x() {
//given
Member member = new Member(2L, "memberBasic", Grade.Basic);
//when
int discount = discountPolicy.discount(member, 10000);
//then
Assertions.assertThat(discount).isEqualTo(1000);
}
}
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
❓ DIP: 주문서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것이 아닌가?
💡 클래스 의존관계를 살펴보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다. 따라서 DIP를 위반한다.
1) 추상(인터페이스) 의존: DiscountPolicy
2) 구체(구현) 클래스 의존: FixDiscountPolicy, RateDiscountPolicy
❓ OCP: 변경하지 않고 확장할 수 있다고 했는데?
💡 지금까지 코드는 기능을 확장해서 변경하면 클라이언트 코드에 영향을 준다. 따라서 OCP를 위반한다.
하지만 실제 의존관계는 클라이언트 OrderServiceImpl이 DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy 구체 클래스도 함께 의존한다. 따라서 DIP를 위반한다.
따라서 정책 변경시(FixDiscountPolicy -> RateDiscountPolicy) OrderServiceImpl의 클라이언트 소스 코드를 함께 변경해야 한다. 결국 OCP를 위반한다.
DIP를 위반하지 않기 위해 인터페이스에만 의존하도록 의존관계를 변경하자.
❗구현체가 없기 때문에 아래 코드 실제 실행시 NPE(Null Pointer Exception)이 발생한다.
💡 누군가 클라이언트 OrderServiceImpl에 DiscountPolicy 구현 객체를 대신 생성하고 주입해주어야 한다.
💡 AppConfig: 일종의 공연 기획자, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스
실제 동작에 필요한 구현 객체 생성: MemberServiceImpl, MemoryMemberRepository, OrderServiceImpl, FixDiscountPolicy
AppConfig는 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입(연결)
MemberServiceImpl -> MemoryMemberRepository
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
@Configuration
public class AppConfig {//애플리케이션에 대한 환경 설정을 다 해주는 역할, ex) 공연 기획자
@Bean
public MemberService memberService() {
return new MemberServiceImpl(MemberRepository());//생성자주입: 생성자를 통해 객체 주입
}
@Bean
public MemoryMemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(MemberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
💡 클라이언트 코드(MemberServiceImpl, OrderServiceImpl)는 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중 가능
💡 클라이언트인 memberServiceImpl 입장에서 의존관계를 외부에서 주입해주는 것과 같다하여 DI(Dependency Injection)이라 한다. 의존관계 주입 또는 의존성 주입 이라 한다.
클래스 다이어그램
객체의 생성과 연결은 AppConfig가 담당
DIP: MemberServiceImpl은 MemberRepository 추상에만 의존하고 구체 클래스는 몰라도 된다.
FixDiscountPolicy를 FixDiscountPolicy구성 영역만 영향을 받고 사용 영역은 전혀 영향을 받지 않는다.