쌩로그

스프링 핵심원리 기본편 - Ch02. 스프링 핵심 원리 이해1 - 예제 만들기 본문

카테고리 없음

스프링 핵심원리 기본편 - Ch02. 스프링 핵심 원리 이해1 - 예제 만들기

.쌩수. 2023. 12. 12. 13:53
반응형

목록

  1. 포스팅 개요
  2. 본론
      2-1. 비즈니스 요구사항과 설계
      2-2. 회원 도메인 설계
      2-3. 회원 도메인 개발
      2-4. 회원 도메인 실행과 테스트
      2-5. 주문과 할인 도메인 설계
      2-6. 주문과 할인 도메인 개발
      2-7. 주문과 할인 도메인 실행과 테스트
  3. 요약

1. 포스팅 개요

인프런에서 영한님의 스프링 핵심 원리 기본편 Section02 스프링 핵심 원리 이해1 - 예제 만들기를 학습하며 정리한 포스팅이다.

| 참고 이전 포스팅

2. 본론

2-1. 비즈니스 요구사항과 설계

요구 사항은 이와 같다.

미확정 혹은 나중에 변경되는 부분은 인터페이스로 역할만 정해놔야 한다는 느낌을 받을 수 있다.

2-2. 회원 도메인 설계

회원 요구사항에 따라 다음과 같이 설계할 수 있다.

도메인 협력 관계는 기획자들도 볼 수 있는 그림이다.
이를 이용해서 개발자가 구체화해서 클래스 다이어그램을 만들어낸다.
클래스 다이어그램은 서버를 실행시키지 않고 클래스만 분석해서 볼 수 있는 것이다.
저장을 메모리에 할지 DB에 할지는 동적으로 결정된다.
즉 서버가 실행될 때 결정하기 때문에 클래스 다이어그램만으론 판단하기 어렵다. 이 때 객체 다이어그램을 활용한다.
객체 다이어그램은 클라이언트가 실제 사용하는 인스턴스끼리의 참조를 보여준다.

2-3. 회원 도메인 개발

member 패키지

Member

public class Member {

    private Long id;
    private String name;
    private Grade grade;

    // 생성자...
    // getter, setter 등
}
public enum Grade {
    BASIC,
    VIP
}

MemberRepository(인터페이스) + 구현체

public interface MemberRepository {

    void save(Member member);

    Member findById(Long memberId);
}
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);
    }
}

MemberService(인터페이스) + 구현체

public interface MemberService {

    void join(Member member);

    Member findMember(Long memberId);
}
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);
    }
}

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

public static void main(String[] args) {
    MemberService memberService = new MemberServiceImpl();
    Member member = new Member(1L, "memberA", Grade.VIP);
    memberService.join(member);

    Member findMember = memberService.findMember(1L);
    System.out.println("mew member = " + member.getName());
    System.out.println("find Member = " + findMember.getName());
}

위의 코드를 실행하면, 결과는 다음과 같다.

눈으로 직접 확인해야하는 단점이 있다.
따라서 테스트 디렉터리를 적극 활용하자.

다음과 같이 작성해서 확인하면 된다.

다음은 실행결과다.

만약 MemberId에 1을 줬지만, 2L로 찾은 member 와 비교를 한다면,
코드는 다음과 같다.

// given
Member member = new Member(1L, "memberA", Grade.VIP);

// when
memberService.join(member);
Member findMember = memberService.findMember(2L);

테스트 결과는 다음과 같다.

눈으로 확인할 필요없이 IDE가 알아서 검증해준다.

사실 중요한 것은 회원 도메인 설계의 문제점이다.

다른 저장소로 변경할 때 OCP 원칙을 잘 준수할수 있을까?
DIP를 잘 지키고 있을까?
의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있다.

주문까지 만들고 문제점과 해결 방안을 설명한다.

2-5. 주문과 할인 도메인 설계

주문과 할인 정책은 다음과 같다.

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

설계는 다음과 같다.

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

참고: 실제로는 주문 데이터를 DB에 저장하겠지만, 예제가 너무 복잡해 질 수 있어서 생략하고, 단순히 주문
결과를 반환한다고 하신다.

위 그림은 단순히 역할에 대한 그림이지만,
다음 그림은 역할의 구현까지 그린 그림이다.

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

다음은 클래스 다이어그림이다.

객체 다이어그램으로는 다음과 같이 그릴 수 있다.

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

2-6. 주문과 할인 도메인 개발

할인 정책 인터페이스이다.

public interface DiscountPolicy {

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

할인 정책 인터페이스를 구현한 정액 할인 클래스이다.

public class FixDicountPolicy 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;
        }
    }
}

주문 엔티티이다.

public class Order {

    private Long memberId;
    private String itemName;
    private int itemPrice;
    private int discountPrice;

    // .. 생성자
    // .. Getter. Sett

    public int calculatePrice() {
        return itemPrice - discountPrice;
    }

주문 서비스 인터페이스이다.

public interface OrderService {

    Order createOrder(Long memberId, String itemName, int itemPrice);
}

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

public class OrderServiceImple implements OrderService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new FixDicountPolicy();

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

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

참고로 잘 설계된 구조이다.
만약 할인정책을 주문에서 같이 관리하면, 할인 정잭이 변경될 때 주문에서도 변경해야한다.

2-7. 주문과 할인 도메인 실행과 테스트

public class OrderApp {
    public static void main(String[] args) {

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

        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());
    }
}
  1. Member를 생성해서 회원가입을 시킨다.
  2. orderService의 createOrder 메서드를 이용해서 내부적으로 할인 정책까지 적용되어 주문이 생성된다.

결과는 다음과 같다.

order = Order{memberId=1, itemName='itemA', itemPrice=10000, discountPrice=1000}
order.calculatePrice() = 9000

이를 테스트로 옮겨보면, 다음과 같이 작성할 수 있다.

결과는 다음과 같다.

참고로 테스트에 사용되는 Assertions는 junit이 아닌 assertj의 라이브러리에서 import 해야한다.

다형성을 잘 활용했는데, 과연 정액 할인이 아니라, 정률 할인으로 잘 바꿀 수 있는지, 다음 Section을 통해서 확인할 수 있다.

3. 요약

비즈니스 요구사항에 따라 순수 자바 코드만으로 요구사항에 대한 설계와 구현을 해보았다.

테스트는 덤이다.

다음 섹션은 문제를 발견하고 해결하면서 리팩터링하실 거 같다.

728x90
Comments