[Spring] 예제 1 만들기_2(JAVA)

이전 포스팅에서 프로젝트 환경을 만들었고 이제 실제로 역할과 구현을 나누어 간단하게 설계를 해보자.


비즈니스 요구사항과 설계

회원

  1. 회원가입을 하고 조회할 수 있다.
  2. 회원은 일반과 VIP 두 가지 등급이 있다.
  3. 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다.(이 기능은 결정이 되지 않은 상태)

주문과 할인 정책

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

위 요구사항을 살펴보면 회원 데이터, 할인 정책 같은 부분은 지금 결정하기 어려운 부분이다. 이들의 인터페이스를 만들고 구현체를 언제든지 갈아 끼울 수 있도록 설계하면 된다.

※ 예제1의 경우, 프로젝트 환경설정을 위해 스프링 부트를 사용한 것이지, 개발 단계에서는 스프링이 전혀 사용되지 않는 자바로만 개발을 진행한다.

 


회원 도메인 설계

회원 도메인 협력 관계

클라이언트가 "회원 서비스" 기능을 호출한다. 회원 서비스는 회원가입, 회원 조회 두 가지 기능을 한다. 
그리고 "회원 저장소"라는 인터페이스를 별도로 만들어 주는데, 요구사항 3에서 자체 DB를 구축할 수도 있고, 외부 시스템과 연동을 할 수도 있기 때문에 회원 데이터에 접근할 수 있는 계층을 따로 만들어 주자. 이 인터페이스의 구현을 '메모리 회원 저장소', DB 회원 저장소', '외부 시스템 연동 회원 저장소' 이렇게 3 가지로 나눌 것이다. 이 구현체들 중 선택만 하면 되는 것이다. 아직 확정이 되지 않은 상태이기 때문에 간단한 자바 코드로 메모리에 회원 객체를 저장하고 호출하는 방식(메모리 회원 저장소)으로 개발을 진행하고자 한다. 메모리에 저장하므로 컴퓨터를 껐다 켜면 모든 데이터가 날아가기 때문에 딱 개발용으로만 사용한다. 추후에 DB나 외부 시스템이 선택되면 그 부분만 구현하여 교체해주면 된다. 

회원 클래스 다이어그램

위의 다이어그램을 토대로 구현을 시작하면 된다. 

회원 객체 다이어그램

실제 서버에 올라왔을 때, 객체 간에 메모리 참조들이 어떻게 되는지 나타낸 것이다. 
클라이언트는 회원 서비스(MemberServiceImpl)를 바라보고 회원 서비스는 메모리 회원 저장소를 바라본다.

위에서 알 수 있듯, 그림은 크게 3가지로 그려진다. 도메인 협력 관계는 기획자들도 볼 수 있는 그림이다. 이것을 바탕으로 개발자가 구체화하여 클래스 다이어그램을 만들어 내고, 이 클래스 다이어그램에 인터페이스와 구현체가 모두 보이게 된다. 
클래스 다이어그램은 실제 서버를 실행하지 않고 클래스들만 분석해서 볼 수 있는 다이어그램이다. 하지만, MemoryMemberRepository를 넣을지 DbMemberRepository를 넣을지는 동적으로 결정되는 것이기 때문에 클래스 다이어그램만으로는 판단하기 어렵다.
따라서 서버가 열려서 클라이언트가 실제 사용하는 객체를 나타낸 객체 다이어그램이 따로 존재한다. 

 


회원 도메인 개발

member 패키지를 하나 생성해 이 안에 회원 도메인의 회원 등급, 엔티티, 저장소를 만들어 준다.

회원 엔티티

회원 등급

회원 등급은 Enum으로 생성해준다. 회원에 대한 등급은 BASIC, VIP 두 가지가 존재한다.

package hello.core.member;

public enum Grade{
    BASIC,
    VIP
}

회원 엔티티

회원 정보는 클래스로 생성한다. 

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;
    }
}
※ MAC 환경 IntelliJ에서 command + N 단축키를 통해 생성자와 Getter & Setter를 자동으로 생성할 수 있다.

 

회원 저장소

회원 저장소 인터페이스

package hello.core.member;

public interface MemberRepository {

    void save(Member member);

    Member findById(Long memberId);
}

회원을 저장하는 기능, 회원의 ID로 회원을 찾는 기능, 이 2 가지만 만들자.

 

메모리 회원 저장소 구현체

인터페이스와 구현체는 같은 패키지보다 다른 패키지에 만드는 것이 설계상 더 좋지만, 예제의 간단함을 위해 같은 패키지에 생성하자.
실제로는 오류처리가 필요하지만, 이 예제의 핵심은 오류처리가 아니기 때문에 이 부분은 생략하겠다.

데이터베이스가 확정되지 않았지만 개발을 진행해야 하므로 가장 단순한 메모리 회원 저장소를 구현해 진행하자.

package hello.core.member;

import java.util.HashMap;
import java.util.Map;

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);
    }
}
여러 곳에서 store에 동시에 접근하는 동시성 문제 때문에 HashMap보다는 ConcurrnetHashMap을 사용하는 것이 맞다. 하지만, 이것은 예제니까!

 

회원 서비스

회원 서비스 인터페이스

회원 서비스는 회원 가입, 회원 조회 2가지 기능을 포함한다. 

package hello.core.member;

public interface MemberService {

    void join(Member member);

    Member findMember(Long memberId);
}

회원 서비스 구현체

관례적인 것이지만, 구현체가 하나만 존재할 경우에는 인터페이스 명 뒤에 -Impl을 붙여 많이 사용한다. 

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

MemberServiceImpl이 가입을 하고 회원을 찾기 위해서는 MemberRepository 인터페이스가 필요하다. 구현 객체를 MemoryMemberRepository로 설정한다. 

join메서드를 살펴보면, save를 호출하면 다형성에 의해서 MemoryRepository 인터페이스가 아니라  MemoryMemberRepository에 있는 save(override한 것)가 호출될 것이다. findMember도 마찬가지이다.


다음 포스팅에서 위 내용들을 실제로 실행해보고 테스트해보자.