[Spring Boot] [3] 2. 스프링 핵심 원리 이해 1_예제 만들기 (2)

윤경·2021년 8월 15일
0

Spring Boot

목록 보기
21/79
post-thumbnail

📌 [Spring Boot] [3] 2. 스프링 핵심 원리 이해 1_예제 만들기 (1)


5️⃣ 회원 도메인 실행과 테스트

✔️ hello.core 패키지 안에 MemberApp.java 클래스 생성 (순수 자바로만)

package hello.core;

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

public class MemberApp {

    public static void main(String[] args) {

        MemberService memberService = new MemberServiceImpl();
        // 아래 상태에서 command + option + v 단축키 이용하면 2줄 아래의 내용 생성
        // new Member(1L, "memberA", Grade.VIP);    // L은 Long 타입을 의미
        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());

    }
}

⬇️ 결과

📌 (단축키) psvm + enter ➡️ public static void main(String[] args){} 생성

‼️ 애플리케이션 로직으로 이렇게 테스트 하는 것은 좋은 방법이 아님! JUnit 테스트를 사용하자.

✔️ test 안에 hello.core 패키지 안에 member 패키지 생성
✔️ member 패키지 안에 MemberServiceTest.java 클래스 생성

package hello.core.member;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.util.Assert;

public class MemberServiceTest {

    MemberService memberService = new MemberServiceImpl();

    @Test
    void join() {
        // given: 이러이러한게
        Member member = new Member(1L, "memberA", Grade.VIP);

        // when: 주어졌을 때
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        // then: 이렇게 된다.
        Assertions.assertThat(member).isEqualTo(findMember);

    }
}

⬆️ 결과 (이렇게 되면 잘 되었다는 뜻)

Test 작성 방법을 공부하는 것은 선택이 아닌 필수

회원 도메인 설계의 문제점

  • 이 코드의 설계상 문제점은 무엇일까?
  • ➡️ 다른 저장소로 변경할 때 OCP 원칙을 잘 준수할까?
  • ➡️ DIP를 잘 지키고 있을까?

📌 의존 관계가 인터페이스 뿐만 아니라 구현까지 모두 의존한다는 문제점이 있다. (주문까지 만들어놓고 문제점과 해결 방안을 설명할 것)

아래를 참고.


6️⃣ 주문과 할인 도메인 설계

주문과 할인 정책을 다시 살펴보면

  • 회원은 상품을 주문할 수 있음
  • 회원 등급에 따라 할인 정책 적용 가능
  • 할인 정책: 모든 VIP 1000원 할인 (추후 변경 가능)
  • 할인 정책은 변경 가능성 높음. 회사는 기본 할인 정책을 아직 정하지 못했으며, 오픈 직전까지 고민을 미루고 싶어함. 최악의 경우 할인 적용 X. (미확정)

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


1. 주문 생성: 클라이언트는 주문 서비스에 주문 생성을 요청
2. 회원 조회: 할인을 위해서는 회원 등급이 필요. 그래서 주문 서비스는 회원 저장소에서 회원을 조회
3. 할인 적용: 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임
4. 주문 결과 반환: 주문 서비스는 할인 결과를 포함한 주문 결과를 반환

(참고): 실제로는 주문 데이터를 DB에 저장. 우리는 예제이기 때문에 복잡해질 수 있어 생략하고 단순히 주문 결과만 반환할 것.

주문 도메인 전체

구현과 역할을 분리하여 자유롭게 구현 객체를 조립할 수 있도록 설계. 덕분에 회원 저장소는 물론, 할인 정책도 유연하게 변경 가능해짐.

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

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

➡️ 회원을 (DB가 아닌) 메모리에서 조회하고, (정률이 아닌) 정액 할인 정책(고정 금액)을 지원해도 주문 서비스 (주문 서비스 구현체)를 변경하지 않아도 됨. 역할들의 협력 관계를 그대로 재사용 가능.

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

➡️ 회원을 메모리가 아닌 실제 DB에서 조회하고 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주문 서비스를 변경하지 않아도 됨. 협력 관계 그대로 재사용


7️⃣ 주문과 할인 도메인 개발

✔️ hello.core 밑에 discount 패키지 생성
✔️ discount 패키지 밑에 DiscountPolicy.java 인터페이스 생성

package hello.core.discount;

import hello.core.member.Member;

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

✔️ 인터페이스를 만들었으니 구현체를 만들자. discount 패키지에 FixDiscountPolicy.java 클래스 생성

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) {
        if(member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        } else {
            return 0;
        }
    }
    
}

✔️ hello.core 밑에 order 패키지 생성 후 Order.java 클래스 생성

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, setter
    public Long getMemberId() {
        return memberId;
    }

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

    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 int getDiscountPrice() {
        return discountPrice;
    }

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

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

📌 command + n ➡️ tostring() 검색해 생성 (출력할 때 보기 쉽게 하기 위함)

✔️ order 패키지 밑에 OrderService.java 인터페이스 생성

package hello.core.order;

public interface OrderService {
        Order createOrder(Long memberId, String itemName, int itemPrice);
}

✔️ order 패키지 밑에 OrderServiceImpl.java 클래스 생성

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.MemoryMemberReposiotry;

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository = new MemoryMemberReposiotry();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();


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

        // 할인 정책은 오직 discountPolicy에게 맡기고 있기 때문에 설계가 잘 된 케이스 (단일 체계 원칙)
        int discountPrice = discountPolicy.discount(member, itemPrice);

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

➡️ 주문 생성 요청이 오면 회원 정보를 조회, 할인 정책을 적용한 다음 주문 객체를 생성하여 반환한다. 메모리 회원 레퍼지토리와 고정 금액 할인 정책을 구현체로 생성


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

✔️ hello.core 패키지 밑에 OrderApp.java 클래스 생성

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 {
    // psvm + tab
    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);

    }
}

⬆️ (실행 결과)

        System.out.println("order.calculatePrice = " + order.calculatePrice());

⬆️ 해당 코드를 추가하면 아래의 결과도 얻을 수 있음.

하지만 애플리케이션 로직으로 이렇게 테스트 하는 것은 좋은 방법이 아님. JUnit 테스트를 사용하자!

✔️ test에 hello.core 패키지 밑에 order 패키지 생성
✔️ order 패키지 밑에 OrderServiceTest.java 생성

package hello.core.order;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.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을 써도 되나 이는 null을 쓸 수 없음.
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        // VIP니까 1000원이 맞는지 검증
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);

    }

}

📌 Assertions import 할 때는 이걸로 하기

⬇️ 테스트 결과

⬇️ test에서 hello.core (모든 테스트)를 실행해도 잘 동작함

단위 테스트는 몇천개가 있어도 몇 초 안에 끝남. 단위 테스트(스프링이나 컨테이너의 도움 없이 순수 자바로 짜여진 테스트)를 잘 만드는 것이 중요.



이상.

profile
개발 바보 이사 중

0개의 댓글