TIL - 2022

[TIL 017] 스프링부트 - 회원 서비스 개발과 테스트

바랄 희 2022. 3. 27. 01:47

김영한 강사님의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 듣고 작성했습니다.

오류가 존재할 가능성이 다분합니다! 댓글로 알려주세요

 


회원 서비스 개발 전체 코드

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 {

    //final 은 불변이 아니라 재할당할 수 없도록 한다.
    // 최초 초기화나 상속 이후 변할 수 없는 값.
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /**
     * 회원 가입
     */

    public Long join(Member member){
        // 같은 이름이 있는 경우에는 안된다.
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    //만약 결과가 이미 존재한다면 (이도 optional 을 사용했기에 가능한 것.if null~ 로 하지 않아도 됨.)
    // 값이 중복되면 발생하는 오류를 발생시킨다.
    // ctrl+ T extract Method
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
       throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    /**
     * 전체 회원 조회
     */

    public List<Member> findMembers(){
            return memberRepository.findAll();
    }

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

}

 

이제까지 만들었던 것들이 레포지토리라면, 이번에는 서비스를 실제로 구현해주는 패키지를 생성하는 것이다. 

 

 

1. Join 가입하기

 

 /**
     * 회원 가입
     */

    public Long join(Member member){
        // 같은 이름이 있는 경우에는 안된다.
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

 

 

 

실제로 가입하는 것을 구현해주는 함수이다. 

Member 클래스를 파라미터로 받고, 이름이 같은 경우에는 가입을 하지 못하도록 한다. 

여기서 validateDuplicateMember 라는 함수는 원래 join 함수 내에 있었다. 해당 함수는 아래와 같다.

 

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

이는 위에서 선언해준 memberRepository 에서 이름으로 찾고, (getName은 member 클래스의 내장함수이다) 이가 존재한다면 오류를 발생시키는 함수이다. 

ifPresent 가 가능한 이유는 optional 로 감싸서 findByName 의 결과값을 반환하도록 했기 때문이다. optional 을 사용하지 않았다면 not null 과 같은 모양이 됐을 것이다. 

ifPresent 는 이가 실제로 존재하는지 확인하는 함수이다.

 

이가 위의 join 함수 내에 있었지만 ctrl + T -> extract Method 를 통해서 join 내의 메서드를 밖으로 빼낸 것이다. 그렇게 한 결과, 이름이 중복인 회원은 가입이 되지 않도록 하는 것이라는 로직이 명확히 보이게 된 것이다. 

 

중복 검사를 해준 뒤에는, 레포지토리에 저장하고 (이 역시도 레포지토리 내의 내장함수이다) 가입된 해당 멤버의 id 를 반환한다.

 

 

2. 회원을 찾는 함수(전체, 하나)

 

 public List<Member> findMembers(){
            return memberRepository.findAll();
    }

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

 

이 부분은 간단하니 짧게 설명하고 넘어가겠다. findMembers는 회원 전체를 찾는 함수이기에 레포지토리에 내장된 all 함수를 이용해 리스트로 반환한다.

findOne 은 MemberId 로 멤버 레포지토리 내의 내장함수로 멤버를 찾아 반환한다. 이가 null 일수도 있기에 optional 로 반환한다.

 

 


회원 서비스 테스트

 

이전에는 테스트 케이스를 전부 직접 생성하고 실행했었는데 이번에는 단축키를 이용해서 생성했다.

테스트하고자 하는 클래스명 (이번에는 MemberService 였다) 을 드래그한 상태에서 cmd+shift+T 를 누르면 테스트를 생성할 수 있다.

 

1. join 에 대한 테스트

 


    @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());

    }

 

테스트는 직관적일 필요가 있기 때문에 두가지 팁을 알려주셨다. 첫번째는 테스트 함수를 굳이 영어로 하지 않아도 된다는 점.

두번째는 given (~이 주어진 상황에서) when (~를 했을 때) then (어떻게 된다) 에 의거하여 코드를 작성하면 테스트를 더 효율적으로 보고 짤 수 있다는 것이다.

 

새로운 멤버 객체를 생성하고, 회원가입을 시도한다. join 함수는 가입된 회원의 id를 반환하기로 했기에 이를 saveId 라는 변수에 담고, 멤버 객체 중에서 해당 Id 를 지닌 멤버 개게를 findMember라는 이름의 객체에 담는다. 

이를 given 에서 생성한 멤버의 이름과 비교하여 일치하는지 확인한다.

일치한다면 정상적으로 저장 및 반환된 것이기에 테스트를 통과시킨다.

 

그러나 사실상 join 함수에서 실제로 중복인 이름의 저장이 정상적으로 막아지는지에 대한 테스트가 되지 않았기에 이를 테스트해야 한다.

 


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

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

        //when
        memberservice.join(member1);
        try{
        memberservice.join(member2);
        fail(); // 여기까지 오면 fail 인 것.
        }
        catch(IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.123");
        }//예외가 발생해야 한다.


        //then

}

 

member1 과 member2의 이름을 동일하게 하고, 오류가 발생하는 지점에서 try - catch 문을 사용한다. 

만일, try 문을 했는데 바로 catch 로 가지 않고, try 에 남아있는다면 테스트가 실패한 것이기에 해당 부분에 fail() 을 넣어준다.

catch 는 오류가 발생하는 해당 오류의 메시지와 미리 설정해둔 오류 메시지가 동일한지 확인한다. 동일하다면 이는 테스트에 통과한 것이다. 그러나, 오타가 발생하거나 조금이라도 오류 메시지가 틀린다면 테스트가 실패했다고 할 것이다. 그렇기에 try - catch 보다 효율적인 방법이 있다. 

 

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

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

        //when
        memberservice.join(member1);
        assertThrows(IllegalStateException.class,()->memberservice.join(member2));}

 

assertThrows 를 이용하는 것이다. 미리 설정해둔 오류인 IllegalStateException 의 클래스와, memberservice의 join 을 실행했을 때의 오류가 동일하다면 테스트가 성공하는 것이다. 

 

2. 테스트 이전과 이후에 각각 해주어야 할 일 

 

 MemberService memberservice;
    //테스트 코드의 경우에는 한국어로 적어도 무방하다. (가독성을 위해서)
    MemoryMemberRepository memberRepository;

@BeforeEach
    public void beforeEach(){
        memberRepository = new MemoryMemberRepository();
        memberservice = new MemberService(memberRepository);
    }

    @AfterEach //하나하나 끝날 때마다 실행되는
    public void afterEach(){
        memberRepository.clearStore();

    }

 

@BeforeEach는 각 테스트가 시작하기 전에 실행한다. 

 

이전에는 MemberRepository 를 테스트에서도 생성하고, MemberService 내에서도 생성했다. 그러나 이는 문제가 된다. 테스트에서 실행하는 레포지토리와 MemberService 내에서 사용하는 레포지토리가 달라지게 되는 것이기 때문이다. 따라서, MemberService에도 약간의 수정을 했다.

 

//수정 전
private final MemberRepository memberRepository = new MemberRepository();

//수정 후
private final MemberRepository memberRepository;

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

 

생성자를 추가하여 MemberService 객체를 생성할 때 아예 레포지토리를 담는 것으로 수정했다. (서비스에서 사용되는 DB와 테스트에서 사용되는 DB가 동일하도록! 사실상 현재에는 static 으로 모델을 생성했기에 문제가 되지 않지만, 이외의 경우에는 문제가 되기에 미연에 방지하는 것!)

 

즉, BeforeEach 는 각 테스트를 실행하기 전에 레포지토리를 생성하고, 이를 서비스에 담아주는 역할을 하는 것이다.

 

AfterEach 는 각 테스트가 끝난 뒤에 DB를 깨끗하게 비우는 것이다. 테스트끼리 DB로 인한 충돌이 발생하지 않도록 하기 위함이다.