BackEnd/Spring

[Spring] 스프링 핵심 원리 이해1 - 예제 만들기

샤아이인 2022. 1. 31.

내돈내고 내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

 

이번시간에는 Spring이 왜 나왔는지 를 이해하기 위해하는 단원으로 잘못된 코드로부터 시작하여 점점 유지 보수가 편한 코드로 바뀌어가는 과정을 확인하는 시간 이였다.

 

블로그에는 일부 코드들은 생략하여 올리지 않았습니다

 

1. 회원 도메인 설계

먼저 회원 클래스를 통하여 설계할 구조를 파악하였다.

출처 - 인프런 스프링 (김영한) 강의

서비스 인터페이스를 구현한 impl 과 MemberRepository 인터페이스를 만든다. 구현체는 언제든 변경 가능하다.

 

다만 class 다이어그램만으로는 runtime때 동적으로 설정되는 관계를 알기 어렵다고 하였다.

따라서 객체 관계도를 따로 만든다 하셨다. 다음 객체 관계도 또한 살펴보자.

출처 - 인프런 스프링 (김영한) 강의

 

2. 회원 도메인 실행과 테스트

직전에 구현한 코드가 정삭 작동하는지 테스트 코드를 작성해보는 시간이였다.

package hello.core.member;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

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

    }
}
 

기존의 콘솔창에서 눈으로 검증하던 방식과 달리 테스트 케이스를 돌리면 빨간색, 녹색 창이 뜨면서 문제점 의 빠른 확인이 가능하다.

 

이제 회원 도메인 설계의 문제점을 찾아봅시다! (스스로 찾아보았습니다!)

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

DIP를 잘 지키고 있을까요?

MemberServiceImpl 부분에서 구상 코드에 의존하고 있다.

new MemoryMemberRepository() 을 직접 생성하여 사용한다는 점에서 일단 유지보수가 어려울 것 이다.

위 코드는 추상화, 구현체 2개 모두에 의존하고 있는 것 이다.

 

다른 저장소로 변경할 때 OCP 원칙을 잘 준수할까요?

그렇지 못한 상태이다. repository를 변경하려면 구현부 코드를 바꿔야 한다.

 

3. 주문과 할인 도메인 설계

● 주문과 할인 정책

회원은 상품을 주문할 수 있다.

회원 등급에 따라 할인 정책을 적용할 수 있다.

할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)

 

할인 정책은 변경 가능성이 높다.

회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)

출처 - 인프런 스프링 (김영한) 강의

주문 도메인 전체는 다음과 같다.

출처 - 인프런 스프링 (김영한) 강의

역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다.

예를 들어 저장소 역할과 이를 구현하는 메모리 저장소구현, DB 저장소구현 을 통해 조립할 수 있다.

덕분에 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다.

 

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

출처 - 인프런 스프링 (김영한) 강의

협력 관계를 그대로 재사용 할 수 있다.

 

4. 주문과 할인 도메인 개발

discount 패키지를 하나 만들었다.

 

이후 DiscountPolicy 라는 인터페이스를 만들었다.

public interface DiscountPolicy {

    int discount(Member member, int price); // 얼만큼 할인이 되었는지 반환해줌
}
 

이 인터페이스는 얼마만큼 할인되었는지를 반환해주고 있다.

(참고로 코드를 작성하던 중 F2 를 누르면 오류가 난곳으로 바로 이동한다.)

 

DiscountPolicy 인터페이스에 대한 구현체인 FixDiscountPolicy코드를 살펴보자. 다음과 같다.

public class FixDiscountPolicy implements DiscountPolicy {

    private final int discoutFixAmount = 1000; // 1000원 할인 고정

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){ // 등급이 VIP인 경우에만
            return discoutFixAmount; 할인 1000원
        }else { // 일반 회원일경우
            return 0;  할인 0원
        }
    }
}
 

 

이번에는 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;
    }

    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 discount) {
        this.discountPrice = discount;
    }

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

클라이언트에서 주문서비스로 넘어가는 주문 class 이다. 주문에서 할인까지 다 한후 객체로 만들어진다.

이 order라는 클래스에는 잘 보면 계산된 가격을 알려주는 calculatePrice() 라는 메소드가 있다.

 

이번에는 주문 서비스 인터페이스를 만들어보자.

위에서 살펴본 다음 그림과 같은 부분에 해당된다. 인자로 회원id, 상품명, 상품 가격을 넘기고 반환값으로는 주문 결과를 반환한다.

출처 - 인프런 스프링 (김영한) 강의

이를 인터페이스로 작성하면 다음과 같다.

package hello.core.order;

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 discountPrice = discountPolicy.discount(member, itemPrice);

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

위의 코드를 보면 orderService 입장에서는 할인에 대해서는 전혀 모른다.

할인에 대한 정책은 discountPolicy에게 위임하여 그 책임을 넘겼다고 할 수 있다.

 

단일책임 원칙을 잘 지켰다고 할 수 있는것이다.

 

또한 MemoryMemberRepository 와 FixDiscountPolicy 객체를 내부에 구성 하고 있다.

 

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

테스트 코드를 작성해 보았다. 다음과 같다.

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

댓글