[기본기] 5-2. 주문과 할인 도메인 설계

khyojun·2022년 9월 8일
1

본 게시글은 김영한님의 스프링 핵심 원리 기본편을 정리한 글입니다.


📌 주문과 할인 설계

  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수
      있다.)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을
      미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)

위 그림에서와 같이 일단 역할을 부여하였고 이에 대한 구현 부분에 대해서 어떻게 설계를 해야 할 지 그림으로 확인해보자.

이렇게 할 경우 유동적으로 나중에 정책이나 저장소가 변경이 되었을 경우 유연하게 변경을 할 수 있게 된다.
그렇게 되면 클래스, 객체에 대한 다이어그램을 확인해 보면

이렇게 유연하게 변경이 될 수 있다는것을 알 수 있다.
그럼 이제 본격적으로 구현작업을 해보자.

주문과 할인 도메인 개발

📂 할인 정책 인터페이스

public interface DiscountPolicy {

    int discount(Member member, int itemPrice);
}

📂정액 할인 정책 구현체

public class FixDiscountPolicy implements DiscountPolicy{
    private int discountAmount=1000;

    @Override
    public int discount(Member member, int itemPrice) {
        if (member.getGrade() == Grade.VIP){
            return discountAmount;
        }
        else{
            return 0;
        }
    }
}

📂 주문 엔티티

package practicecore.ptcore.order;

public class Order {
    private Long memberId;
    private String itemName;
    private int itemPrice;
    private int discountPrice;

    public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
        this.memberId = memberId;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }

    public String getItemName() {
        return itemName;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public int getItemPrice() {
        return itemPrice;
    }

    public void setItemPrice(int itemPrice) {
        this.itemPrice = itemPrice;
    }

    public Long getMemberId() {
        return memberId;
    }

    public void setMemberId(Long memberId) {
        this.memberId = memberId;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }

    @Override
    public String toString() {
        return "Order{" +
                "itemName='" + itemName + '\'' +
                ", itemPrice=" + itemPrice +
                ", memberId=" + memberId +
                ", discountPrice=" + discountPrice +
                '}';
    }
}

📂 주문 서비스 인터페이스

public interface OrderService {

    Order createOrder(Long memberId, String itemName, int itemPrice);
}

📂 주문 서비스 구현체

public class OrderServiceImpl implements OrderService{
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy(); 

 

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

        int discountP=discountPolicy.discount(member, itemPrice);

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

📌 주문, 할인 도메인 테스트

📂 주문,할인 도메인 테스트 구현

public class OrderServiceTest {
    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();


    @Test
    void create(){
        Long memberId=1L;
        Member member= new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        System.out.println("member.getGrade() = " + member.getGrade());
        Order order = orderService.createOrder(memberId, "itemA", 10000);
     Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

❗ 비상비상 쵸~비상!! 테스트 초~비~상~! NPE 발~생~!

앞에서 나온 코드를 보게 되면 memorymemberrepository에 저장을 하기위하여 MAP을 선언하였던 것을 기억하는가?
앞선 블로그에서는 static이 빠진 아래의 코드로 처음에는 넣었었다.

private final Map<Long, Member> store = new HaspMap<>();

그랬더니 NPE가 터지면서 Discount쪽 문제가 생겼다고 한다. 그러나....

기본기가 너무나도 부족하였던 나 자신을 발견하게 되었다.


//private final Map<Long, Member> store = new HaspMap<>();

private static final Map<Long, Member> store = new HaspMap<>();

위 코드와 달리 밑에는 static이 들어가게 되었다.

🔍 NPE가 일어났던 이유

static을 붙이지 않아서 일어났었는데 일단 오류에 대한 분석을 해보자면 다음과 같다.

  1. 테스트 진행시 회원가입 과정과 주문을 등록하기 위한 과정에서 문제 발생
  2. MemberService, OrderService 에게 인스턴스를 줬을때? 각각의 구현체 코드를 보게 되면 동일하게 memberRepository가 하나씩 선언이 된다.
  3. MemoryMemberRepository에서 store라는 Map이 있는데 각각의 인스턴스의 store가 자꾸 생성이 된다.
  4. 그렇게 됬기 때문에 테스트를 진행하였을때 하나의 store가 아니라 여러개의 store가 생성되어서

public class OrderServiceTest {
    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();
    @Test
   void create(){
        Long memberId=1L;
        Member member= new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

memberService.join, orderService.createOrder 두 가지의 store는 서로 다른 친구이다. 그래서 static을 붙여줌으로써 store가 각각의 인스턴스에게 부여해주는 것을 방지하고 하나의 store만 존재할 수 있도록 하는 작업이 필요하였다.

생각보다 이 문제 잘 못찾고 있어서 되게 고생하고 있었다. ㅠㅠㅠ

📌 좋은 객체 지향 설계 법칙들 잘 지켰나?

주문 할인 서비스까지는 잘 개발을 하였나? 라고 한다면 어긴 법칙들이 있다. OCP, DIP인데 MemberRepository memberRepository = new MemoryMemberRepository(); DiscountPolicy discountPolicy = new FixDiscountPolicy(); 에서 보게 되면 DIP를 어긴 것이 주로 추상체에 집중을 해야하는데 의존하는것이 구현체까지 클라이언트 코드에서 신경을 써줘야 한다는 것이 보인다. 그리고 만약 할인정책을 변경하게 된다면 FixDiscountPolicy라는 코드 자체를 변경해야하므로 OCP도 어기게 되는 부분임을 알 수 있다. 그렇다면 이러한 문제들은 어떻게 해결해야할 것인지 다음 글에서 하나씩 알아보자.

출처

  1. 김영한님의 스프링 핵심 원리 기본편(https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8)
profile
코드를 씹고 뜯고 맛보고 즐기는 것을 지향하는 개발자가 되고 싶습니다

0개의 댓글