[Java] 새로운 할인 정책 개발과 적용(3)

JJoSuk·2023년 5월 30일
0

본 프로젝트 자료는 김영한님 스프링 핵심 원리 - 기본편을 참고로 제작됐습니다.

새로운 할인 정책 개발

기존에 개발했던 정책에서 기획자가 지금처럼 고정 금액 할인이 아닌 정률% 할인으로 변경하고 싶어한다. 예를 들어 정률을 10%로 지정해두면 10000원 상품은 1000원 할인, 20000원 상품은 2000원 할인으로 말이다.

기존에 있던 FixDiscountPolicy 정책을 빼고 RateDiscountPolicy 정책을 개발해 추가하면 된다.


  • 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에 위반하는 코드 작성법이다.

  • OCP
    지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다(OCP 위반)
  • DIP
    추상 클래스인 DiscountPolicy 는 구체 클래스인 FixDiscountPolicy 와 RateDiscountPolicy 를 의존하고 있다(DIP 위반)

해결 방안은?

  • OrderServiceImpl은 DiscountPolicy 만 의존하는게 아닌 구체 클래스도 같이 의존하기에 구체 클래스를 변경할 때 클라이언트 코드도 같이 변경해야 한다.
  • 그럼 추상에만 의존하도록 변경하면 된다.(의존관계를 변경)

기존

public class OrderServiceImpl implements OrderService {

	private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

변경 후

public class OrderServiceImpl implements OrderService {

	private DiscountPolicy discountPolicy;
}
  • 인터페이스만 의존하도록 설계와 코드를 변경했다.
  • 하지만 이대로 수정을 하게 된다면 인터페이스와 구체의 관계가 끊기게 되는 상황이라 NPE(null pointer exception)가 발생한다.

이에 대한 해결 방안도 간단하게 구현객체를 생성하고 연결하는 책임만 가진 별도의 설정 클래스를 만들면 된다.

AppConfig

public class AppConfig {

	public MemberService memberService() {
		return new MemberServiceImpl(new MemoryMemberRepository());
}
	public OrderService orderService() {
		return new OrderServiceImpl(
			new MemoryMemberRepository(),
			new FixDiscountPolicy());
	}
}
  • AppConfig 에 실제 동작에 필요한 구현 객체를 생성한다.
  • AppConfig 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.

지금은 각 클래스에 생성자가 없어서 컴파일 오류가 발생한다. 바로 다음에 코드에서 생성자를 만든다.

MemberServiceImpl

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 는 MemberRepository 인터페이스만 의존한다.
  • 변경된 MemberServiceImpl 는 실제 값이 들어오기 전 까지 정답을 알 수 없게 됐다.
  • 그리고 MemberServiceImpl 실제 값을 받기 위해서는 오직 AppConfig 에서 결정된다.
  • 마지막으로 의존관계의 대한 고민은 이제 사라지며 오직 실행에만 집중하게 된다.

현재 memberServiceImpl 은 의존관계는 외부에서만 주입해주는 것 같다고 해서 이걸 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라고 한다.


OrderServiceImpl

위와 동일한 방법으로 변경해주면 된다

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);
    }
}
  • OrderServiceImpl 는 FixDiscountPolicy 를 의존하지 않는다
  • DiscountPolicy 인터페이스만 의존한다
  • OrderServiceImpl 는 어떤 구현 객체가 주입될지 모르며, 어떤 구현 객체를 주입할지는 오직 AppConfig 에서 결정된다.
  • OrderServiceImpl 실행에만 집중한다.

이제 나머지 클래스에도 SOLID 방식에 맞게 수정보안하면 된다.

MemberApp

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에 연결

OderApp

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());
    }
}

위와 동일한 방식

이제 제대로 작동하는지 테스트 코드도 오류 수정 후 테스트 진행

MemberServiceTest

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);
    }
}

OrderServiceTest

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 는 각 테스트를 실행하기 전에 호출된다.


AppConfig

중복을 제거하고, 역할에 따른 구현이 보이도록 리팩터링 하자

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 에서 new MemoryMemberRepository() 이 부분이 중복 제거되었다. 이제 MemoryMemberRepository 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.
  • AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.

  • AppConfig 에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy 객체로 변경했다.

  • 이제 할인 정책을 변경해도, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다. 클라이언트 코드인 OrderServiceImpl 를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.

  • 구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자. 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.


전체 흐름 정리

  • 새로운 할인 정책 개발

다형성 덕분에 새로운 정률 할인 정책 코드를 추가로 개발하는 것 자체는 아무 문제가 없음

  • 새로운 할인 정책 적용과 문제점

새로 개발한 정률 할인 정책을 적용하려고 하니 클라이언트 코드인 주문 서비스 구현체도 함께 변경해야함 주문 서비스 클라이언트가 인터페이스인 DiscountPolicy 뿐만 아니라, 구체 클래스인 FixDiscountPolicy 도 함께 의존 DIP 위반

  • 관심사의 분리

    • 애플리케이션을 하나의 공연으로 생각
    • 기존에는 클라이언트가 의존하는 서버 구현 객체를 직접 생성하고, 실행함
    • 비유를 하면 기존에는 남자 주인공 배우가 공연도 하고, 동시에 여자 주인공도 직접 초빙하는 다양한 책임을 가지고 있음
    • 공연을 구성하고, 담당 배우를 섭외하고, 지정하는 책임을 담당하는 별도의 공연 기획자가 나올 시점
    • 공연 기획자인 AppConfig가 등장
    • AppConfig는 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임
    • 이제부터 클라이언트 객체는 자신의 역할을 실행하는 것만 집중, 권한이 줄어듬(책임이 명확해짐)
  • AppConfig 리팩터링

    • 구성 정보에서 역할과 구현을 명확하게 분리
    • 역할이 잘 드러남
    • 중복 제거
  • 새로운 구조와 할인 정책 적용

    • 정액 할인 정책 정률% 할인 정책으로 변경
    • AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리
    • 할인 정책을 변경해도 AppConfig가 있는 구성 영역만 변경하면 됨, 사용 영역은 변경할 필요가 없음. 물론 클라이언트 코드인 주문 서비스 코드도 변경하지 않음

참고 자료 : 김영한님 스프링 핵심 원리 - 기본편

profile
안녕하세요

0개의 댓글