본문 바로가기

Dev/Spring Boot

[코드로 배우는 스프링 부트] 회원 관리 예제(2)

본 포스팅은 인프런의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 수강하며 정리한 내용입니다.

 

백엔드 개발 순서

1. 비지니스 요구사항 정리 

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

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

4. 회원 서비스 개발

5. 회원 서비스 테스트 ( JUnit 사용 )

 

 

 이전 포스팅에서 3. 회원 리포지토리 테스트 케이스 작성까지 진행했습니다. 아래 링크를 참고해주세요^^ 이번 포스팅에서 4, 5번을 알아보도록 하겠습니다. 

 

[코드로 배우는 스프링 부트] 회원 관리 예제(1)

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

no-delay-code.tistory.com

 

 

 

4. 회원 서비스 개발

 이번에는 회원 서비스 클래스를 만들겠습니다. 서비스는 도메인과 리포지토리를 활용해서 실제 비지니스 로직을 작성하는 부분입니다.

hello.hellospring 패키지 밑에 service 패키지를 생성하고, MemberService 클래스를 생성합니다. 

package hello.hellospring.service;
 
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
 
public class MemberService {
 
    // 서비스를 작성하려면, 리포지토리가 필요하므로 멤버변수 선언
    private final MemoryMemberRepository memberRepository = new MemoryMemberRepository();
 
    /*
    * 회원 가입
     */
    public Long join(Member member){
 
        // 중복 회원 검증
        validateDuplicateMember(member);
        // 리포지토리에의 save 메소드 호출
        memberRepository.save(member);
        return member.getId();
    }
 

   // extract method 해서 리팩토링
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하면 회원입니다.");
                });
    }
 
    /*
     * 전체 회원조회
     */
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }
 
    /*
     * id로 특정 회원조회
     */
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}
 

 

서비스 vs 리포지토리

 메소드명도 리포지토리 메서드보다 '비지니스' 적인 느낌의 용어를 써야합니다. 그러면 개발할때, 메소드 명을 보고 어느 부분을 봐야할 지 유추가 가능합니다. 즉, 서비스는 비지니스를 처리하고 리포지토리는 데이터의 입출력 특징이 있는 것이 특징입니다.

 

 

5. 회원 서비스 테스트

 작성한 서비스 클래스에 마우스를 가져가서 ctrl + shift + t 를 하면, 테스트 클래스를 단축키로 생성할 수 있습니다. JUnit5로 설정하고 테스트할 메소드를 체크합니다. test/hello.hellospring/service/MemberServiceTest.java 위치에 생성됩니다. 이때 메소드명을 한글로 적기도 합니다. 영어권에서 작업하는 것이 아니라면 한글이 더 직관적이기 때문입니다. 프로젝트가 빌드될 때 테스트 코드는 포함되지 않습니다.

 

 메소드를 작성할 때, given, when, then의 로직으로 작성하는 것이 좋습니다. 이런 상황이 주어졌을 때, 이런 조건이라면, 이렇게 작동해라. 이렇게 주석을 달아놓으면 어떤 이슈인지 알기가 좋습니다. 물론 이 패턴이 맞지 않는 상황도 있지만, 처음 스프링을 공부할 때는 도움이 된다고 합니다.

 

테스트는 정상 플로우도 중요하지만 예외 플로우도 매우 중요합니다. 43번 줄 참고

  1. package hello.hellospring.service;

  2.  
  3. import hello.hellospring.domain.Member;

  4. import hello.hellospring.repository.MemoryMemberRepository;

  5. import org.assertj.core.api.Assertions;

  6. import org.junit.jupiter.api.AfterEach;

  7. import org.junit.jupiter.api.Test;

  8.  
  9. import static org.assertj.core.api.Assertions.assertThat;

  10. import static org.junit.jupiter.api.Assertions.assertThrows;

  11. import static org.junit.jupiter.api.Assertions.fail;

  12.  
  13.  
  14. class MemberServiceTest {

  15.  
  16.     MemberService memberService = new MemberService();

  17.  
  18.     // clear 해주기 위해 리포지토리 멤버변수 생성

  19.     MemoryMemberRepository memberRepository = new MemoryMemberRepository();

  20.  
  21.     @AfterEach

  22.     // 동작이 끝날때마다 어떤 처리를 해줍니다. (콜백 메소드)

  23.     public void afterEach() {

  24.         memberRepository.clearStore();

  25.     }

  26.  
  27.     @Test

  28.     void 회원가입() {

  29.         // given

  30.         Member member = new Member();

  31.         member.setName("user1");

  32.         // when

  33.         Long saveId = memberService.join(member);

  34.  
  35.         // then

  36.         Member findMember = memberService.findOne(saveId).get();

  37.         // 

  38.         // add static import (블럭잡고 전구 클릭)

  39.         assertThat(member.getName()).isEqualTo(findMember.getName());

  40.     }

  41.  
  42.     @Test

  43.     public void 중복_회원_예외(){

  44.         // given

  45.         Member member1 = new Member();

  46.         member1.setName("spring1");

  47.  
  48.         Member member2 = new Member();

  49.         member2.setName("spring1");

  50.  
  51.         // when

  52.         memberService.join(member1);

  53.  
  54.         /*

  55.         try{

  56.             memberService.join(member2);

  57.             fail();

  58.         }catch (IllegalStateException e){

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

  60.         }

  61.          */

  62.  
  63.         // try-catch 보다는 assertThrows 사용

  64.         // assertThrows(기대하는 예외, 로직) : 로직이 수행되는 과정에서 어떤 예외가 발생하길 기대

  65.         IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));

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

  67.         // then

  68.     }

  69.     @Test

  70.     void findMembers() {

  71.     }

  72.  
  73.     @Test

  74.     void findOne() {

  75.     }

  76. }

 

 

위 코드 로직의 문제점

 MemberServiceTest.java 파일의 16 번째 줄에서 MemberService memberService = new MemberService(); 가 실행되면서 새 리포지토리가 생성되고, 19 번째 줄에서  MemoryMemberRepository memberRepository = new MemoryMemberRepository(); 를 하면서 리포지토리를 생성합니다. 현재 리포지토리 MemberRepository가 가진 멤버변수(db)가 static으로 설정되어 있어서 문제가 없습니다. private static Map<Long, Member> store = new HashMap<>();

 하지만 만약 static 속성이 빠졌다면, 테스트 코드에선 서로 다른 두개의 db 에 접근하는 문제가 발생합니다. 뿐만 아니라 하나의 테스트에서 두개의 리포지토리를 이용할 수 없습니다. 이를 해결하기 위해 다음과 같이 코드를 수정해야 합니다.

 

code

MemberService.java

package hello.hellospring.service;
 
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
 
public class MemberService {
 
    // 서비스를 작성하려면, 리포지토리가 필요하므로 멤버변수 선언
    private final MemoryMemberRepository memberRepository;
 
    // add constructor
    // 리포지토리를 직접 new로 생성하지 않고, 외부에서 넣어주도록 구현
    public MemberService(MemoryMemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
 
    /*
    * 회원 가입
     */
    public Long join(Member member){
 
        // 중복 회원 검증
        validateDuplicateMember(member);
        // 리포지토리에의 save 메소드 호출
        memberRepository.save(member);
        return member.getId();
    }
 
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }
 
    /*
     * 전체 회원조회
     */
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }
 
    /*
     * id로 특정 회원조회
     */
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}
 

 

MemberServiceTest.java

package hello.hellospring.service;
 
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
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.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
 
 
class MemberServiceTest {
 
    // 리포지토리를 인자에 넣어줌
    MemberService memberService;
    // clear 해주기 위해 리포지토리 멤버변수 생성
    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("user1");
        // when
        Long saveId = memberService.join(member);
 
        // then
        Member findMember = memberService.findOne(saveId).get();
 
        // add static import (블럭잡고 전구 클릭)
        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);
 
        /*
        try{
            memberService.join(member2);
            fail();
        }catch (IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
         */
 
        // try-catch 보다는 assertThrows 사용
        // assertThrows(기대하는 예외, 로직)
        IllegalStateException e = assertThrows(IllegalStateException.class() -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        // then
    }
    @Test
    void findMembers() {
    }
 
    @Test
    void findOne() {
    }
}

 

Dependency Injection

 서비스 클래스에서 리포지토리를 new로 직접 생성하지 않고, consturctor을 이용하여 외부에서 삽입해주도록 코드를 수정합니다. 즉, 테스트 코드를 작성할 때, 서비스 클래스를 만드는 과정에서 인자로 리포지토리를 삽입합니다. 그러면, 한 테스트에서 하나의 리포지토리만을 접근하게 됩니다.

 MemberService 입장에서 봤을때, 외부에서 넣어주는 객체를 받아 사용합니다. 이것을 Dependency Injection 이라고 합니다. DI 와 관련된 개념은 다음 포스팅에서 좀 더 자세히 다루겠습니다.