회원 관리 예제 - 서비스 개발 및 테스트

guswls·2023년 1월 16일
1

스프링 입문

목록 보기
5/13
post-thumbnail

이 포스트는 김영한 이사님의 스프링 입문 강의를 듣고 작성하였습니다.

이번에는 저번 시간에 이어서 회원 관리에 관련된 서비스 개발과 테스트를 진행하고자 한다. 서비스리포지토리도메인을 활용해서 실제 비지니스 로직을 담당하는 부분이다.

1. 회원 가입 구현

//memberService.java
package memberpractice.memberpractice.service;

import memberpractice.memberpractice.domain.Member;
import memberpractice.memberpractice.repository.MemberRepository;
import memberpractice.memberpractice.repository.MemoryMemberRepository;

import java.util.Optional;

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

    /**
     * 회원 가입
     */
    public Long join(Member member){
        //같은 이름이 있는 중복 회원 X
        memberRepository.findByName(member.getName())
            .ifPresent(m -> {
                throw new IllegalStateException("이미 존재하는 회원입니다.");
            });

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

우선 서비스를 만들기 위해서는 리포지토리도메인이 필요하다. 여기서는 MemoryMemberRepository를 가져와서 사용하기로 하였다.

첫번째로 만들 기능은 회원가입 기능 join 이다. 사실 MemoryMemberRepository에 구현된 save만 호출하면 구현은 가능하다. 하지만 우리가 생각해야 할 점은 같은 이름이 존재하는 경우 회원가입을 하지 못하게 하는 것이다.

그렇기 때문에 save를 호출하기 전에 findByName을 실행하여 만약 해당 이름으로 값이 이미 존재한다면 예외를 던지도록 구현을 하였다. 여기서 ifPresent()null이 아닌 값이 존재할 때 실행된 부분을 구현할 수 있는데 이것은 우리가 결과값을 Optional로 한번 감싸서 사용했기 때문이다.

또한 중복 이름 검사같은 경우 로직이 존재하기 때문에 이러한 경우는 메소드로 뽑아서 사용하는 것이 좋다. InteliJ에서는 윈도우 기준 crtl + shift + alt + t 단축키를 사용하여 Extract Method를 실행하면 선택한 영역을 메소드로 뽑아서 사용할 수 있다.

결과물은 다음과 같다.

//memberService.java
package memberpractice.memberpractice.service;

import memberpractice.memberpractice.domain.Member;
import memberpractice.memberpractice.repository.MemberRepository;
import memberpractice.memberpractice.repository.MemoryMemberRepository;

import java.util.Optional;

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

    /**
     * 회원 가입
     */
    public Long join(Member member){
        validateDuplicateMember(member);

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

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

위와 같이 로직을 메소드로 분리하면 더욱 직관적인 코드 이해가 가능하다.

2. 회원 조회, 전체 회원 조회 구현

//memberService.java
package memberpractice.memberpractice.service;

import memberpractice.memberpractice.domain.Member;
import memberpractice.memberpractice.repository.MemberRepository;
import memberpractice.memberpractice.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);

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

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

회원 조회는 아주 간단하게 우리가 MemoryMemberRepository에서 구현한 findAllfindOne을 호출함으로써 쉽게 구현할 수 있다.

참고: 서비스와 리포지토리의 네이밍

서비스 안의 메소드 이름이 우리가 리포지토리를 구현할 때에 비해 조금 더 비지니스에 가까운 것을 확인할 수 있다. 왜냐하면 서비스비지니스 로직을 담당하고 리포지토리는 단순히 저장소에 접근하여 데이터를 넣었다 뺐다 하는 역할을 담당하기 때문이다.

역할에 맞는 네이밍

우리는 맡은 역할에 맞도록 서비스에는 비지니스에 가까운 이름을, 리포지토리에는 좀 더 개발에 관련된 이름을 사용하여야 한다.

3. 서비스 Test

test 디렉토리에 바로 테스트 파일을 생성하는 것이 아닌 윈도우 기준 crtl + shift + t단축키를 활용하면 아래와 같이 바로 테스트를 만들어주는 창이 뜨게 된다.
위 화면에서 테스트하고자 하는 메소드를 선택하고 OK를 누르면 테스트를 자동으로 생성하여 준다.

3-1. 회원 저장 Test

회원 저장에 대한 Test는 다음과 같다.

//memberServiceTest.java
package memberpractice.memberpractice.service;

import memberpractice.memberpractice.domain.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

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

class MemberServiceTest {

    MemberService memberService = new MemberService();

    @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
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

하지만 이렇게만 Test를 진행한다면 중복 이름 검사에 관한 Test는 진행할 수 없다. 중복 회원 검사에 대해서도 Test Case를 작성하자.

3-2. 중복 회원 검사 Test

//memberServiceTest.java
package memberpractice.memberpractice.service;

import memberpractice.memberpractice.domain.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

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

class MemberServiceTest {

    MemberService memberService = new MemberService();

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

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

        //when
        memberService.join(member1);
//        try{
//            memberService.join(member2);
//            fail();
//        }catch (IllegalStateException e){
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        
        //then
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

예외에 대한 검증을 할 때 위의 주석과 같이 try-catch를 통해 오류를 잡아내도 되지만 assertThrows를 통해 예외가 발생했을 때 반환되는 예외의 종류를 가져와서 값을 비교할 수도 있다.

3-3. 저장소 리셋

리포지토리와 마찬가지로 이전에 했던 테스트가 현재의 테스트에 영향을 미치면 안되기 때문에 테스트가 끝날 때마다 저장소를 초기화해주어야 한다.

//memberServiceTest.java
package memberpractice.memberpractice.service;

import memberpractice.memberpractice.domain.Member;
import memberpractice.memberpractice.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

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

class MemberServiceTest {

    MemberService memberService = new MemberService();
    MemoryMemberRepository repository = new MemoryMemberRepository();

	//추가
    @AfterEach
    public void aftereach(){
        repository.clearStore();
    }

    @Test
    void 회원가입() {
        //given : 무언가 주어졌을 때
        Member member = new Member();
        member.setName("spring");

        //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("spring");

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

        //when
        memberService.join(member1);
//        try{
//            memberService.join(member2);
//            fail();
//        }catch (IllegalStateException e){
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        //then
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

여기서 문제점이 발생한다.

우리는 MemoryMemberRepositoryMemberServiceTest에서 new를 통해 생성하기 전에 MemberService에서 이미 MemoryMemberRepository에서 생성을 하였다.

test의 리포지토리service의 리포지토리는 서로 다른 객체라는 것이다. 이러한 경우엔 어떻게 해결해야 할까??

3-4. DI를 통한 리포지토리 생성

우선 MemberService의 필드를 변경할 필요가 있다.

package memberpractice.memberpractice.service;

import memberpractice.memberpractice.domain.Member;
import memberpractice.memberpractice.repository.MemberRepository;

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

public class MemberService {
	//변경
    private final MemberRepository memberRepository;
	
    //추가
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     */
    public Long join(Member member){
        validateDuplicateMember(member);

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

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

기존의 memberServicememoryMemberRepository를 직접 new를 통해 생성하던 방법에서 생성자를 통해 외부에서 생성된 memoryMemberRepository를 끌어오는 방법으로 변형하였다.

이것을 우리가 그동안 많이 들어봤을 의존성 주입, Dependency Injection, DI라고 부른다.

즉, memberServiceTest에서 리포지토리를 끌어와서 사용함으로써 memberService에서도 동일한 객체를 사용하게 하는 것이다.

그러므로 memberServiceTest의 코드도 변경하여야 한다.

package memberpractice.memberpractice.service;

import memberpractice.memberpractice.domain.Member;
import memberpractice.memberpractice.repository.MemberRepository;
import memberpractice.memberpractice.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);
    }

    @AfterEach
    public void aftereach(){
        MemberRepository.clearStore();
    }

    @Test
    void 회원가입() {
        //given : 무언가 주어졌을 때
        Member member = new Member();
        member.setName("spring");

        //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("spring");

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

        //when
        memberService.join(member1);
//        try{
//            memberService.join(member2);
//            fail();
//        }catch (IllegalStateException e){
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        //then
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

이렇게 @BeforeEach를 사용하여 모든 테스트케이스의 시작 전에 memoryMemberRepository 객체를 생성하고 이를 memoryService생성자의 인자로 넘겨주는 작업을 하도록 프로그램을 짰다.

이렇게 했을 때 Test가 정상적으로 진행된 것을 확인할 수 있다.

참고 : test의 네이밍

test의 네이밍의 경우 한글로 적어도 상관은 없다.

참고 : given-when-then 방법

//memberServiceTest.java
package memberpractice.memberpractice.service;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService = new MemberService();

    @Test
    void 회원가입() {
        //given : 무언가 주어졌을 때

        //when : 이것을 실행하면

        //then : 결과로 이것이 나와야 된다.
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

어떠한 테스트 케이스를 짤 때 위의 주석과 같이 테스트를 구성하여 짜는 것을 의미한다. 자세한 설명은 여기를 참고하자. 처음 테스트를 짤 때는 이 패턴에 익숙해지면서 상황에 맞게 변형하는 것이 좋다.

4. 총정리

저번 포스팅에서 구현한 도메인과 리포지토리를 바탕으로 이번엔 비지니스 로직을 담당하는 서비스까지 구현을 해보고 Test를 진행하였다. 사실 Service의 구현 그 자체 보다는 이에 대한 test에 대해 중점적으로 다뤄졌다.

특히 given-when-then부터 DI까지 많은 중요한 개념들을 함축하여 배울 수 있었다. 이에 대해서는 추후에 자세히 정리해볼 예정이다.

아직까진 스프링 그 자체에 대해서 배우는 것이 아닌 순수 자바코드로 실습을 진행하고 있으나 점차 기본적인 스프링의 웹 어플리케이션 구조와 테스트 방법에 대한 가닥이 잡히고 있는 것 같다.

다음 포스팅에서는 스프링의 의존관계 설정에 관해서 간단하게 다뤄보고자 한다.

profile
안녕하세요

2개의 댓글

comment-user-thumbnail
2023년 1월 16일

잘 보고 갑니다 ㅎㅎ

1개의 답글