[spring 입문] 회원 관리 예제

김영한 선생님 인강들으면서 정리중,,

비즈니스 요구사항 정리


데이터 : 회원id, 이름만 가지는 단순한 시나리오
가상시나리오로 아직 데이터 저장소 선정 안됐다고 가정(nosql할지 관계형할지 등)

일반적인 웹 애플리케이션 계층 구조는
컨트롤러 - 서비스 - 리포지토리     -    db
            ↘        ↓        ↙
                    도메인

  • 컨트롤러는 웹MVC역할 또는 api만들고
  • 서비스는 여기에 핵심 비즈니스 로직에 들어가있음 (ex. 중복가입 안됨)
  • 도메인은 주로 데이터베이스에 저장되고 관리되는 비즈니스 객체이다.
  • 서비스는 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직 구현한 객체라고 보면 된다.

여기서도 일반적인 계층형 구조 따라갈 것임.

아직 데이터 저장소가 선정되지 않아서 메모리 구현체를 만들고 구체적 기술 선정되고 나면 바꿔끼울 것이다. 바꿔끼우려면 interface 필요해서 MemberRepository interface 설계
(아직 jdbc쓸지 mybatis쓸지 jpa쓸지 바꿀 수있다는 가정하에 설계)

회원 도메인과 리포지토리 만들기


controller 폴더 위치한 상위 폴더(hello.hellospring)에
domain폴더 만들고
회원 객체 (class Member)와 그 안에 id, name주고 getter , setter만듦

동일하게 repository 폴더 만들고 interface MemberRepository 생성
여기서 optional은 java8에 들어간거. 나중에 더 설명


MemoryMemberRepository 클래스 만들고 방금만든 MemberRepository 인터페이스를 구현

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {        
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }


    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

멤버를 세이브할 때 sequence 값을 하나 올려주고 store Map에 put해준다.

null 반환 가능성 있으면 Optional로 감싼다. Optional.ofNullable()

루프 돌면서 Map에서 찾아지면 바로 반환
끝까지 없으면 Optional에 넣어서 반환

코드가 제대로 동작 하는지 안하는지 검증하는 방법
--> 테스트 케이스 작성

회원 리포지토리 테스트 케이스 작성


코드 정상 동작 검증하는 방법
개발 기능 실행해서 테스트할 때 자바에서 main메서드 통해서 실행하거나, 또는 웹 어플리케이션 컨트롤러로 실행하는 방법있음 .
이는 반복실행 어렵고 준비하고 실행헤 오래걸린다.
자바에서는 junit이라는 프레임워크로 문제 해결 가능

test폴더에 java, hellospring하위에 repository 폴더 생성하고 MemoryMemeberRepositoryTest클래스 생성 .
Test 어노테이션으로 명시하고 일단 실행시켜도 실행되는거 확인

가져온 result와 member가 같은지 비교하는 테스트

같은지 확인하는 함수로 Assertions.assertEquals가 있음
또는
Assertions.assertThat(member).isEqualTo(result);가 있음

클래스 단위로 하면 에러가 날 수 있는데 레포지토리가 지워지지 않았기 때문이다.

테스트 끝날 때마다 리포지토리 깔끔하게 지워주는 코드 필요

@AfterEach
    public void afterEach() {
        repository.clearStore();
    }
public void clearStore() {
        store.clear();
    }

테스트는 서로 순서 관계없이 의존관계 없이 설계되어야함.
하나 테스트 끝날때마다 공용데이터 지워줘야 문제없다.

테스트 클래스 먼저 작성하고 memeberMemoryRepository 만들 수 있다.
검증할 수 있는 틀을 만들고 구현클래스 만들어서 확인하는 것 : 테스트 주도 개발 (TDD)

테스트 코드 없이 개발은 여러명이면 불가능하다.
테스트 관련 꼭 깊게 공부.

회원 서비스 개발


실제 비지니스 로직
가정 비즈니스 로직 중에 같은 이름 안된다는 로직 있음 (중복회원)
예전에는 if=null로 햇지만 요새는 null 가능성 있으면 Optional로 한번 감싸서 반환 해주고 감싼 덕분에 ifPresent 쓸 수 있음

Optional<Member> result = memberRepository.findByName(member.getName());
        result.ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다");
        });

이 코드는 밑의 코드처럼 짤 수 있다.

memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});


메소드로 뽑아서 작성 가능
window에서 ctrl+alt+shift+T가 리펙토링 단축키
extract Method 로 메소드 뽑을 수 있음

서비스는 비즈니스에 의존적으로 설계하고 레포지토리는 서비스보단 단순히 기계적으로 개발스럽게 용어 선택한다.
roll에 맡도록 네이밍 한다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    // 회원가입
    public Long join(Member member) {
        validateDuplicateMember(member);    // 같은 이름 X 중복회원검증

        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }
    //전체 회원조회
    public List<Member> findMember() {
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

회원가입 했을 때 중복이면 안되는지 등등 테스트 해보려한다.

회원 서비스 테스트


테스트 편하게 하는 법
단축키 : ctrl+shift+T 윈도우 : Create new Test

그러면 같은 패키지(service)에 만들어진다.

회원가입이 잘 되는지 보고
테스트는 과감하게 한글로 바꿔도 된다.
production 코드가 아니기 때문에 한글로 적어도됨 (관례상)
빌드될 때 테스트코드는 포함되지 않는다.

//given

//when

//then 문법을 사용해주면 좋다.
처음 배울때 권장한다.

회원가입 성공

이거는 너무 단순하다 (정상 플로우)
예외 플로우가 중요하다.
join은 저장도 중요한데 중복 로직 예외 터트려지는 것도 봐야함

try catch로 하는 것도 가능하지만
assertThrows(IllegalStateException.class, () -> memberService.join(member2));

그리고 assertThrows는 반환이 돼서 메세지 확인도 가능

IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

메모리에 누적되고 있으니 clear를 해줘야한다.
MemberService밖에 없으니 MemoryMemberRepository 가져와서

MemoryMemberRepository memberRepository = new MemoryMemberRepository();

@AfterEach
    public void afterEach() {
        repository.clearStore();
    }

단축키 shift +F10 하면 이전 시행했던거 다시 시행해준다.

근데 memberRepository는 new 해서 다른 객체이다.

다른 인스턴스기 때문에 내용 달라질 수 있어서
(지금은 static이기 때문에 문제없지만 static 없으면 다른 db되면서 문제가 생긴다.)

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long, Member> store = new HashMap<>();

같은 레파지토리로 테스트하는게 맞기에
같은 인스턴스 쓰도록 하려면 MemberService 클래스에
직접 내부생성 아니라 외부생성하도록 바꿔준다.

public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

MemberServiceTest에서

각 테스트 실행 전에 BeforeEach로 같은 메모리 리포지토리가 사용된다.
MemberService 입장에서 DI라 한다. (Dependency injection)

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }
    // 돌때마다 db 클리어해준다.
    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void 회원가입() {
        //given
        Member member = new Member();
        member.setName("hello");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());

    }

    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring1");

        Member member2 = new Member();
        member2.setName("spring1");

        //when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        //memberService.join(member2);

        //then
    }

    @Test
    void findMember() {
    }

    @Test
    void findOne() {
    }
}

좋은 웹페이지 즐겨찾기