새로운 할인 정책 적용과 문제점
할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// 할인 정책 변경, OCP 원칙 위반, DIP 원칙 위반(추상화 뿐만 아니라 구체화 의존)
private final DiscountPolicy discountPolicy = new RateDiscountPolicy()
문제점 발견
- 우리는 역할과 구현을 충실하게 분리했나? => OK
- 다형성도 활용하고, 인터페이스와 구현 객체를 분리했나? => OK
- OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했나? =>그렇게 보이지만 사실은 아니다.
DIP: 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데?
- 클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
- 추상(인터페이스) 의존: DiscountPolicy
- 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
- OCP: 변경하지 않고 확장할 수 있다고 했는데!
- 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다.
왜 클라이언트 코드를 변경해야 할까?
클래스 다이어그램으로 의존관계를 분석해보자.
기대했던 의존관계
실제 의존관계
클라이언트인 OrderServiceImpl 이 DiscountPolicy 인터페이스 뿐만 아니라
FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다. => DIP 위반
정책 변경
중요!
그래서 FixDiscountPolicy 를 RateDiscountPolicy 로 변경하는 순간
OrderServiceImpl 의 소스코드도 함께 변경해야 한다! => OCP 위반
어떻게 문제를 해결할 수 있을까?
- 클라이언트 코드인 OrderServiceImpl 은 DiscountPolicy 의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존한다.
- 그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.
- DIP 위반 => 추상에만 의존하도록 변경(인터페이스에만 의존)
- DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.
- 즉, 구체 클래스를 지워버리면 된다!
인터페이스에만 의존하도록 설계를 변경
인터페이스에만 의존하도록 코드 변경
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private final DiscountPolicy discountPolicy;
- 인터페이스에만 의존하도록 설계와 코드를 변경했다.
- 그런데 구현체가 없는데 어떻게 코드를 실행할 수 있을까?
- 실제 실행을 해보면 NPE(null pointer exception)가 발생한다.
해결방안
- 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.
AppConfig 등장
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고,
연결하는 책임을 가지는 별도의 설정 클래스를 만들자.
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
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());
}
}
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
- MemberServiceImpl
- MemoryMemberRepository
- OrderServiceImpl
- FixDiscountPolicy
AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
- MemberServiceImpl => MemoryMemberRepository
- OrderServiceImpl => MemoryMemberRepository , FixDiscountPolicy
각 클래스에 생성자를 만든다.
MemberServiceImpl - 생성자 주입
package hello.core.member;
public class MemberServiceImpl implements MemberService{
// MemberRepository 인터페이스만 의존한다.
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);
}
}
- 설계 변경으로 MemberServiceImpl 은 MemoryMemberRepository 를 의존하지 않는다!
- 단지 MemberRepository 인터페이스만 의존한다.
- MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
- MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서 결정된다.
- MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
클래스 다이어그램
- 객체의 생성과 연결은 AppConfig 가 담당한다.
- DIP 완성: MemberServiceImpl 은 MemberRepository 인 추상에만 의존하면 된다.
- 이제 구체 클래스를 몰라도 된다.
- 관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
회원 객체 인스턴스 다이어그램
- appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl 을 생성하면서 생성자로 전달한다.
- 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다
OrderServiceImpl - 생성자 주입
public class OrderServiceImpl implements OrderService{
// 인터페이스만 의존하도록 변경 => DIP 원칙
private final DiscountPolicy discountPolicy;
private final MemberRepository memberRepository;
// 생성자 주입
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 인터페이스만 의존한다.
- OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
- OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부( AppConfig )에서 결정한다.
- OrderServiceImpl 은 이제부터 실행에만 집중하면 된다.
- OrderServiceImpl 에는 MemoryMemberRepository , FixDiscountPolicy 객체의 의존관계가 주입된다.
AppConfig 실행
public class MemberServiceTest {
MemberService memberService;
// 테스트 코드에서 @BeforeEach 는 각 테스트를 실행하기 전에 호출된다.
// 테스트가 2개 있다면 2번 실행됨
@BeforeEach
public void beforeEach() {
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를 보면 중복이 있고, 역할에 따른 구현이 잘 안보인다.
기대하는 그림
리팩터링 전
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());
}
}
- 중복을 제거하고, 역할에 따른 구현이 보이도록 리팩터링 하자.
리펙토링 후
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
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(
memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
- new MemoryMemberRepository( ) 이 부분이 중복 제거되었다.
- 이제 MemoryMemberRepository 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.
- AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다.
- 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.
새로운 구조와 할인 정책 적용
- 처음으로 돌아가서 정액 할인 정책을 정률% 할인 정책으로 변경해보자.
- FixDiscountPolicy => RateDiscountPolicy
- 어떤 부분만 변경하면 되겠는가?
AppConfig의 등장으로 애플리케이션이 크게 사용 영역과,
객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다.
FixDiscountPolicy RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고,
사용 영역은 전혀 영향을 받지 않는다.
할인 정책 변경 구성 코드
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
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(
memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy(); // 한줄 변경
}
}
- AppConfig 에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy => RateDiscountPolicy 객체로 변경했다.
- 이제 할인 정책을 변경해도, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다.
- 클라이언트 코드인 OrderServiceImpl 를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.
- 구성 영역은 당연히 변경된다.
- 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자.
- 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.
'Spring' 카테고리의 다른 글
[Spring] 스프링 컨테이너 (0) | 2024.06.12 |
---|---|
[Spring] IoC, DI 컨테이너 (0) | 2024.06.12 |
[Spring] 객체 지향 설계 SOLID 원칙 (0) | 2024.06.10 |
[Spring] AOP (0) | 2024.06.10 |
[Spring] 스프링 DB 접근 기술 (0) | 2024.06.10 |