본 프로젝트 자료는 김영한님 스프링 핵심 원리 - 기본편을 참고로 제작됐습니다.
기존에 개발했던 정책에서 기획자가 지금처럼 고정 금액 할인이 아닌 정률% 할인으로 변경하고 싶어한다. 예를 들어 정률을 10%로 지정해두면 10000원 상품은 1000원 할인, 20000원 상품은 2000원 할인으로 말이다.
기존에 있던 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;
}
}
}
기존 1000원 할인에서 10%로 교체 적용
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
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); // 설정된 값보다(0) 더 들어가면 오류 발생할 수 밖에 없다
}
}
기존에 적용되어 있던 할인 정책을 새로 개발한 정률 정책으로 교체 적용해본다.
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
코드도 정상 작동하며 문제점이 없지만, 처음에 배우고자 하는 SOLID에 위반하는 코드 작성법이다.
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
public class OrderServiceImpl implements OrderService {
private DiscountPolicy discountPolicy;
}
이에 대한 해결 방안도 간단하게 구현객체를 생성하고 연결하는 책임만 가진 별도의 설정 클래스를 만들면 된다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
new MemoryMemberRepository(),
new FixDiscountPolicy());
}
}
지금은 각 클래스에 생성자가 없어서 컴파일 오류가 발생한다. 바로 다음에 코드에서 생성자를 만든다.
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);
}
}
현재 memberServiceImpl 은 의존관계는 외부에서만 주입해주는 것 같다고 해서 이걸 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라고 한다.
위와 동일한 방법으로 변경해주면 된다
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;
}
@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);
}
}
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
// MemberService memberService = new MemberServiceImpl();
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());
}
}
기존에 있는 의존관계를 정리하고 AppConfig에 연결
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
OrderService orderService = appConfig.orderService();
MemberService memberService = appConfig.memberService();
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order = " + order.toString());
}
}
위와 동일한 방식
이제 제대로 작동하는지 테스트 코드도 오류 수정 후 테스트 진행
public class MemberServiceTest {
//Member domain design
MemberService memberService;
@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
assertThat(member).isEqualTo(findMember);
}
}
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void createOrder() {
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
여기서 수정된 @BeforeEach 는 각 테스트를 실행하기 전에 호출된다.
중복을 제거하고, 역할에 따른 구현이 보이도록 리팩터링 하자
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
private static MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.
AppConfig 에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy 객체로 변경했다.
이제 할인 정책을 변경해도, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다. 클라이언트 코드인 OrderServiceImpl 를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.
구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자. 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.
다형성 덕분에 새로운 정률 할인 정책 코드를 추가로 개발하는 것 자체는 아무 문제가 없음
새로 개발한 정률 할인 정책을 적용하려고 하니 클라이언트 코드인 주문 서비스 구현체도 함께 변경해야함 주문 서비스 클라이언트가 인터페이스인 DiscountPolicy 뿐만 아니라, 구체 클래스인 FixDiscountPolicy 도 함께 의존 DIP 위반
관심사의 분리
AppConfig 리팩터링
새로운 구조와 할인 정책 적용
참고 자료 : 김영한님 스프링 핵심 원리 - 기본편