관리 메뉴

공부 기록장 💻

[Spring] 회원 주문 서비스 예제 비즈니스 요구 사항 정리 - 회원 도메인 설계 및 구현, 테스트 본문

# Tech Studies/Java Spring • Boot

[Spring] 회원 주문 서비스 예제 비즈니스 요구 사항 정리 - 회원 도메인 설계 및 구현, 테스트

dream_for 2023. 1. 19. 17:40

인프런 - 스프링 핵심 원리 기본편 정리



이전에 스프링 입문 강의에서 회원 예제를 만들어 본 회원 서비스 예제에서 확장되어,
이번에 학습하게 된 스프링 핵심 원리 강의에서 다루는 회원-주문 서비스 예제의 비즈니스 요구 사항을 정리하고,
간단하게 회원 Domain에 대한 Service를 만들어, Test를 진행해보자.
그리고 스프링의 핵심 원리를 이해해보도록 하자.




가장 먼저 https://start.spring.io/ 에서 Spring Boot 프로젝트를 생성해보자.
아래와 같이 기본적인 프로젝트 설정을 마치고, 메타데이터 또한 입력해주었다.
프로젝트의 식별자 이름은 hello, 이름은 core 이로 설정해주었다.




Spring Boot 개발 환경 기본 정보는 다음과 같다.

  • Project: Gradle - Groovy
  • Language: Java
  • Spring Boot Version: 3.0.1
  • Packaging: Jar
  • Java: 17


build.gradle 파일을 살펴보면 다음과 같다..


비즈니스 요구사항 정리

회원-주문 서비스의 기본적인 비즈니스 요구사항은 다음과 같이 나타나 있다.

회원

  • 회원을 가입하고 조회할 수 있다.
  • 회원에는 일반, VIP 두 등급이 있다.
  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

주문과 할인 정책

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



스프링 입문 강의에서 배운 것을 토대로, 역할과 구현을 명확하게 분리하여 객체 지향적인 설계를 하기 전, 요구 사항을 분석하며 고민을 해보면 다음과 같다.

  • 회원 가입 기능: 회원의 Id 값은 auto increment 기능을 부여해야 하며, name 필드가 있겠다. 가입 시 기존 회원 목록에 동일한 name 값을 갖는 경우에는 등록하지 못한다.
  • 회원 등급: 각 회원은 일반 등급, 또는 VIP 등급 중 하나의 등급을 가질 것이다. 그리고 등급은 언제나 바뀔 수 있기 때문에 쉽게 바뀔 수 있도록 설계해야 한다.
  • 회원 DB: 데이터베이스가 미확정이기 때문에, DB와 접근하는 repository 구현체가 언제든 바뀔 수 있도록 설계해야 한다.
  • 상품 주문: 각 회원은 여러 상품을 주문할 수 있다. 한 상품에 대해 여러 수량을 주문할 수도 있을 것이다. 상품 도메인과 주문 도메인이 필요하다. 주문 도메인에는 해당 주문을 한 고객, 그리고 상품과 상품의 수량 정보를 필요로 할 것이다. 상품에 대한 주문 방식도 언제든 바뀔 수 있기 때문에 이 또한 인터페이스를 잘 설계하는 것이 좋지 않을까?
  • 등급에 따른 할인 정책: 주문 도메인에서, 회원의 등급에 따라 각 상품에 대한 할인 정책이 적용되므로, 상품의 수량에 맞는 전체 할인가 정보를 갖고 있어야 한다. 기존 상품가, 할인가, 최종 구매가격의 정보가 담겨 있어야 하겠다.
  • 할인 정책: 변경 가능성이 많으므로, 언제든 할인 정책 구현체가 바뀔 수 있도록 설계해야 한다. 회사의 기본 할인 정책과 고정 금액 할인 정책이 있지만, 할인을 적용하지 않는 경우도 있으므로 이 부분을 고려해야 한다.


요구사항 분석 및 설계

회원 도메인의 협력 관계


클라이언트 계층에서는 회원 가입과 회원 조회라는 서비스를 이용하게 된다. 어떠한 요청이 들어왔을 때, 회원 저장소에 접근하여 새로운 회원을 저장하거나, 회원들의 목록을 가져오게 될 것이다.

이때, 회원 저장소로는 회원 DB를 자체적으로 구축할 수도, 외부 시스템과 연동할 수도 있기 때문에 회원 데이터에 접근하는 계층, 회원 저장소라는 인터페이스를 만들고, 언제든 갈아 끼울 수 있도록 다음과 같이 설계하자.
회원 저장소 역할의 구현체는 Java의 로컬 내부 메모리를 사용하는 1) 메모리 회원 저장소, 실제 데이터베이스를 선택하게 되는 경우 2) DB 회원 저장소, 3) 외부 시스템 연동 회원 저장소 구현체를 만들 것이다.



회원 클래스 다이어그램


위의 도메인이 구현 레벨로 내려오게 되면, 다음과 같이 회원 클래스 다이어그램을 설계할 수 있다.

우선 회원 서비스의 역할을, MemberService라는 이름의 인터페이스로 만들어야 한다. MemberServiceImpl 이 서비스의 구현체가 될 것이고,
회원 저장소의 역할을 하는 MemberRepository 인터페이스에 대한 구현 클래스는 MemoryMemberRepository, DbMemberRepository, 그리고 외부 시스템 연동 클래스가 있겠다.

회원 객체 다이어그램


객체 간의 메모리의 참조는 다음의 다이어그램으로 표현할 수 있다.
클라이언트는 회원 서비스를 바라보고, 회원 서비스 MemberServiceImpl 인스턴스는 메모리 회원 저장소 MemoryMemberRepository 인스턴스를 바라보는 동적인 구조는 다음과 같다.




회원 도메인 구현


회원

  • 회원을 가입하고 조회할 수 있다.
  • 회원에는 일반, VIP 두 등급이 있다.
  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

이제 위의 설계와 회원 도메인의 요구 사항을 바탕으로, 회원 도메인을 구현해보자.
Member 클래스와 Grade Enum, Repository 인터페이스와 메모리 구현체, Service 인터페이스와 구현체,
그리고 간단하게 test를 실행해 볼 MemberApp 클래스를 만들어보자.


Grade Enum


회원 등급을 나타낼 Enum 클래스 Grade는 다음과 같다. 필드로 BASIC, VIP를 포함한다.

public enum Grade {
    BASIC,
    VIP
}

Member 엔티티


회원의 정보를 나타내는 Member 클래스는 다음과 같다.
기본값인 id, 이름 Name, 등급 Grade를 필드로 갖고 있으며, 생성자와 Getter, Setter 메서드도 포함하고 있다.

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

MemberRepository 인터페이스


회원 데이터에 접근하는 Repository 인터페이스에는, 회원 가입 시 회원 데이터를 저장하는 save, 회원을 ID 값으로 조회하는 findById 메서드의 틀을 포함한다.

public interface MemberRepository {

    void save(Member member);
    Member findById(Long memberId);
}

MemoryMemberRepository 구현체


MemberRepository 인터페이스의 Java 메모리 버전 구현체인 MemoryMemberRepository 클래스를 작성해보자.
실무에서는 여러 곳에서 동시에 메모리에 접근하는 경우 동시성을 고려하기 위해 Concurrent HashMap 을 써야 하지만, 예제로는 HashMap을 사용하도록 하자.
위의 인터페이스에서 만든 save(), findById() 메서드의 틀을 구현하면 다음과 같다,

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 인터페이스


회원과 관련된 비즈니스 로직을 담당하는 service 의 인터페이스인 MemberService에는 마찬가지로 회원 가입 기능의 join, 회원을 조회하는 findMember 메서드의 틀을 포함하고 있다.

public interface MemberService {
    void join(Member member);
    Member findMember(Long memberId);
}


MemberServiceImpl 구현체

MemberService 인터페이스의 구현체 MemberServiceImpl 클래스를 다음과 같이 작성하자.
(인터페이스에 대한 구현체가 하나인 경우, 인터페이스명 뒤에 Impl 을 붙여 구현체 클래스의 이름을 짓는 것이 하나의 관례라 할 수 있다.)

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


회원 도메인 Test

MemberApp main 메서드로 간단한 테스트


순수한 Java code 만으로 테스트를 해보면 다음과 같다.
위에서 Member 클래스의 생성자를 작성했듯, id, name, grade 값을 직접 작성함으로써 Member 객체를 생성할 수 있다.
(id의 경우 Long 타입이므로, 정수 뒤에 L을 꼭 붙여주자.)

memberA라는 이름 값을 갖는 회원을 join 하고, id 값으로 회원을 조회하여 두 회원의 이름을 콘솔에 출력해보면,

public class MemberApp {
    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("member = " + member.getName());
        System.out.println("find Member = " + findMember.getName());
    }
}


다음과 같이 memberA 라는 이름이 동일하게 출력됨을 확인할 수 있다.

JUnit 을 이용한 Test


이제 JUnit 프레임워크를 이용하여 보자.


member 패키지 내 MemberServiceTest 클래스를 만들고, 간단한 회원가입 join 테스트 케이스를 작성해보자.
given-when-then 패턴을 이용해 작성하면 다음과 같다.

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

728x90
반응형
Comments