[Spring] 예제1

노유성·2023년 7월 11일
0
post-thumbnail

비즈니스 요구사항 및 설계

회원 도메인 설계

요구사항

  1. 회원 가입 및 조회
  2. 회원에는 VIP, 일반 등급이 있음
  3. 자체 DB가 있음

다이어그램


Repository는 여러 DB를 사용할 가능성이 있기 때문에 역할과 구현을 분리하여 interface를 만들었고 회원 가입 및 조회의 기능을 하는 service도 역할과 구현을 나누었다.

참고로 회원 도메인 협력 관계 그래프는 개발자가 다른 도메인의 사람들과 함께 논의를 하기 위해 만들고 이를 바탕으로 실질적으로 어떻게 역할과 구현을 나눌 건지에 대해 그린 다이어그램이 그 밑의 다이어그램들이다.

회원 도메인 개발

회원 등급

회원의 등급은 enum을 이용해서 만들었다.
/member/grade

public enum Grade {
    BASIC,
    VIP
}

도메인 엔티티

entity란 실제 DB의 테이블과 매핑되는 객체이다.
/member/Member.java

public class Member {
    private Long id; // 회원 ID
    private String name; // 회원 이름
    private Grade grade; // 회원 등급

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

	// getter
    
    // setter

회원 repository

repository는 간단하게 설명하면 service에서 필요한 데이터를 db에서 조회하고 혹은 입력해서 그 값을 service에게 반환해줄 수 있는 인프라를 갖춘 class이다.

interface

/member/repoisitory/MemberRepository.java

public interface MemberRepository {
    void save(Member member);

    Member findById(Long memberId);

}

거창할 거 없이 db에 데이터를 저장하고 조회하는 기능을 가졌다.
명세서에서 요구한 회원을 가입하고 조회하는 기능을 구현하는 부분은 아니다. 그 기능들은 service에서 구현할 것이고 service에서 조회를 위해 필요한 아주 간단한 curd기능만 repository에서 구현하는 것이다.

구현체

/member/repoisitory/MemoryMemberRepository.java

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long, Member> store = new HashMap<>();
    @Override
    public void save(Member member) {
        store.put(member.getId(), member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);
    }
}

위 역할(interface)를 구현한 구현체이다.

회원 service

이제부터 명세서에서 정의한 기능들을 구현하는 service를 만들겠다.
repository에서 제공하는 CURD기능을 바탕으로 비즈니스 로직을 구현한 부분이다. 현재 명세서에서는 회원을 가입하고 조회하는 2가지의 기능이 필요하다.

interface

/member/service/MemberService.java

public interface MemberService {
    void join(Member member);

    Member findMember(Long memberId);
}

가입과 조회를 정의한 interface이다.

구현체

/member/service/MemberServiceImpl.java

public class MemberServiceImpl implements MemberService{
    private MemberRepository memberRepository = new MemoryMemberRepository(); // 인터페이스와 구현체에 모두 의존하고 있음. DIP 위반
    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

역할을 구현한 구현체이다. 지금 보면은 repository에서 제공하는 curd만을 그대로 가져다가 쓴 거라 뭐가 필요한건지는 모르겠지만 나중에 여기서 유효성 검사도 시행하고 어떻게 효율적으로 data를 가져올 것인지에 대한 부분도 시행된다.

더하여 MemberService에 대한 구현체가 하나밖에 없으면은 interface의 파일명 뒤에 Impl만 붙히는게 관례라고 한다.

주문과 할인 도메인 설계

요구사항

다이어그램

주문과 할인 도메인 개발

할인 정책 interface

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

모든 할인 정책의 공통점은 그래서 할인율이 얼마인데? 이 부분이다.
member의 등급과 가격을 기준으로 어떤 로직(정책)을 거쳐서 얼마의 할인을 해줄 것인지를 결정해야 하기 때문에 이 역할을 맡은 interface이다.

할인 정책 구현체

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

위 interface를 구현한 구현체이다. member의 등급이 VIP이면 고정으로 1000원을 할인해준다.

참고로 enum은 비교할 떄 == 연산자를 사용한다.

주문 엔티티

마찬가지로 DB의 table과 매핑되는 entity이다.

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

간단하게 주문을 한 주체(member)의 정보, item의 가격과 이름과 할인가격에 대한 정보를 정의한 entity이다.

여기서 calculatePrice()를 주목해보자. 시스템을 보면은 OrderService가 주문을 받으면은 DB에서 데이터를 조회하고, 할인 정책을 받아서 할인 가격을 정한 후 Order 객체를 찍어서 반환한다. 자 그럼 여기서 calculatePrice()에서 사용되는 discountPrice는 OrderService에서 생성된다(당연하게도 여기서 Order객체를 찍어내니까). 자 그래서 할인된 가격을 게산하는 calculatePrice()는 왜 Order class의 메소드로 정의되어야할까?

답은 간단하다. OrderService의 관심사는 주문을 받아서 주문 객체를 찍어내는 것이다. 그래서 할인된 가격이 얼마인지 알아내는 것이 OrderService의 주 관심사가 아니다. 그리고 설령 OrderService에 calculatePrice()를 구현한다고 해도 매우 비효율적이다. 이미 찍어내고 내보낸, 손을 떠난 object가 다시 찾아와서 '그래서 난 얼마야?' 하고 계산해달라고 하는 건 많이 비효율적이다. 이미 Order class에는 할인가격을 계산하기 위한 필드가 구성이 되어있으므로 할인 가격을 계산하는 거는 Order class 단에서 해결한다.

주문 서비스 interface

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

DB조회를 해서 member의 등급을 알아내고 할인 정책에서 내보내준 할인 가격을 바탕으로 Order instance를 찍어낸다. 여기서 좋은 점이 할인 가격을 계산하는 것(정책)은 service의 관심사가 아니기에 discountPolicy에게 필요한 정보만 넘기고 계산에는 일절 관여하지 않는다. 그러기에 할인 정책이 변경된다고 할 지라도 OrderService에서는 그에 영향을 받지 않는다.

여기서 아쉬운 점은 service는 repository의 역할을 바라보아야 하는데 구현체에도 의존하고 있고 정책도 마찬가지로 구현체에도 의존하고 있어 DIP 원칙이 위반된다.

마무리하며

사실 내가 혼자 만들었으면 OrderService에서 할인률을 계산했을 것 같다. 그러면 SRP원칙을 위반했을 것이다. 또 그러면 당연하게도 코드의 유지보수가 어려워졌을 것인데 역할과 책임을 분리해 설계하고, 직관적으로 이를 바라볼 수 있게 다이어그램을 작서앟는 것이 매우 중요하다고 느꼈다.

profile
풀스택개발자가되고싶습니다:)

0개의 댓글