인프런의 김영한님의 스프링입문 강의를 듣고 정리한 내용입니다.
1. 비즈니스 요구사항 정리
2. 회원 도메인과 레포지토리 만들기
3. 회원 레포지토리 테스트 케이스 작성
4. 회원 서비스 개발
5. 회원 서비스 테스트
4. 회원 서비스 개발
회원 레포지토리와 도메인을 이용해 비즈니스 로직을 작성하는 회원 서비스 클래스를 만든다.
1. Service 패키지 생성 뒤 MemberService.java 클래스 생성
src > main> java > hello > hellospring > Service > MemberService.java
package hello.hellospring.service;
import hello.hellospring.repository.MemberRepository;
public class MemberService {
// 회원 서비스를 만들려면 MemberRepository 인스턴스가 필요
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
2. join 회원가입 메서드 작성
public Long join(Member member){
validateDuplicateName(member); // 중복회원검증(같은 이름이 있으면 안된다.)
memberRepository.save(member);
return member.getId(); // 회원가입을 하면 id를 반환해준다.
}
// 메서드 추출 (추출할 부분 드래그 + Ctrl + Alt + M)
private void validateDuplicateName(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 이름입니다.");
});
}
Optional<Member> result = memberRepository.findByName(member.getName()); result.ifPresent(m -> { throw new IllegalStateException("이미 존재하는 이름입니다."); });
- ctrl + alt + v : 인스턴스화 자동 완성 (result 변수 만들 때)
- 과거에는 값을 꺼내 if, null로 비교했다면, 지금은 null일 때, Optional로 감싸주었기 때문에 Optional의 다양한 메서드를 이용할 수 있다.( ifPresent: 값이 있다면 실행하는 Optional의 메서드 )
- Optional <Member>를 빼서 코드를 작성하는 것은 권장하지 않기 때문에 Optional <member>는 생략해준다.memberRepository.findByName(member.getName()) .ifPresent(m -> { throw new IllegalStateException("이미 존재하는 이름입니다."); });
- 메서드로 만들 때의 단축키 : 추출할 부분 드래그 + Ctrl + Alt + M
private void validateDuplicateName(Member member) { // Optional<Member>를 빼서 코드를 작성하는 것은 권장하지 않는다. memberRepository.findByName(member.getName()) .ifPresent(m -> { throw new IllegalStateException("이미 존재하는 이름입니다."); }); }
메서드가 완성되었다!
repository의 경우, findby, save 등 단순하게 저장소에 데이터를 처리하는 느낌이라면 service는 비즈니스 로직에 가깝게 구성되어 있기 때문에(join 등) 메서드 이름도 그것에 맞게 작성하는 것이 좋다.
3. 전체 회원을 조회하는 findMembers( ) 작성
전체 회원을 조회할 때는 findAll( )을 호출하고, 한 명의 회원을 조회할 때는 findById( )를 호출한다.
// 전체 회원 조회
public List<Member> findMembers(){
return memberRepository.findAll(); // findAll은 데이터타입이 List이기 때문에 OK
}
// 한 명의 회원 조회
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
5. 회원 서비스 테스트
1. Service 폴더와 그 안에 MemberServiceTest.java 클래스 생성
src > test > java > hello > hellospring > service > Member.ServiceTestjava
단축키를 이용해 Service 프로젝트(폴더)와 그 안에 MemberServiceTest.java 클래스를 만들 수 있다.
테스트하고자 하는 클래스에서 ctrl + shift + T 하면 Create Test 팝업창이 뜨고 거기서 생성할 수 있다.
package hello.hellospring.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
@Test
void join() {
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
*given(주어진 것, 상황), when(실행문), then(결과) 패턴을 기본으로 진행하면 좋다.*
1. join( ) 회원가입 작성
빌드 될 때 테스트 코드는 포함되지 않기 때문에 테스트 코드는 한글로 바꿔도 된다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class MemberServiceTest {
MemberService memberService = new MemberService();
@Test
void 회원가입() {
//given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member); // login(join) 검증
//then(결과)
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
Long saveId = memberService.join(member);
- MemberService.java에서 join의 return은 member.getId이다. -> 회원가입을 하면 id를 반환한다. -> 그래서 변수명이 saveId인 것이다.
Member findMember = memberService.findOne(saveId).get();
- 결과를 get으로 받아서 findMember에 저장한다.
assertThat(member.getName()).isEqualTo(findMember.getName());
- member의 이름이 findMember의 이름과 같은가?
*테스트는 정상플로우도 중요하지만 예외 플로우가 훨씬 더 중요하다. 회원가입의 핵심인 중복 회원 예외 코드를 작성해서 테스트를 해봐야 한다.
예외를 try catch로 잡을 수 있지만, assertThrows로 해보는 것도 좋다.*
2. 중복회원 예외 코드 작성
try-catch문 + assertThat( ).isEqualTo( )
@Test
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(); // import static org.junit.jupiter.api.Assertions.fail;
} catch(IllegalStateException e){ //validateDuplicateName()
assertThat(e.getMessage()).isEqualTo("이미 존재하는 이름입니다.");
}
}
try{
memberService.join(member2);
fail(); // import static org.junit.jupiter.api.Assertions.fail;
}
- 강제로 fail 시키게 되는 데 이 코드까지 온다는 건 중복회원이 아니라는 뜻이다.
catch(IllegalStateException e){
assertThat(e.getMessage()).isEqualTo("이미 존재하는 이름입니다.");
}
- IllegalStateException과 "이미 존재하는 이름입니다."는 MemberSerive.java의 validateDuplicateName( )에서 가져온 것 만약 "이미 존재하는 회원입니다"같이 메시지를 다르게 하면 에러가 뜬다.
assertThrows + Ramda
//when
memberService.join(member1);
//import static org.junit.jupiter.api.Assertions.assertThrows;
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 이름입니다.");
assertThrows(IllegalStateException.class, () -> memberService.join(member2));
- member2를 join할 때, 예외를 발생시킨다!!(중복회원이기 때문에) : 람다식으로 작성
- 만약 IllegalStateException 대신 NullPointerException을 넣으면 오류 발생!(예외 타입이 안 맞기 때문에)
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 이름입니다.");
- 만약 메시지도 검증하고 싶다면 앞에서 assertThrows한 것을 ctrl + alt + v로 해서 인스턴스를 자동 완성해서 그 변수를 사용한다.
만약 테스트를 구현할 때 이름을 동일하게 쓰면?
@Test
void 회원가입() {
//given
Member member = new Member();
member.setName("Spring");
...
}
@Test
void 중복_회원_예외(){
//given
Member member1 = new Member();
member1.setName("Spring");
DB에 데이터가 누적이 되고, 제대로 삭제되지 않아서 회원가입 테스트 메서드가 에러가 발생하게 된다. 그래서 Clear를 해줘야 하지만, MemberServiceTest에는 MemberService 클래스만 있고, clearStore()가 있는 MemoryMemberRepository 클래스가 없다.
그래서 MemoryMemberRepository를 가져와야 한다.
class MemberServiceTest {
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
// Test가 끝날 때마다 DB의 값 청소
@AfterEach
public void afterEach(){ //MemoryMemberRepositoryTest에서 가져옴
memberRepository.clearStore();
}
참고로 shift + F10을 누르면 이전에 실행된것이 실행된다.
4. 인스턴스 중복 없애기
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
class MemberServiceTest {
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
MemberServiceTest와 MemberService에서 사용하는 repository객체가 new로 인해 서로 다른 인스턴스이기 때문에 문제가 될 수 있고, 테스트할 때는 동일한 Repository를 이용하는 것이 좋기 때문에 같은 인스턴스를 쓰도록 바꿔주는 게 좋다.
현재는 임시 DB store로 쓸 Map과 index로 쓸 sequence를 static으로 사용하기 때문에 문제가 없지만, static이 아니라면, 위에 new로 생성한 객체들은 다른 DB이기 때문에 문제가 발생할 수 있다.
4-1. MemberService 변경
new 연산자로 인스턴스 생성 -> 생성자 주입(MemberService를 생성할 때 외부에서 넣어주도록)
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach //import org.junit.jupiter.api.BeforeEach;
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
- @BeforeEach로 각 테스트를 실행하기 전에 memberRepository 객체를 생성하고 memberService를 생성할 때 넣어준다.
- MemberService 입장에서는 직접 new 연산자로 인스턴스 생성을 하지 않고, 외부에서 넣어주는 것을 받게 된다.
-> 이것을 의존성 주입 DI(Dependency Injection)이라고 한다.
-> DI로 인해 MemberService 객체와 MemberServiceTest 객체가 동일한 Repository를 쓰게 되는 것이다.
의존성 주입 DI(Dependency Injection)
DI, 의존성 주입은 필요한 객체를 직접 생성하는 것이 아닌 외부로부터 필요한 객체를 받아서 사용하는 것이다.
이를 통해 객체간의 결합도를 줄이고 코드의 재활용성을 높여준다.
'Java > Spring입문' 카테고리의 다른 글
Spring 입문(9) 스프링 빈과 의존관계-자바 코드로 스프링 빈 등록 (0) | 2023.05.03 |
---|---|
Spring 입문(8) 스프링 빈과 의존관계-컴포넌트 스캔(@애너테이션) (0) | 2023.05.03 |
Spring 입문(6) 회원관리 예제 - 백엔드 개발-1(테스트케이스작성까지) (0) | 2023.04.20 |
[Spring입문 - Eclipse(2)] 백엔드 개발 - 회원 관리 예제(테스트포함) (0) | 2022.11.30 |
[Spring입문 - Eclipse(1)] View 환경설정 및 빌드하고 실행, 스프링 웹 개발 기초 (0) | 2022.11.30 |