본 포스팅은 인프런의 '스프링 입문 - 코드로 배우는 스프링 부트, 웹 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번 줄 참고
-
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 |