본 포스팅은 인프런의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 강의를 수강하며 정리한 내용입니다.
백엔드 개발 순서
1. 비지니스 요구사항 정리
2. 회원 도메인과 리포지토리 만들기
3. 회원 리포지토리 테스트 케이스 작성
4. 회원 서비스 개발
5. 회원 서비스 테스트 ( JUnit 사용 )
이전 포스팅에서 3. 회원 리포지토리 테스트 케이스 작성까지 진행했습니다. 아래 링크를 참고해주세요^^ 이번 포스팅에서 4, 5번을 알아보도록 하겠습니다.
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번 줄 참고
-
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.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 = new MemberService();
-
// clear 해주기 위해 리포지토리 멤버변수 생성
-
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
-
@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() {
-
}
-
}
위 코드 로직의 문제점
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 와 관련된 개념은 다음 포스팅에서 좀 더 자세히 다루겠습니다.
'Dev > Spring Boot' 카테고리의 다른 글
Maven 프로젝트 실행 환경 구성 (0) | 2022.02.13 |
---|---|
[코드로 배우는 스프링 부트] 스프링과 빈의 의존관계 (1) | 2021.02.23 |
[코드로 배우는 스프링 부트] 회원 관리 예제(1) (1) | 2021.02.22 |
[코드로 배우는 스프링 부트] 정적 콘텐츠, 템플릿엔진, API 방식 (1) | 2021.02.18 |
[코드로 배우는 스프링 부트] 빌드하고 실행하기 (0) | 2021.02.18 |