Spring 핵심 원리 - 새로운 할인 정책 개발

김태훈·2023년 1월 9일
0

Spring 핵심 원리

목록 보기
6/15

상황 : 기존의 고정 할인 정책에서 정률 할인 정책으로 변화를 하고자 한다.

1. RateDiscountPolicy 코드 작성

package Goat.core.discount;

import Goat.core.member.Grade;
import Goat.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class RateDiscountPolicyTest {

   DiscountPolicy discountPolicy = new RateDiscountPolicy();
   @Test
   @DisplayName("VIP는 10%할인이 적용되어야만 한다.")
   void vip_o(){
       Member member = new Member(1l,"goat1", Grade.VIP);

       int discount = discountPolicy.discount(member,10000);

       Assertions.assertThat(discount).isEqualTo(1000);
   }

   @Test
   @DisplayName("VIP가 아니면 할인이 적용되지 않습니다.")
   void vip_x(){
       Member member = new Member(1L, "goat2", Grade.BASIC);

       int discount = discountPolicy.discount(member,10000);

       Assertions.assertThat(discount).isEqualTo(1000);
   }


}

2. 할인 정책 변경을 위해 OrderServiceImpl 코드 변경

package Goat.core.order;

import Goat.core.discount.DiscountPolicy;
import Goat.core.discount.FixDiscountPolicy;
import Goat.core.discount.RateDiscountPolicy;
import Goat.core.member.Member;
import Goat.core.member.MemberRepository;
import Goat.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();

//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member resultMember = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(resultMember,itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

이렇게 변경을 하지만 이것이 문제가 있다는 것이다.
바로 '클라이언트의 코드를 변경' 했다는 점인데, 이 점이, 좋은 객체지향 코드의 조건에 어긋난다.

  • DIP : OrderServiceImpl이 결국 추상(인터페이스) 인 DiscountPolicy 뿐 아니라, RateDiscountPolicy(구현체)도 의존하고 있으므로 어긋남.

  • OCP : 할인정책을 변경하기 위해서 OrderServiceImpl 코드도 변경 해야하므로 어긋남

3. DIP를 해결하려면 ?

package Goat.core.order;

import Goat.core.discount.DiscountPolicy;
import Goat.core.discount.FixDiscountPolicy;
import Goat.core.discount.RateDiscountPolicy;
import Goat.core.member.Member;
import Goat.core.member.MemberRepository;
import Goat.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private DiscountPolicy discountPolicy;

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member resultMember = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(resultMember,itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

하지만 DiscountPolicy 의 구현객체가 없다 -> 문제가 생길 수 밖에 없음

final
final은 값이 반드시 할당되어야 함. (생성자 방식이든, 동호로 하든)

4. 해결책 AppConfiguration의 등장

각 클래스마다 생성자를 통해서
AppConfig 파일에서 인터페이스와 구현체를 연결해준다.

package Goat.core.member;

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

위 처럼, MemberServiceImpl 에서 MemoryMemberRepository를 불러오는게 아닌,

package Goat.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);
    }
}

생성자 방식으로 만든다. -> 생성자 주입
이렇게하면 오로지 Interface인 추상에만 의존하는 DIP를 철저히 지킬 수 있다.

그 후, AppConfig 파일에 다음과 같이 작성한다.

package Goat.core;

import Goat.core.member.MemberService;
import Goat.core.member.MemberServiceImpl;
import Goat.core.member.MemoryMemberRepository;

public class AppConfig {
   public MemberService memberService(){
       return new MemberServiceImpl(new MemoryMemberRepository());
   }
}

마찬가지로 OrderService도 적절히 수정해보자.

  • OrderServiceImpl
package Goat.core.order;

import Goat.core.discount.DiscountPolicy;
import Goat.core.member.Member;
import Goat.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 resultMember = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(resultMember,itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}
  • AppConfig
package Goat.core;

import Goat.core.discount.FixDiscountPolicy;
import Goat.core.member.MemberService;
import Goat.core.member.MemberServiceImpl;
import Goat.core.member.MemoryMemberRepository;
import Goat.core.order.OrderService;
import Goat.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는 실제 동작에 필요한 구현 객체를 생성자 주입을 통해 생성, 연결한다.

5. AppConfig를 활용한 실행 코드

package Goat.core;

import Goat.core.member.Grade;
import Goat.core.member.Member;
import Goat.core.member.MemberService;
import Goat.core.member.MemberServiceImpl;
import Goat.core.order.Order;
import Goat.core.order.OrderService;
import Goat.core.order.OrderServiceImpl;

public class OrderApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();

        Long memberId = 1L;
        Member member = new Member(memberId,"Goat", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId,"itemA",10000);
        System.out.println("order = "+order);

    }
}

한가지 예시로만 가져왔다.

궁금증

TEST CODE에서는 왜 이렇게 작성해야 하는걸까?

package Goat.core.member;

import Goat.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

    MemberService memberService;

    @BeforeEach
    public void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }

    @Test
    void join(){
        //given
        Member member = new Member(1L,"Goat1",Grade.VIP);

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);
        //then
        Assertions.assertThat(member).isEqualTo(findMember);
    }
}

앞에서 AppConfig로 바로 불러올 수 없었다.

profile
기록하고, 공유합시다

0개의 댓글