1. 비즈니스 요구사항 정리 2. 회원 도메인과 레포지토리 만들기 3. 회원 레포지토리 테스트 케이스 작성 4. 회원 서비스 개발 5. 회원 서비스 테스트
1. 비즈니스 요구사항 정리
데이터: 회원ID, 이름 기능: 회원 등록, 조회 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)
일반적인 웹 애플리케이션 계층 구조
컨트롤러
웹 MVC의 컨트롤러 역할
도메인
비즈니스 도메인 객체 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
서비스
도메인 객체를 가지고 핵심 비즈니스 로직 구현(회원 중복xx 등등)
레포지토리
데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
클래스 의존관계
1. 아직 데이터 저장소가 선정되지 않았기 때문에, 우선 인터페이스(MemberRepository)로 구현 클래스를 변경할 수 있도록 설계한다. 2. 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민 중인 상황으로 가정한다. 3. 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소(MemoryMemberRepository가 MemberRepository를 구현)를 사용한다.
package hello.hellospring.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;
}
}
2. repository 패키지(폴더) 생성 뒤 MemberRepository 인터페이스 생성
save: 회원을 저장 findById: ID로 회원을 찾는다. findByName: name으로 회원을 찾는다. findAll: 회원 정보를 조회한다.
Optional은 java8에서 추가된 기능으로써, findById 나 findByName 등으로 회원의 정보를 가져왔을 때, 정보가 null 일 수가 있는데, 이 null을 처리하는 데 있어서 null을 그대로 반환하는 대신 Optional을 이용하여 한번 감싸준 후 반환하는 방식을 많이 쓴다. (Optional 안에 있는 메서드를 사용할 수 있다.)
3. MemberRepository 인터페이스를 구현할 구현체 생성(MemoryMemberRepository.java 클래스)
filter내의 람다식을 통해, 매개변수로 받은 name과 member.getName()이 일치하는 지 확인한다. Map의 값들을 스트림으로 필터링해서 하나라도 맞으면 반환한다. store.values(). stream(). filter(). findAny(); -> 루프로 돌리면서 하나라도 찾아지면 결과를 Optional로 반환하고, 하나도 못 찾는다면 optional에 null이 포함되어 반환된다.
Stream에서 어떤 조건에 일치하는 요소(element) 1개를 찾을 때, findAny( )와 findFirst( )를 사용 1. findAny( ) : Stream에서 가장 먼저 탐색되는 요소를 리턴 2. findFirst( ) : 조건에 일치하는 요소들 중에 Stream에서 순서가 가장 앞에 있는 요소를 리턴, 조건에 일치하는 요소가 없으면 empty 리턴
findAll( )
strore에 있는 모든 member들을 반환한다. (map에 있는 values)
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
테스트 케이스 할 때는 @Test(import org.junit.jupiter.api.Test) 어노테이션을 꼭 붙여줘야 한다.
package hello.hellospring.repository;
import org.junit.jupiter.api.Test; //@Test
import org.junit.jupiter.api.AfterEach; // @AfterEach
class MemoryMemberRepositoryTest { // 굳이 public으로 하지 않아도 된다.)
// MemoryMemberRepository 객체를 생성한다.
MemoryMemberRepository repository = new MemoryMemberRepository();
@Test
public void save(){
}
@Test
public void findByName(){
}
@Test
public void findAll(){
}
// 테스트가 끝날때마다 지워주는 메서드
@AfterEach
public void afterEach(){
repository.clearStore();
}
}
save( )
Member객체와 repository(MemoryMemberRepository)를 이용해서 테스트를 해본다.
@Test
public void save(){
Member member = new Member();
member.setName("Spring"); // 멤버 객체 이름을 Spring으로 생성
repository.save(member); // MemoryMemberRepository에 이름을 저장
Member actual = repository.findById(member.getId()).get();
// import static org.assertj.core.api.Assertions.*;
// == Assertions.assertEquals(member, actual);
assertThat(actual).isEqualTo(member);
}
Member result = repository.findById(member.getId()).get();
System.out.println("result = " + (result == member)); // (actual == expect)
- findById의 반환 타입이 Optional이기 때문에 Optional에서 값을 꺼낼 때는 get() 사용(테스트 코드니까 get으로, 원래는 좋은 방법 x)- member와 repository에서 꺼낸 member(result)가 일치하는지 확인할 수 있다. - 이렇게 출력해서 볼 수 있지만 매번 글자로 볼 수 없기 때문에 Assertions를 사용한다.
- Add on demand static import for 'org.assertj.core.api.Assertions'를 클릭하면 static으로 import 할 수 있다. -> (command + Enter / alt + Enter) - 그러면 Assertions 없이 assertThat메서드만 코드에 써도 된다.
*실무에서는 이것을 빌드 툴과 엮어서 테스트 케이스가 통과하지 않으면 다음 단계로 못 넘어가게 한다!*
findByName( )
2명의 이름을 member에 저장하고, result객체와 각 member를 비교하는 테스트이다. 현재 Spring2이라는 이름의 회원이 result객체이기 때문에, result와 member2는 같다.
@Test
public void findByName(){
Member member1 = new Member();
member1.setName("Spring1");
repository.save(member1);
// 좀더 정교한 테스트를 위해서 (변수이름 바꾸기: 드래그+shift+F6)
Member member2 = new Member();
member2.setName("Spring2");
repository.save(member2);
Member actual = repository.findByName("Spring2").get();
// Member actual = repository.findByName(member2.getName()).get();
Assertions.assertEquals(member, actual);
//assertThat(actual).isEqualTo(member2);
}
Member result = repository.findByName("Spring2").get( );
assertThat(result).isEqualTo(member1);
Spring2과 member1을 비교시
Member result = repository.findByName("Spring1").get( );
assertThat(result).isEqualTo(member1);
Spring1과 member1를 비교
Member result = repository.findByName("Spring1").get( );
assertThat(result).isEqualTo(member2);
Spring1과 member2를 비교
findAll( )
Member 객체 2개를 저장 후, result List에 findAll 메서드를 통해 저장된 result의 사이즈가 2개인지 확인한다. -> 멤버를 2개를 만들었으니 size가 2가 나와야 한다.
@Test
public void findAll(){
Member member1 = new Member();
member1.setName("Spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("Spring1");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
member1의 name와 id를 볼 수 있고, member2의 name와 id를 볼 수 있다.
테스트 케이스의 순서는 보장이 안되기 때문에 모든 테스트는 순서와 상관없이 메서드 별로 다 따로 동작하도록 설계를 해야 한다.(순서 의존적으로 설계하면 안 된다.)
오류가 나는 이유는 findAll( )과 findByName( )에서의 변수 이름이 같기 때문이다. 이때 의존관계없이 테스트가 끝날 때마다 콜백 함수(@AfterEach)를 이용해서 Data를 Clear 해줘야 한다.
MemoryMemberRepository에서 clearStore( )를 가져와서 afterEach( ) 메서드에서 실행되도록 한다.
@AfterEach // import org.junit.jupiter.api.AfterEach;
public void afterEach(){
repository.clearStore();
}
- 이제까지는 구현 클래스 Repository를 작성 후, 맞는지 검증하는 과정으로 작성했는데, 이것과 반대로 테스트 케이스를 먼저 작성 후 그다음에 Repository를 작성할 수도 있다. - 이 방법을 TDD(Test-Driven Development), 즉 테스트 주도 개발이라고 한다. (틀을 만들고 그다음 구현 클래스) - 테스트 케이스는 필수!!!!!!