인프런/스프링핵심원리(기본)

4)회원 도메인 설계,회원 도메인 개발

backend dev 2022. 12. 1.

이전에 들은 요구사항에 대해 듣고 개발자가 설계해야하는 부분이다.

 

기획자가 원하는 회원관련 요구사항

 설계할때 플로우를 생각해본다.

1. 클라이언트가 회원서비스를 호출한다. (회원서비스는 회원가입,회원조회 기능을 가지고 있다)

2. 회원저장소 인터페이스를 만든다 ( 어떤 저장소로 바꿔끼울수 있게끔)

 

도메인 레벨의 큰 그림 (기획자도 볼수 있는 그림)

 

각각 클라이언트 역할 ,회원서비스 역할, 회원 저장소 역할이고

메모리회원저장소,DB 회원 저장소, 외부 시스템 연동 회원저장소는 구현이다. 구현중 하나를 회원저장소 역할에 꼽아쓰면된다.

 

아직 회원저장소에 대해 기획이 안나왔지만 개발을 해야한다. 

그래서 메모리 회원 저장소를 만들어서 개발을 시작한다. 메모리를 이용한 저장소는 로컬에서 개발할때도 쓰이고, 테스트할때도 쓰여서 유용하다.  (메모리이므로 서버를 껐다키면 데이터가 날아가는 휘발성 메모리이다.== 딱 개발용으로 사용하는것임 )

그렇게 메모리 회원저장소를 이용해 개발하다가 나중에 다른저장소로 쓰게끔 정해졌으면 바꾸기 위한 코드 추가수정만 하면 된다. ( 회원저장소라는 역할에 다른 저장소라는 구현(구현체)을 갈아 끼운다.)

 

개발 레벨의 큰그림 (도메인 협력관계를 바탕으로 개발자가 구체화해서 클래스 다이어그램을 만든다.) 

실제 서버를 실행하지않고 클래스만 분석해서 보는 그림 , 어떤 리포지토리를 넣을지는 동적으로 결정되는 사항(서버가 뜰때 new ~리포지토리) 로 정하는것이므로 클래스다이어그램으로만으로 판단하기 어렵다. 그래서 객체 다이어그램이라는게 따로있다.

멤버서비스(역할)을 인터페이스로 만들고 MemberServiceImpl이라는 구현체를 구현한다.

멤버리포지토리(역할)을 인터페이스로 만들고 메모리멤버리포지토리,DB멤버리포지토리를 구현체로 구현할 예상을 한다.(리포지토리는 미정이므로)

객체 다이어그램

실제 어떤 구현체를 참조하는지에 대해 보기 쉽게끔 하는 객체 다이어그램

클라이언트는 회원서비스구현체를 바라보고 회원서비스구현체는 메모리회원저장소(구현체)를 바라보게된다.

구현체를 바라본다! (초반에는 메모리회원저장소를 이용해서 개발할거니까-> 회원저장소가 기획 픽스가 안났으니까)

클라이언트 객체는 회원서비스구현체 객체를 바라보고 회원서비스구현체 객체는 메모리회원 저장소 구현체 객체를 바라본다.

 

멤버서비스는 왜 인터페이스로?

멤버서비스는 회원가입,회원조회 2가지 기능이 있다고 정해져있다.

멤버리포지토리는 어떤 저장소를 사용할 지 모르므로 인터페이스로 구현하지만

멤버서비스는 왜 인터페이스로 구현할까?에 대한 김영한님 대답

회원가입,회원조회에 대해 추후 더 최적화된 코드를 구현할 수 있을때, 새로운 구현체를 만들어서 갈아끼우는게 편하게끔 하려고

 

회원 도메인 개발

member 패키지를 하위에 만든다.

 

Grade.java

등급에 대한 Grade

package com.example.demo.member;

public enum Grade {
    BASIC,
    VIP
}

enum 클래스는 간단하게 관련있는 상수들의 집합 

Member.java -> 회원 엔티티 

멤버에 받을 값들 , 게터,세터,생성자 정의

package com.example.demo.member;

import lombok.*;

@Getter // 게터
@Setter // 세터
@AllArgsConstructor // 모든변수를 매개변수로 가지는 생성자 생성
public class Member {
    private Long id;
    private String name;
    private Grade grade;

    //Generate를 이용해서 직접 ,Constructor, getter,setter 만들어줘도 된다.
}

MemberRepository.java  (인터페이스)

인터페이스로 하나 만들어준다.

package com.example.demo.member;

public interface MemberRepository {

    void save(Member member); //구현해야하는 메소드
    Member findById(Long memberId); //구현해야하는 메소드
}

 

MemoryMemberRepository.java (구현체)

 

 

구현체와 인터페이스가 같은 패키지상에 있는것은 구조상 좋지않지만 예제니까 같은 패키지상에 넣는다

package com.example.demo.member;

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

public class MemoryMemberRepository implements MemberRepository {

    //원래는 실무에서 저장소 동시접근 오류때문에 HashMap이 아닌 ConcurrentHashMap을 사용해야한다고 한다. (예제니까 넘어감)
    private Map<Long, Member> store = new HashMap<>(); // 저장용 Map , 멤버아이디,멤버객체 를 저장함

    @Override
    public void save(Member member) {
        store.put(member.getId(), member);

    }

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

MemberService.java (인터페이스)

package com.example.demo.member;

public interface MemberService {

    void join(Member member); //구현해야하는 메소드 -> 회원가입 메소드

    Member findMember(Long memberId);//구현해야하는 메소드 -> 멤버아이디로 멤버찾기 메소드
}

MemberServiceImpl.java(구현체)

package com.example.demo.member;

public class MemberServiceImpl implements MemberService {
    //MemberService의 구현체가 1개인경우 구현체 뒤에 Impl이라는 단어를 붙여 생성하는게 관례상 많이 쓰는 방법이라고 한다.

    // 멤버 리포지토리안에 있는 메소드를 사용해야하므로 선언해준다.
    //인터페이스 변수를 만들고 , 그 안에 구현체객체를 저장해준다. (다형성을 이용함)
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    @Override
    public void join(Member member) {
        memberRepository.save(member); // save를 하게되면 다형성에 의해 MemoryMemberRepository에 있는 save메소드가 실행될것이다.
    }

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

현재까지 상황

현재 프로젝트 상황
현재 Db멤버리포지토리말고 다 만든상태

 

테스트해보기

지금까지 만든걸 테스트해본다.

MemberApp이라는 클라이언트를 만들고

MemberServiceImpl을 참조하게 하고 , MemberServiceImpl은 MemmoryMemberRepository를 잘사용하는지 체크하는 테스트

 

MemberApp.java (클라이언트)

테스트용 클라이언트 파일

package com.example.demo.member;

public class MemberApp {

    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl(); //멤버서비스 인터페이스 변수를 이용해서 멤버서비스 구현체를 저장
        Member memberA = new Member(1L, "memberA", Grade.VIP); //테스트용 멤버생성
        memberService.join(memberA); //가입메소드 사용

        Member findMember = memberService.findMember(1L); // 정상 가입됬는지 findMember 메소드를 사용해본다.
        System.out.println("new member = " + memberA.getName());
        System.out.println("find Member = " + findMember.getName());
    }

}

멤버서비스의 메소드를 사용해서 테스트한다.

결과

이것은 순수한 자바코드로 테스트하는것이므로 불편함이 많다 (일일이 결과를 눈으로 확인해야하는 등)

그래서 Junit을 이용한다.

 

MemberServiceTest.java

MemberApp에서 했던 테스트 보다는 쉽게 결과를 확인할 수 있는 테스트코드를 짜야한다. (필수적)

테스트 패키지->멤버 패키지 -> 테스트파일

package com.example.demo.member;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

    private final 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 -> 이렇게 된다. (값 검증)
        //import org.assertj.core.api.Assertions;에 있는 Assertions를 사용해야한다.
        Assertions.assertThat(member).isEqualTo(findMember); //멤버는 찾은 멤버랑 같은지 체크

    }

}

한눈에 성공실패여부 확인 가능

 

 

의존성 주입부분이 없고 , 구현체를 직접 넣어주는 부분에서 수정이 필요할것 같다. 

ex)

멤버서비스Impl에서 멤버리포지토리 인터페이스 변수에다가 구현체를 직접넣는 부분과 같은 문제점. 

추상화(멤버리포지토리 인터페이스)에도 의존하고 , 구체화(메모리멤버리포지토리)에도 의존하는 문제점 ->DIP 위반

 

다음강의는 주문도메인까지 만들고 ,이런 코드를 수정하는 내용이다.

댓글