-
[김영한 스프링] 06. 새로운 할인 정책 적용과 문제점, 관심사의 분리Spring/스프링 핵심 원리 - 기본편 2023. 4. 12. 00:47
할인 정책을 변경하려면 클라이언트인 OrderServiceImpl의 코드를 고쳐야 한다.
문제점 발견
- 우리의 역할과 구현을 충실하게 분리했다 -> OK
- 다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. -> OK
- OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다.
- -> 그렇게 보이지만 사실은 아니다.
- DIP : 주문서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데?
- -> 클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
- 추상(인터페이스) 의존 : DiscountPolicy
- 구체(구현) 클래스 : FixDiscountPolicy, RateDiscountPolicy
- -> 클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
- OCP : 변경하지 않고 확장할 수 있다고 했는데!
- -> 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다.
정책변경
어떻게 문제를 해결할 수 있을까?
- 클라이언트 코드인 OrderServiceImpl은 DiscountPolicy의 인터페이스뿐만 아니라 구체 클래스도 함께 의존한다.
- 그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.
- DIP 위반 -> 추상에만 의존하도록 변경 (인터페이스에만 의존)
- DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.
인터페이스에만 의존하도록 Private DiscountPolicy discountPolicy로 변경
실행 시 NullPoint에러 발생
관심사의 분리
- 애플리케이션을 하나의 공연이라 생각해보자. 각각의 인터페이스를 배역이라 생각하자. 그런데! 실제 배역에 맞는 배우를 선택하는 것은 누가 하는가?
- 로미오와 줄리엣 공연을 하면 로미오 역할을 누가 할지, 줄리엣 역할을 누가 할지는 배우들이 정하는 것이 아니다.
- 이전 코드는 마치 로미오 역할(인터페이스)을 하는 레오나르도 디카프리오(구현체, 배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같다. 디카프리오는 공연도 해야하고 동시에 여자 주인공도 공연에 직접 초빙해야 하는 다양한 책임을 가지고 있다.
관심사를 분리하자
- 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 한다.
- 디카프리오는 어떤 여자 주인공이 선택되더라도 똑같이 공연을 할 수 있어야 한다.
- 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 나올시점이다.
- 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리하자.
main/java/hello.core/AppConfig 생성
지금까지 객체를 생성하고 '이 인터페이스에 어떤 것을 할당해야 해!'라는 것을 MemberServiceImpl이 직접함
MemberServiceImpl안에 MemberRepository와 MemoryMemberRepository을 직접 지정함 -> 배우가 담당 배우를 직접 섭외하는 것과 같은 것이다.
이것을 AppConfig에서 다 하도록 변경 해야함
private final MemberRepository memberRepository = new MemoryMemberRepository();에서 = new MemoryMemberRepository() 제거
생성자 생성
memberRepository에 어떤 구현체가 들어갈지를 생성자를 통해서 확인
new MemoryMemberRepository() 추가(생성자 주입)
MemberServiceImpl에 MemoryMemberRepository는 없고 MemberRepository라는 인터페이스만 있다. -> 추상화에만 의존
OrderService 추가
생성자 생성
인터페이스에만 의존 -> DIP 지키고 있음
구체적인 클래스는 전혀 모름
MemoryMemberRepository가 들어올지, DbMemoryRepository가 들어올지 전혀 모름
대본 보고 연기하듯이 로직만 실행하면 됨
AppConfig.java
package hello.core;
import hello.core.discount.FixDiscountPolicy;
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, MemoryMemberRepository, OrderServiceImpl, FixDiscountPolicy
- 생성한 객체 인스턴스의 참고(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
- MemberServiceImpl -> MemoryMemberRepository
- OrderServiceImpl -> MemoryMemberRepository, FixDiscountPolicy
MemberServiceImpl.java
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);
}
}- MemoryMemberRepository를 의존하지 않는다.
- MemberRepository 인터페이스만 의존한다.
- 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
- 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
- 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
그림 - 클래스 다이어그램
OrderServiceImpl.java
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);
}
}- OrderServiceImpl은 FixDiscountPolicy를 의존하지 않는다!
- DiscountPolicy 인터페이스만 의존한다.
- 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
- 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정한다.
- 이제부터 실행에만 집중하면 된다.
- MemoryMemberRepository, FixDiscountPolicy 객체의 의존관계가 주입된다.
'Spring > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
[김영한 스프링] 08. 좋은 객체 지향 설계의 5가지 원칙 적용, IoC, DI, 그리고 컨테이너 (0) 2023.04.12 [김영한 스프링] 07. AppConfig 리팩터링, 새로운 구조와 할인 정책 적용 (0) 2023.04.12 [김영한 스프링] 05. 새로운 할인 정책 개발 (0) 2023.04.08 [김영한 스프링] 04. 주문과 할인 도메인 (1) 2023.04.07 [김영한 스프링] 03. 회원 도메인 (0) 2023.04.05