[Spring Boot] 간단한 예제 - 회원 관리 예제 2
지난 글에서는 리포지토리와 관련한 이번에는 회원 서비스 개발 및 테스트이다.
회원가입과 전체 회원 조회, 개별 회원 조회 이렇게 세 가지 서비스를 추가하려고 한다.
새로 service 패키지를 만들어서 그 안에 MemberService 클래스를 만든다. 그리고 아래 코드를 추가한다.
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 {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/**
* 회원가입
*/
public Long join (Member member){
//동명의 중복회원은 회원가입 불가
/* 자주 사용되는 내용은 메소드로 따로 뽑음 -> Ctrl + T
Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(m ->{
throw IllegalStateException("이미 존재하는 회원입니다");
}); // ifPresent 는 optional 이기 때문에 사용 가능. Optional 사용 안할시 if !null 이런 식으로 사용될 수 있음.
*/
validateDuplicateMember(member);
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);
}
}
회원 가입 시에 중복된 이름의 회원은 가입할 수 없도록 하기 위해서 Exception을 던져줘야 하는데, 다음 코드를 좀 더 상세히 보자.
Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(m ->{
throw IllegalStateException("이미 존재하는 회원입니다");
}); // ifPresent 는 optional 이기 때문에 사용 가능. Optional 사용 안할시 if !null 이런 식으로 사용될 수 있음.
Optional 객체인 result로 중복된 이름이 있다면 불러온다. 이 때 객체가 optional로 둘러쌓여 있어서 null일지라도 불러올 수 있고, ifPresent를 통해 null인지 확인한 후 null이 아니라면 람다식을 통해 뭔가 행동을 취해준다. 이 코드에서는 IllegalStateException을 던져줬다. Optional 객체를 사용하지 않고 그냥 null 값인지 확인하는 방식도 있긴 하지만 최근에는 Optional 객체를 많이 사용한다고 한다.
그리고 이렇게 좀 자주 사용될 수 있는 코드 같은 경우는 별도의 메소드로 빼주는 게 좋다. 너무 편하게도 인텔리제이에서는 리팩토링을 위한 방법들을 제공하는데 단축키로 (Mac 기준) ctrl + T를 누르면 리팩토링 관련 기능들이 나온다. 여기서 Extract method를 누르면 된다.
테스트 코드 작성
다음으로는 이렇게 작성된 코드들이 정상적으로 작동하는지, 특히 중복된 이름의 회원이 가입하려고 시도할 때 가입이 안되도록 잘 막고 있는지를 확인하기 위한 테스트 케이스들을 작성해줘야 한다. 테스트를 만들기에 앞서 DI(Dependency Injection, 의존성 주입)가 가능하도록 회원 리포지토리 코드를 수정해준다. 기존에는 회원 서비스가 회원 리포지토리를 직접 생성하게 했는데 코드는 아래와 같이 수정하면 된다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
⬇️
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
...
}
이후 테스트 코드들을 작성해주면 된다. 테스트 케이스는 소스코드에서 cmd + shift + T를 눌러서 쉽게 test 코드 껍데기를 만들 수 있다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
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 java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
//test는 직관적으로 쉽게 알아볼 수 있도록 메소드 이름을 한글로 적기도 한다. (빌드 될 때 테스트 코드는 포함되지 않음)
MemberService memberService;
MemoryMemberRepository memberRepository; //각 케이스별로 clear 해주기 위해서 가져와야 함.
@BeforeEach
public voide beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@Test
void 회원가입() {
//test code는 뭔가가 주어졌을 때, 이걸 하면, 이렇게 나와야 한다는 걸 테스트 하는 것 -> given, when, then
//given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//then
Member result = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(result.getName());
}
@Test
public void 중복회원예외(){
//given
Member member1 = new Member();
member1.setName("Spring");
Member member2 = new Member();
member2.setName("Spring");
//when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.")
/*
try{
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
*/
}
}
* beforeEach를 통해 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 새로 맺어준다.
** Test code는 1) 무언가가 주어졌을 때 2) 어떤 행동을 하면 3) 어떤 결과가 나와야 한다는 것을 테스트하는 과정이다. 따라서 주석으로 Given, When, Then을 적어두고 그에 맞는 코드를 작성하면 효과적으로 테스트를 할 수 있다. (모든 경우에 세 가지가 다 필요한 것은 아니니 케이스에 맞게 사용하면 된다)
*** 테스트를 할 때 정상 flow를 테스트하는 것도 중요하지만 예외적인 케이스를 테스트하는 것이 매우 중요하다. 따라서 이 예제에서는 중복 회원 예외 여부를 별도 케이스로 작성하여 테스트 해보았다.
*** 예외 케이스를 테스트하면서 어떤 Exception을 마주해야 정상적인 경우라면 try, catch를 사용할 수 있다. 하지만 좀 더 간단한 코드로는 Assertions의 assertThrows 메서드와 람다를 사용하는 것이 있다.

이렇게 테스트까지 정상적으로 완료된 것을 확인하면 된다!