spring boot 03 [ 회원 관리 예제 ① ]
[ 비지니스 요구사항 정리 ]
(일단 가장 단순하게)
· 데이터: 회원ID, 이름
· 기능: 회원등록, 조회
· 아직 데이터 저장소가 선정되지 않음
· 컨트롤러 : 웹 MVC의 컨트롤러 역할
· 서비스 : 핵심 비지니스 로직 구현
· 리포지토리 : DB에 접근, 도메인 객체를 DB에 저장하고 관리
· 도메인 : 비지니스 도메인 객체( 예)회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨 )
MemberService --------------- > interface <MemberRepository> <-----------Memory MemberRepository
[ 회원 도메인과 리포지토리 ]
파일구조
domain 패키지에 Member.java를 생성한다.
public class Member {
private Long id;
private String name;
}
생성할 객체를 만들고 getter,setter를 만들어준다.
package hello.hellospring.domain;
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
}
repository 패키지에 interface로 MemberRepository 파일을 생성해주고
package hello.hellospring.repository;
import hello.hellospring.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);
List<Member> findAll();
}
Optional은 처음봤는데, java8의 기능으로
findBy--에서 값이 없을때 null을 그대로 반환하는 대신 optional을 써서 감싸서 반환하는방법이라고 한다.
try-catch로만 null을 잡아봤는데, 요즘은 Optional 방법을 더 선호한다고!
repository 패키지에 MemoryMemberRepository.java 파일을 생성해준다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long,Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
return null;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.empty();
}
@Override
public Optional<Member> findByName(String name) {
return Optional.empty();
}
@Override
public List<Member> findAll() {
return null;
}
}
MemberRepository.interface 파일을 implements 해주고 오버라이드를 해준뒤,
값을 넣어줄 Map을 생성해주고, id값은 sequence로 넣어줄거라서 초기화해준다.
오버라이드한 메소드들 값을 넣고 받을 수 있게 수정해주자
@Override
public Member save(Member member) {
member.setId(++sequence); // 아이디값은 시퀀스를 증가한 값을 넣어준다.
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); // null일 경우 옵셔널로 감싸서 반환해줄수있다.
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream().filter(member -> member.getName().equals(name)).findAny();
// lambda식을 사용해서 간단하게 값을 가져왔다.
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
이게 제대로 실행되는지는 테스트를 진행해보자
테스트 폴더에 패키지와 파일을 생성해준다. > src 폴더와 동일하게 적어주면 된다
코드작성
우선 save에 값이 제대로 들어오는지 보자
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import net.bytebuddy.implementation.auxiliary.MethodCallProxy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@Test //jUnit의 기능
public void save(){
Member member = new Member(); // src에서 생성한 Member 메소드 객체 생성해서 사용해준다
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
Assertions.assertEquals(member,result); // member와 result가 같은지 비교
}
}
테스트를 실행해보자
값이 참이라면
이렇게 나온다.
result 자리에 다른 값을 넣고 다시 테스트를 실행해보면
이렇게 오류 나오는것을 확인할 수 있다
다른방법!!
org.assertj.core.api를 눌러서 아래와 같이 코드를 작성해주면 똑같이 기능한다!
Assertions.assertThat(member).isEqualTo(result);
static import : Alt + Enter
Assertions에서 alt+enter치고 아래 나오는 Add on을 눌러주면 된다!
save는 테스트 통과, 이제 findByName을 가져오는지 확인한다
@Test
public void findByName(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
// 일부러 객체를 두개를 만들었다
Member result = repository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
이렇게 실행하면 테스트 통과가 되는데
Member result = repository.findByName("spring2").get();
assertThat(result).isEqualTo(member1);
spring2으로 변경하면 당연히 오류가 난다. 지금은 명확하게 차이가 느껴져서 괜찮겠지만 나중에 변수가 많아지면 이렇게 확인해보는게 좋은것같다
다음으로 findAll을 테스트해보자
@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);
}
findByName처럼 코드를 작성해주고 마지막에 List를 추가해서 값을 list로 받아온다음에 사이즈값을 확인해준다.지금 가져온값이 2개니까 테스트 결과도 참으로 나온다
여기서 숫자 2를 다른숫자로 변경하면 failed가 나오는게 맞다.
이제 전체코드를 테스트해본다
여기서 초록색 실행 버튼을 누르면 아까 테스트 통과했던 findByName이 오류가 뜬다.
테스트가 진행된 순서가 findAll() -> findByName() -> save() 인데 이때 순서를 변경하는거나 하지 않는다. 각 메서드의 의존관계없이 독립적인 상태를 유지해야 하기 때문에 !
findAll에서 이미 setName에 값이 저장되어버려서 findByName에서 오류발생한것!그럼 메소드 하나끝나고 하나 넘어갈때 clear를 해주는게 좋겠다!
일단 src에 있는 MemoryMemberRepository로 가서 다음 메서드르 만들어주자
public void clearStore(){
store.clear();
}
그리고 MemoryMemberRepositoryTest에서 위의 메서드를 받아오는 코드를 작성해준다. 이때 AfterEach 어노테이션을 사용한다
@AfterEach
public void afterEach(){
repository.clearStore();
}
이렇게하면 한개의 메소드 테스트가 끝나고 clear해주고 다음으로 넘어가기 때문에 전체코드 테스트를 할때에도 오류없이 진행이 된다!
테스트의 기초를 진행해보았다!
>> 실무에서는 테스트에서 Build툴이랑 연결해서 테스트 통과하면 넘어가지 못하게 막아버린다고 한다
>> 테스트 주도 개발은 테스트를 먼저 만들고 구현클래스를 만들어서 진행하는 방식이다(TDD - > 지금 해본 방식에서 거꾸로!