[스프링 핵심원리] - 2.스프링 핵심 원리 이해1(예제 만들기) 주문과 할인 도메인 설계, 주문과 할인 도메인 개발, 주문과 할인 도메인 실행과 테스트

Chooooo·2022년 10월 5일
0
post-thumbnail

이 글은 강의 : 김영한님의 - "스프링 핵심원리 - 기본편"을 듣고 정리한 내용입니다. 😁😁


이전까지 회원 도메인에 대한 개발이 끝났다. 하지만 회원만 있다고 서비스가 만들어지는 것은 아니고, 다른 도메인이 필요하다.

이번에는 주문과 할인 도메인에 대해서 설계해보자

주문과 할인 도메인 설계

주문과 할인 정책

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

주문 도메인 협력, 역할, 책임

주문 도메인의 역할에 대해서 살펴보면,

1) 주문 생성: 클라이언트는 주문 서비스에 주문 생성을 요청한다.
실무에서는 상품이라는 객체를 만들어서 구현하지만 여기서는 간단하게 구현하기 위해 데이터(회원 id, 상품명, 상품가격)로 보낸다.
2) 회원 조회: 할인을 위해서 회원 등급이 필요하다. 그래서 주문 서비스는 회원 저장소에서 회원을 조회한다. (회원의 등급을 알기 위해서)
3) 할인 적용: 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임한다.
4) 주문 결과 반환: 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다.
(참고) 실제로는 주문 데이터를 DB에 저장하지만 여기서는 단순히 주문 결과를 반환하도록 구현한다.

해당 사진은 아직 역할(인터페이스)만 나타낸 것이야! 구현체는 뒤에!

주문 도메인 전체

처음에는 역할끼리의 의존관계를 보여줬고, 여기서는 구현체까지도 포함해서 보여준다. 이렇게 되니 원하는 구현체를 언제든지 갈아낄 수 있다는 장점이 보인다.
즉, 역할과 구현을 분리해서 자유롭게 객체를 조립할 수 있도록 설계했다.

주문 도메인 클래스 다이어그램

실제로 코드레벨로 보는 클래스 다이어그램이다. 이미 역할에 대한 설계가 끝났기 때문에 클래스 다이어그램도 명확하게 나온다.
어떤 인터페이스 구현체로 new하는지에 따라서 주문 도메인 객체 다이어그램은 2가지로 나타낼 수 있다.
역할들의 협력 관계를 그대로 재사용할 수 있다.

주문 도메인 객체 다이어그램1

서버에 올라갔을 때 보는 객체 다이어그램이다. 메모리 회원 저장소가 사용될 수 있고, 정액 할인 정책이 사용될 수 있다.

주문 도메인 객체 다이어그램2

구현체를 다르게 변경할 수도 있어. 메모리가 아니라 DB회원 저장소를 사용할 수 있고, 정액이 아닌 정률 할인 정책으로도 사용할 수 있다. 하지만 전혀 협력관계는 변경되지 않는 것을 볼 수 있다.

구현이 아닌 역할을 의존하기 때문에 나오는 좋은 설계의 장점!!!!


주문과 할인 도메인 개발

주문과 할인 도메인에 대한 설계가 끝났으니 개발을 진행하자.

할인 정책 인터페이스

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {
    /**
     * @return 할인 대상 금액
     */
    int discount(Member member, int price);
}

할인 정책 인터페이스는 할인 대상 금액을 리턴해주는 메서드 하나만 가진다.

정액 할인 정책 구현체

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

public class FixDiscountPolicy implements DiscountPolicy {

    private int discountFixAmount = 1000;   // 고정 할인 금액(1000원 할인)

    // 등급에 따른 할인 금액 반환 메서드
    @Override
    public int discount(Member member, int price) {
        // enum 타입은 == 으로 비교
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        } else {
            return 0;
        }
    }
}

DiscountPolicy 인터페이스를 구현한 FixDiscountPolicy
할인 정책 인터페이스의 구현체인 정액할인 정책이다. 인스턴스 변수로 할인해줄 금액을 1000원 가진다. VIP라면 할인 금액 1000원 리턴, BASIC이라면 0원 리턴.
(enum타입은 == 으로 비교해! 이런 것도 기억해두자)

주문 엔티티(도메인)

package hello.core.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 int calculatePrice() {
        return itemPrice - discountPrice;
    }
    
    // getter and setter 생략

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

주문이 완료되었을 때, 나오는 객체 값이고 회원 아이디, 주문 아이템 이름, 주문 아이템 가격, 할인 가격을 필드로 가진다.

주문 서비스 인터페이스

package hello.core.order;

public interface OrderService {
    // 주문 생성 메서드
    Order createOrder(Long memberId, String itemName, int itemPrice);
}

주문 서비스 역할인 인터페이스도 생성해준다. 여기서 회원 아이디와 주문 아이템 이름, 주문 아이템 가격을 받아서 주문(Order) 객체를 만들어서 리턴한다.

주문 서비스 구현체

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{

    // MemoryMemberRepository & FixDiscountPolicy 를 구현체로 생성
    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);            // 해당 id를 가진 회원 조회
        int discountPrice = discountPolicy.discount(member, itemPrice); // 회원의 등급에 따른 할인 금액

        return new Order(memberId, itemName, itemPrice, discountPrice); // 최종 생성된 주문 반환
    }
}

주문 서비스의 구현체(OrderServiceImpl). 여기서는 회원 아이디로 실제 회원을 찾고 할인 정책에 회원을 넘겨서 할인 가격을 받아온 후 주문을 만들어서 리턴한다.
(메모리 회원 리포지토리, 고정 금액 할인 정책을 구현체로 생성)

이 부분에서 단일 책임 원칙이 잘 적용되었다. 할인에 대한 내용이 변경되더라도 주문 서비스는 변경될 것이 없는 것! 오로지 할인 정책에서 할인에 대한 내용을 관리하면 된다.


주문과 할인 도메인 실행과 테스트

주문과 할인 도메인을 만들었으니 제대로 동작하는지 테스트해보자.

package hello.core;

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

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

        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); // order.toString() 출력됨
        //System.out.println("order.calculatePrice = " + order.calculatePrice()); // 할인된 최종 가격
    }
}

main 메서드를 위와 같이 만들고 돌려보면...
정상적으로 출력되는 것을 볼 수 있다. 하지만 main 메서드를 통해서 테스트하는 것은 역시나 좋은 방법은 아니다.

주문과 할인 정책 테스트(JUnit 활용)

package core.order.order;

import core.order.member.*;
import core.order.member.MemberService;
import core.order.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();

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

JUnit으로 테스트..
이렇게 테스트하면 값이 변경되면 테스트에서 실패하기 때문에 훨씬 정확하게 테스트할 수 있다.

profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글