❗ 주문 서비스의 클라이언트(OrderServcieImpl)가 구현 객체인 FixDiscountPolicy에 의존하고 있다. = OrderServiceImpl에서 FixDiscountPolicy 객체를 생성하고 있다. (new FixDiscountPolicy)
기획자 : 기존의 고정 금액 할인이 아닌 주문 금액 당 할인하는 정률 할인으로 변경해주세요.
개발자 : RateDiscountPolicy
(정률 할인)만 추가로 개발해서 쓱 바꿔주면 되겠다 !
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 {
/*
이 Policy가 정말 10퍼센트 할인이 되는지 테스트
성공 테스트 작성 + 실패 테스트도 만들어 봐야함
*/
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
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
assertThat(discount).isEqualTo(0);
}
}
RateDiscountPolicy
를 적용해보자 public class OrderServiceImpl implements OrderService {
**// private final DicountPolicy discountPolicy = new FixDiscountPolicy();**
**private final DicountPolicy discountPolicy = new RateDiscountPolicy();**
}
OrderServiceImpl
을 고쳐야한다.OverServiceImpl
는 DiscountPolicy
인터페이스에 의존함.FixDiscountPolicy
& RateDiscountPolicy
→ 인터페이스(추상)와 구체 클래스(구체)에 모두 의존해서 DIP 위반FixDiscountPolicy
를 RateDiscountPolicy
로 변경), 클라이언트 코드(OrderServiceImpl
)에 영향을 준다.OrderServiceImpl
은 DiscountPolicy
의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존하고 있음 public class OrderServiceImpl implements OrderService {
// 인터페이스 = new 구체 클래스
private final DicountPolicy discountPolicy = new RateDiscountPolicy();
}
public class OrderServiceImpl implements OrderService {
private DicountPolicy discountPolicy;
}
인터페이스에만 의존하지만 구현체가 없다 !
→ 실행하면 NPE(Null Pointer Exception) 발생
⭐ 6. 결국, 이 문제를 해결하려면 누군가가 클라이언트(OrderServiceImpl
)에 DiscountPolicy
의 구현 객체를 대신 생성하고 주입해주어야 한다.
공연을 예로 들면, 배역을 인터페이스라고할 때
배우(구현 객체)가 공연 뿐만 아니라 다른 역할(인터페이스)의 배우(구현체)까지 캐스팅하는 것은 하나의 배우(구현 객체)가 “다양한 책임”을 가지고 있는 것이다.
배우는 본인의 역할인 배역을 수행하는 것에만 집중해야한다.
배역의 배우를 지정하는 일(책임)은 별도의 공연 기획자가 할 일이다.
따라서, 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 분리해보자
애플리케이션의 전체 동작 방식을 구성하기위해, 구현 객체를 생성
하고 연결
하는 책임을 가지는 별도의 설정 클래스
public class MemberServiceImpl implements MemberService {
private final MemberRepository memeberRepostiroy = new MemoryMemberRepository(); (1)
}
(1) MemberServiceImpl
에는 MemberRepostiroy
(인터페이스, 배역)과 동시에 MemoryMemberRepository
(구현체)가 존재한다.
이것은 MemberServiceImpl
이 MemoryMemberRepository
를 선택한 것이고 배우가 배역의 배우를 직접 지정한 것과 같다.
⭐ 배우가 다양한 책임을 지지않도록, AppConfig를 통해 배우(구현 객체)를 지정하자 !
@Configuration
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository()); (1)
}
}
(1) MemberServiceImpl
이 직접 구현체를 지정하지 않고 AppConfig에서 지정할 수 있도록 함수를 생성하고, MemberServiceImpl
에서 사용할 구현체를 매개 변수에 명시해준다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memeberRepostiroy; (1)
public MemberServiceImpl(MemberRepository memberRepository) { (2)
this.memberRepository = memberRepotiroy;
}
}
(1) 구현체를 지우고 인터페이스만 남도록한다.
(2) MemberRepository의 구현체를 생성자 함수의 매개변수로 받아온다.
❗ 이렇게하면
MemberServiceImpl
에는 MemoryMemberRepository가 존재하지 않는다. = 인터페이스(추상화)에만 의존한다 ! = DIP 준수
❗
MemberServiceImpl
의 구현체를 밖에서 생성한 뒤, 생성자를 통해 값을 할당 받을 수 있기 때문에 생성자 주입이라고 한다.
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;
} (1)
...
(1) OrderServiceImpl
구체적인 클래스를 전혀 알 수 없다. FixDiscountPolicy()가 들어올지 RateDiscountPolicy()가 들어올지 전혀 알 수 없다.
@Configuration
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository()); (1)
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}(1)
(1) ⭐⭐⭐ AppConfig는 애플리케이션 실제 동작에 필요한 “구현 객체를 생성”한다. (new 키워드) 이렇게 생성된 객체 인스턴스의 참조(레퍼런스)를 “생성자를 통해서 주입(연결) “해준다. (파라미터를 통해)
=> appConfig
객체는 memoryMemberRepository
객체를 생성하고 그 참조값을 memberServiceImpl
을 생성하면서 생성자로 전달한다.
=> 이를 DI
(의존관계 주입)이라고한다.
MemberServiceImpl
→ MemoryMemberREpository
OrderServiceImpl
→ MemoryMemberRepository
, FixDiscountPolicy
설계를 변경함으로써 MemberServiceImpl
은 MemoryMemberREpository
를 의존하지 않는다 ! → MemberRepository
인터페이스에만 의존한다. ⇒ DIP 준수
MemberServiceImpl
의 입장에서는 생성자를 통해 어떤 구현 객체가 들어올지(주입될지) 알 수 없다.
MemberServiceImpl
의 생성자를 통해서 어떤 구현 객체를 주입할지는 외부(AppConfig
)에서 결정된다.
-> AppConfig
가 객체의 생성과 연결을 담당한다 !
MemberServiceImpl
은 이제부터 의존 관계에 대한 고민은 외부에 맡기고(= 추상에만 의존하고, 구체 클래스를 몰라도 된다) 실행에만 집중할 수 있다.
OrderServiceImpl
은 FixDiscountPolicy
를 의존하지 않음!DiscountPolicy
인터페이스만 의존한다.OrderServiceImpl
에는 MemoryMemberRepository, FixDiscountPolicy
객체의 의존 관계가 주입된다.AppConfig
를 실행해보며 의존 관계에 대해 생각해보자.
public class MemberApp {
public static void main(String[] args){
AppConfig appConfig = new AppConfig();
// MemberServie memeberService = new MemberServiceImpl(); (1)
MemberServie memberService = appConfig.memberService(); (2)
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find member = " + findMember.getName());
}
}
(1) 기존엔 직접 MemberServiceImpl
을 생성하고, 그 다음에 MemberServiceImpl
에서 FixDiscountPolicy
를 순차적으로 생성함
(2) 생성자 주입 후에는 AppConfig
에게 MemberService
를 달라고 하면 인터페이스의 구현체 들을 넘겨준다.
public class MemberServiceTest {
MemberService memberService;
@BeforeEach // 테스트 실행 전에 무조건 실행되는 것
public void beforeEach() {
// 가장 먼저 AppConfig를 생성해주자
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join() {
// given
Member member = new Member(1L, "memberA", Grade.VIP);
// when
memberService.join(member);
Member findMember = memberService.findMember(1L);
// then 검증
Assertions.assertThat(member).isEqualTo(findMember);
}
}
AppConfig
가 구체 클래스를 선택해주니까 인터페이스들은 구현체를 몰라도 괜찮고 실행하는 책임만 지면 된다.
현재 AppConfig는 “중복"이 있고, “역할"에 따른 “구현”이 잘 안 보인다. ⇒ 구성 정보
에는 역할에 따른 구현이 잘 보이는 것이 중요하다.
// 현재 AppConfig
@Configuration
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository()); (1)
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
new MemoryMemberRepository
부분을 드래그 한 뒤, Extract Method으로 memberRepository
함수 생성public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
// return type은 인터페이스(역할)가 되도록 작성
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
이제 할인 정책을 고정 할인 정책에서 정률 할인 정책으로 변경하자
어떤 부분을 변경해야할지 고민해보자
기존에는 클라이언트 코드까지 변경했어야하지만, AppConfig의 등장으로 애플리케이션이 크게 사용 영역 / 구성 영역(객채 생성 및 구성)으로 분리되었다.
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy(); (1)
}
(1) 기존 할인 정책 대신 새로운 할인 정책의 구현체를 생성 후 리턴한다.
❗ 이제 할인 정책을 변경해도, 클라이언트 코드(
OrderServiceImpl
)를 포함한 사용 영역의 어떠한 코드도 변경할 필요가 없다 ! 구성 영역의 AppConfig만 변경하면 된다.