이번 글에서는 회원 도메인, 레포지토리, 서비스들과 각각의 테스트 케이스 까지 작성해 보는 강의를 듣고 복습 해보겠습니다.
hello1.hellospring1.domain package를 생성 후 Member class 생성
package hello1.hellospring1.domain;
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
또 hellospring1.respository.MemberRepository interface 생성
package hello1.hellospring1.repository;
import hello1.hellospring1.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
// Optional은 null값이 나올 때 null을 반환하는 것이 아닌 optional로 감싸서 반환
List<Member> findAll();
}
구현체인 class파일 생성 MemoryMemberRepository 생성
package hello1.hellospring1.repository;
import hello1.hellospring1.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository {
// option + enter 키로 import 해줌
private static Map<Long, Member> store = new HashMap<>();
// 실무에서는 동시성 문제가 있을 수 있기 때문에 공유되는 변수 일 때는
// 컨커런시 해시맵을 사용해야 하지만 예제이니 해시맵 사용
private static long sequence = 0L;
// 여기서도 원래는 automicLong 사용해야 함
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
// store에 넣기 전에 member에 아이디값을 세팅해주고 이름은 넘어온 상태를 저장
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
// 결과가 없을 때는 null이 발생하는데
// ofNullable을 사용하면 null값이어도 Optional로 감싸서 반환할 수 있다.
// 이렇게 감싸서 반환해 주면 클라이언트에서 사용할 수 있는 기능이 있다.
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
// lamda 표현식으로 구현
// member의 name값이 parameter로 넘어온 name과 같은지 확인하고
// 같은 경우에는 필터링이 되고 찾으면 반환이 된다.
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
// 자바에서 실무할 때는 List를 많이 사용한다.
// store.values()는 member들이다.
public void clearStore() {
store.clear(); // store를 비워줌
}
}
이 MemoryMemberRepository를 만들고 이게 정상적으로 동작하는지 확인하기 위해 테스트 케이스를 사용한다.
개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
main쪽이 아닌 test쪽에서 똑같은 package 생성 후 클래스파일을 생성한다.
package hello1.hellospring1.repository;
import hello1.hellospring1.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
// afterEach 없이 전체를 Test하면 findByName이 오류가 난다.
// 테스트 순서가 findAll이 먼저 실행되고 findByName이 실행되면서
// spring1과 spring2가 이미 저장되었기 때문에 오류가 난다.
// 그러므로 테스트가 하나 끝날 때 마다 데이터를 clear 해주어야 한다.
// 그 기능을 하는 것이 afterEach 이다.
repository.clearStore();
// MemoryMemberRepository에 clearStore를 통해 데이터를 clear해주는 메소드 생성
}
@Test
public void save() {
Member member = new Member();
member.setName("spring"); // 이름 세팅
repository.save(member);
Member result = repository.findById(member.getId()).get();
// 검증, 반환타입 Optional이면 get을 통해 꺼낼 수 있음
assertThat(member).isEqualTo(result);
// Assertions 기능 사용 result와 member의 값이 같은지 확인
}
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
// member1 > member2로 바꿀 때 shift + f6 사용시 한번에 바꿀 수 있음
Member result = repository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2); // 결과가 2개 인지?
}
}
지금까지 우리가 한 것은 repository를 만들고 test를 만들어서 테스트했지만
이를 반대로 테스트를 먼저 만들고 repository(구현 클래스)를 작성하는 방법을 TDD (테스트 주도 개발 - Test-Driven Development)라고 한다.
sevice package 생성 후 MemberService class 생성
package hello1.hellospring1.service;
import hello1.hellospring1.domain.Member;
import hello1.hellospring1.repository.MemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
// Test 쉽게 만들기 (단축키 command + shift + t 후 test 할 메소드 선택 후 ok)
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// constructor 사용 || MemberRepository를 new로 만드는게 아닌 외부에서 넣어주도록 만들어준다.
// 회원가입
public Long join(Member member) {
validateDuplicateMember(member);
// 중복 회원 검증 : 같은 이름의 중복 회원이 있으면 안된다.
memberRepository.save(member);
return member.getId();
}
// 같은 이름이 있는 중복 회원 X 메소드
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
// 메소드로 뽑기 (단축키 ctrl + t 에서 extract Method || option + command + M)
// IllegalStateException : 불법 또는 부적절한시기에 메소드가 호출되었음을 알립니다.
// Optional<Member> result = memberRepository.findByName(member.getName());
// result.ifPresent(m -> {
// throw new IllegalStateException("이미 존재하는 회원입니다.");
// }); 코드를 더 간단하게 만들기
}
// 전체 회원 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
// 회원 조회
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
위의 주석에서 볼 수 있듯이 public class MemberService 에서
commnad + shift + t 를 통해 쉽게 테스트 케이스를 만들 수 있다.
package hello1.hellospring1.service;
import hello1.hellospring1.domain.Member;
import hello1.hellospring1.repository.MemoryMemberRepository;
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.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
// MemberService에서 사용하는 new MemberMemoryRepository와
// test에서 사용하는 new MemberMemoryRepository가 다르다. 서로 다른 걸로 테스트를 하고 있으니 아래와 같이 바꾼다.
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
// 테스트 실행할 때 마다(실행 하기 전에) 독립적으로 생성
// MemoryMemberRepository를 만들어서 memberRepository에 넣어주고
// memberRepository를 MemberService에 넣어주면 같은 MemoryMemberRepository를 사용하게 된다.
// 이런 것을 DI(dependency injection) 의존성 주입이라 한다.
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
void 회원가입() { // given when then 패턴 사용
// given 이 데이터를 기반
Member member = new Member();
member.setName("hello");
// when 이걸 검증
Long saveId = memberService.join(member);
// then 검증 부분
Member findMember = memberService.findOne(saveId).get();
// 이 데이터가 repository에 있는게 맞는지?
assertThat(member.getName()).isEqualTo(findMember.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("이미 존재하는 회원입니다.");
/*
memberService.join(member1);
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
*/
// then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
다음 시간에 DI 의존성 주입에 대하여 더 알아볼 예정이다.