WEB/Spring

김영한 (스프링 핵심원리 2) 스프링의 핵심원리이해 1 - 예제 만들기

Tony Lim 2021. 2. 9. 13:54

비즈니스 요구사항과 설계

 

회원

회원을 가입하고 조회 할 수 있다.

회원은 일반과 VIP 두 가지 등급이 있다.

회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다.

 

주문과 할인정책

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

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

할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라.

할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아지 정하지 못헀고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않ㅇ르 수 도 있다.

 

회원 도메인 설계

 

package hello.core.member;

public class Member
{
    private Long id;
    private String name;
    private Grade grade;

    public Member(Long id, String name, Grade grade)
    {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    public Long getId()
    {
        return id;
    }

    public void setId(Long id)
    {
        this.id = id;
    }

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public Grade getGrade()
    {
        return grade;
    }

    public void setGrade(Grade grade)
    {
        this.grade = grade;
    }


}

 

우선 도메인을 구축한다. 회원의 속성은 기본적인 식별자인 id , 이름, 등급으로 구성된다. 

package hello.core.member;

public interface MemberRepository
{
    void save(Member member);

    Member findById(Long memberId);
}

db에 현재는 메모리이지만 추후에 구현체를 다르게 구성할 수 있으므로 인터페이스를 설계해준다.

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

메모리 상으로 간단하게 구현해준다. 현재는 직접 객체를 생성해서 넣어주는 방식이다.

package hello.core.member;

public interface MemberService
{
    void join(Member member);

    Member findMember(Long memberId);
}

서비스 단도 인터페이스를 구축해준다. 원래는 변경 계획이 없다면 구체적인 객체를 의존하는게 맞다고 했다.

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

서비스 인터페이스를 구현한 객체이다. 

지금은 패키지를 따로 domain ,repository, service 등등으로 구분하지않음은 간단한 예제이기 때문이다. 패키지 구성을 스케일에 따라 달리 하는것도 능력이라 했다.

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

}

잘 되는지 junit을 통한 테스트 이다. given 주어진 상황에서 when 이러한 액션을 취했을 때  then 이러한 결과가 나온다.

 

주문과 할인 도메인 설계

1. 주문생성: 클라이언트는 주문 서비스에 주문 생성을 요청한다.

2. 회원조회: 할인을 위해서는 회원 등급이 필요하다. 그래서 주문 서비스는 회원 저장소에서 회원을 조회한다.

3. 할인적용: 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임한다.

4. 주문 결과 반환: 주문 서비스는 할인 결과를 퐇마한 주문 결과를 반환한다.

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

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy
{
    
    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;

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

정액할인정책을 구현한 결과입니다. Grade.VIP는 enum이기에 == 연산을 통해 비교해줍니다. enum을 사용하게 되면 compile 시점에 에러를 발견된 확률을 높여줍니다. 

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

    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 +
                '}';
    }
}

Order 도메인이다. 

package hello.core.order;

public interface OrderService
{
    Order createOrder(Long memberId, String itemName, int itemPrice);
}
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
{
    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);
    }
}

order service 단이다. Orderservice 입장에서는 discountpolicy가 정액일지 정률일지 몰라도 상관이없는 방식이다. 단일체계 원칙을 잘 지켜진것이다. discountpolicy가 바뀌어도 Orderservice는 전혀 문제가 없다. 

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

OrderService 가 잘 되는지 테스트 해본 결과이다.