회원 관리 예제

회원 관리 예제

  1. 비즈니스 요구 사항 정리
  2. 회원 도메인과 리포지토리 만들기
  3. 회원 리포지토리 테스트 케이스 작성
  4. 회원 서비스 개발
  5. 회원 서비스 테스트

비즈니스 요구 사항 정리

  • 데이터 : 회원ID, 이름
  • 기능 : 회원 등록, 조회
  • 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)

일반적인 웹 어플리케이션 계층 구조

Untitled

  • 컨트롤러 : 웹 MVC의 컨트롤러 역할
  • 서비스 : 핵심 비즈니스 로직 구현
  • 리포지토리 : DB에 접근, 도메인 객체를 DB에 저장하고 관리
  • 도메인 : 비즈니스 도메인 객체 ex) 회원, 주문, 쿠폰 등등 주로 DB에 저장하고 관리

클래스 의존관계

Untitled

  • 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경 할 수 있도록 설계
  • 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민 중인 상황으로 가정
  • 개발을 진행하기 위해서 초기 개발 단계에서는 구현 체로 가벼운 메모리 기반의 데이터 저장소 사용

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

회원 객체

package hello.hellospring.domain;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter

public class Member {
    private Long id; // 시스템이 저장하는 ID
    private String name;
}

회원 interface

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

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

public interface MemberRepository {
    //repository에 4가지 기능 구현
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();

}

회원 리포지토리 메모리 구현체

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) {
        // 만약에 null일 가능성이 있을땐 Optional.ofNullable로 감싸준다
        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() {
        // Map -> List
        return new ArrayList<>(store.values()); // member들 반환
    }
}

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

개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 어플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

회원 리포지토리 메모리 구현체 테스트

src/test/java 하위 폴더에 생성

package hello.hellospring.repository;

import ch.qos.logback.core.net.SyslogOutputStream;
import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

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

import static org.assertj.core.api.Assertions.*;

public class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository(); // MemoryMemberRepository만 테스트 하는 것이므로

//    전체 테스트를 돌려봤을 때 findAll()에서 저장된 spring1, spring2 때문에 findByName 오류 발생
//    테스트 케이스간 순서간 의존도가 있으면 좋지 않은 테스트 케이스
//    따라서 위의 문제를 해결하기 위해 테스트가 끝나고 나면 데이터를 클리어 한다.
    @AfterEach // 테스트하나가 끝날때마다 실행하겠다.
    public void afterEach() {
        repository.clearStore();
    }

    @Test
    public void save() {
        // new Member()에서 한것과 result 가 같으면 문제가 없는것
        Member member = new Member();
        member.setName("spring");
        repository.save(member);

        Member result = repository.findById(member.getId()).get();

//        1. sout => System.out.println("result = " + (result == member)); : 계속 글자로 볼수는 없으니 2번 방법

//        import org.junit.jupiter.api.Test;
//        2. Assertions.assertEquals(member, null)

//        요즘에 많이 쓰는 코드
//        import org.assertj.core.api.Assertions;
//        3. Assertions.assertThat(member).isEqualTo(result);

        // alt + enter (optional Enter) -> static : 다음부터는 assertThat 만 사용해서 검증해볼수 있음
        assertThat(member).isEqualTo(result); // member가 result랑 똑같을 때
//        실무에서는 test case를 통과하지 않으면 그 다음 단계로 넘어갈수 없게 설정함.
    }

    @Test
    public void findByName() {
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

//        정교한 테스트를 위해서 케이스 2개 생성
//        shift + f6 : Rename

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member result = repository.findByName("spring1").get(); // get() : Optional을 한번 열어서 가져오는 방법
        assertThat(result).isEqualTo(member1);

    }

    @Test
    public void findAll() {
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        List<Member> result = repository.findAll();

        assertThat(result.size()).isEqualTo(2);

    }

}
  • @AfterEach : 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이렇게 되면 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다. @AfterEach 를 사용하면 각 테스트가 종료될 때 마다 이 기능을 실행한다. 여기 서는 메모리 DB에 저장된 데이터를 삭제한다.
  • 테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존 관계가 있는 것은 좋은 테스트가 아니다.
  • 검증 할 수 있는 틀을 먼저 만든 후에 개발하는 것 (테스트 주도 개발 TDD)
  • 개발 과정에서 필수, 중요!

회원 서비스 개발

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) {
        // 같은 이름이 있는 중복 회원 X
        validateDuplicateMember(member);

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

    // Optional을 바로 반환하는 것을 권장하지 않는다.
    /* private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                Optional<Member> result = memberRepository.findByName(member.getName());
                result.ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    } */
// ctrl alt m => 코드를 메소드화 해줌.

    // 따라서 아래처럼 코드를 사용함.
    // 중복 회원 검증
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                //member에 값이 있으면
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    // repository는 단순히 저장소에 넣었다 빼는 느낌으로 네이밍하고 (기계적으로 개발)
    // service는 비즈니스에 가깝게 네이밍을 짓는다.(비즈니스에 의존적으로 개발)

    /*
     * 전체 회원 조회
     * */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

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

회원 서비스 테스트

ctrl + shift + t

기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하게 했다.

public class MemberService {

    //    같은 인스턴스에서 데이터를 사용하게 만들기 위해
    private final MemberRepository memberRepository = new MemoryMemberRepository();
}

회원 리포지토리의 코드가 회원 서비스 코드를 DI 가능하게 변경한다.

public class MemberService {

    //    같은 인스턴스에서 데이터를 사용하게 만들기 위해
//    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final MemberRepository memberRepository;

    //    외부에서 넣어주게 바꿔줌 (alt + Insert)
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

회원 서비스 테스트

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

    //  1. clear 해주기 위해 MemoryMemberRepository 가져옴

//    new로 다른 객체로 repository가 생성되면 내용물이 달라진다던지 좋지 않기 때문에,
//    MemberService의 MemoryMemberRepository와 MemberServiceTest의 MemoryMemberRepository는 서로 다른 DB라고 생각하면 빠름.
//    MemoryMemberRepository memberRepository = new MemoryMemberRepository();

//    따라서 같은것으로 테스트 하는 것이 맞기 때문에

    //  2. 돌 때 마다 DB의 정보(메모리)를 지워주는 코드
    @AfterEach // 테스트하나가 끝날때마다 실행하겠다.
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void 회원가입() {
        //given (이 데이터를 기반으로)- when (이걸 검증하는구나) - then (아 이곳이 검증 부구나) 문법, 상황에 따라 변형

        // given
        Member member = new Member();
        member.setName("hello");

//        member.setName("spring");
//        이미 중복되는 회원으로 가입하여 문제가 생겨 clear를 하고 싶은데 없네?

        // when
        // 우리가 저장을한게 리포지토리에 있는게 맞아?
        Long saveId = memberService.join(member);

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

    // 테스트는 정상 flow도 중요하지만, 예외(오류) flow가 더 중요하다.
    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

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

        //when
        memberService.join(member1);

        // 2번째 조인을 했을때 member의 이름이 같을때 validate에서 예외 발생
        // 해결하기 위해 try-catch 사용
//        try {
//            memberService.join(member2);
//            fail("예외가 발생해야 합니다.");
//        } catch (IllegalStateException e) {
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.123123");
//        }

//        하나때문에 try - catch 문을 쓰는것이 애매하므로 내장 메서드 사용
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
//        메시지 검증은 어떻게 할까요?
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        //then
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}
  • BeforeEach : 각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존 관계도 새로 맺어준다.

You might also enjoy