✔️ 목표
간단하게 비즈니스 요구사항은
데이터: 회원 ID, 이름
기능: 회원 등록, 조회
그리고 가상의 시나리오로 아직 데이터 저장소가 선정되지 않았다고 가정.
우선 main/java/com.example.hellospring/ 디렉토리 안에 domain 이라는 이름의 패키지 생성
⬇️ 그 안에 Member라는 java class 생성 후 아래의 코드 작성
package com.example.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;
}
}
그리고 다시 domain과 같은 위치에 repository 패키지 생성 (회원 객체를 저장할 저장소)
⬇️ 패키지에 MemberRepository interface를 생성한 뒤 아래와 같은 코드 작성
package com.example.hellospring.repository;
import com.example.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은 java 8에 들어간 기능. 간단하게 findById 이런식으로 가져오는데 이 내용이 null일 수 있음. 이때 그대로 Null을 반환하지 않고 optional로 감싸 반환하는 방식을 씀.)
save하면 회원 정보가 저장됨.
그 다음부터는 findById나 findByName으로 회원 정보를 찾아올 수 있음.
findAll로 지금까지 저장된 모든 회원 정보를 불러올 수 있음. (list를 모두 반환)
이제,
Repository 패키지에 MemoryMemberRepository java class를 생성
public class MemoryMemberRepository implements MemberRepository {
이렇게 implements를 해주고 generate로 overide method를 선택하여 모두 생성시켜줌.
⬇️ 그리고 아래와 같이 코드 작성
package com.example.hellospring.repository;
import com.example.hellospring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L; // sequnece는 0, 1, 2 이렇게 그냥 key 값을 생성해주는
@Override
public Member save(Member member) {
// store에 넣기 전에 sequence값을 증가시켜 Id값을 설정해줌.
// 이전에 Id는 시스템이 정해준다고 했는데 우리는 여기서 정해주는 것.
member.setId(++sequence);
// map에 저장이 됨.
store.put(member.getId(), member);
return member;
}
@Override
// store에서 꺼내오면 됨.
public Optional<Member> findById(Long id) {
// store.get(id)라고만 하면 null이 반환될 가능성이 있는데 이걸 그대로 반환하게되므로
// Optional로 null을 감싸 반환.
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
// parameter로 넘어온 name과 같은지 비교. 그 중에서 찾으면 반환.
.filter(member -> member.getName().equals(name))
// findAny()는 하나라도 찾는 것.
// 맵을 돌면서 하나 찾아지면 그냥 반환하고 끝까지 못 찾으면 optional에 null이 포함이 돼서 반환.
.findAny();
}
@Override
public List<Member> findAll() {
// java에서 실무할 때 list를 많이 씀.
// store에 있는 values가 멤버들.
return new ArrayList<>(store.values());
}
}
개발한 기능을 실행해 테스트 할 때 자바의 main 메소드를 통해 실행하거나, 웹 애플리케이션의 컨트롤러를 통해 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어려우며 여러 테스트를 한 번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해 이러한 문제를 해결한다.
test/java/com.example.hellospring 디렉토리 안에 repository 라는 패키지를 생성.
그 안에 MemoryMemberRepositoryTest 라는 java class 생성
⬇️ 아래와 같이 코드 작성
package com.example.hellospring.repository;
import com.example.hellospring.domain.Member;
import org.junit.jupiter.api.Test;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
// 반환타입이 optional이기 때문에 .get()으로 꺼낼 수 있음.
// get으로 그냥 꺼내는 것이 좋은 방법은 아님.
Member result = repository.findById(member.getId()).get();
System.out.println("result = " + (result == member));
}
}
해당 버튼으로 run 시켜 결과를 확인
❗️ 그렇지만 이렇게 직접 글자로 볼 순 없으니까 assert라는 기능을 이용
System.out.println("result = " + (result == member));
⬆️ 위의 코드를 ⬇️ 아래와 같이 변경해줌.
단, 이때 이것을 씀.
Assertions.assertEquals(member, result);
그리고 실행시켜보면
⬆️ 아무 결과도 출력되지는 않지만 이렇게 녹색 체크가 뜬 것을 확인.
⬇️ 실패 케이스
코드를 이렇게 바꾸어 실행시켜보면 이렇게 에러 뱉는 모습을 확인 😦
그런데 이제 위와 다르게 Assertions를 ⬆️ 이걸로 선택 (이게 더 편하게 사용할 수 있음)
import static org.assertj.core.api.Assertions.*;
⬆️ 이 코드를 추가하여 (원래 Assertions.assertThat()... 이지만)
assertThat(member).isEqualTo(result);
⬆️ 이렇게 간단하게 쓸 수 있음.
실행 결과는 위의 코드와 같이 성공한 것을 확인할 수 있음. 물론 result대신 null을 넣는다면 위와 같이 에러를 확인할 수 있음. (생략)
@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("spring1").get();
에서 spring1을 spring2로 수정해보기. 이때 isEqualTo로 member1을 넣었기 때문에 에러가 뜨는 것이 맞음.
⬇️ 결과
📌 test1, test2를 둘 다 실행하고 싶다면 test 중 해당 파일 자체를 실행(나는 이때 test파일 자체를 run했기 때문에 더 뜨는 것)
@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);
}
⬇️ 결과 에러 케이스를 확인하려면 ➡️ 여기서 isEqualTo(3)으로 바꾸면 당연히 에러
⬇️ 이제 파일 자체를 실행 시켜보자 이렇게 에러 발생
📌 그 이유는 지금 실행 순서를 보면 findAll() ➡️ findByName() ➡️ save() 인데 실행 순서는 보장이 안되어 있음. 즉, 순서와 상관없이 메소드가 따로 동작할 수 있도록 설계해야 함. 순서에 의존하도록 설계하면 절!대 안 됨.
findAll()이 실행될 때 이미 spring1, spring2 를 저장해버렸는데 그 뒤에 또 똑같은 spring1, 2를 쓰니까 이때 저장한 spring1, 2가 아닌 그 전에 저장했던 spring1, 2 가 나오게 되어버린 것.
그러므로 테스트가 하나 끝나고 나면 다음 것이 제대로 동작하도록 clear 과정이 필요.
⬇️ MemoryMemberRepository.java 에 아래와 같은 코드를 추가
public void clearStore() {
store.clear();
}
⬇️ 그리고 다시 MemoryMemberRepositoryTest.java의 repository 객체 생성 바로 밑에 아래의 코드 추가 (위치가 헷갈리면 아래의 전체 코드를 참조)
@AfterEach
public void afterEach() {
repository.clearStore();
}
이렇게되면 테스트를 하나씩 마칠 때마다 레퍼지토리를 지우게 됨. (즉, 순서와 상관이 없어짐)
@AfterEach: 한번에 여러 테스트를 실행하면 DB에 직전 테스트의 결과가 남을 수 있음. 이렇게되면 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있음. @AfterEach를 사용하면 각 테스트가 종료될 때마다 이 기능을 실행하게 함. 여기서는 메모리 DB에 저장된 데이터를 삭제함.
테스트는 각각 독립적으로 실행되어야 함. 테스트 순서에 의존 관계가 있는 것은 좋은 테스트 ❌
⬇️ 수정 후 결과
📌 그런데 지금과 반대로 테스트 케이스를 먼저 작성한 후 MemoryMemberRepository를 작성할 수도 있음. 이를 테스트 주도 개발이라고 함. (TDD: Test Driven Development. 예를 들어 별을 만든다고 할 때 별 모양 틀을 먼저 만들어놓고 별을 만든 후 틀에 별이 맞는지 확인하는.)
테스트는 실무에서 빠질 수 없음. 그만큼 중요하기 때문에 테스트에 대해서는 깊게 공부가 필요!!
package com.example.hellospring.repository;
import com.example.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
// 반환타입이 optional이기 때문에 .get()으로 꺼낼 수 있음.
// get으로 그냥 꺼내는 것이 좋은 방법은 아님.
Member result = repository.findById(member.getId()).get();
// System.out.println("result = " + (result == member));
// Assertions.assertEquals(member, null);
assertThat(member).isEqualTo(result);
}
@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);
}
@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);
}
}
src/main/java/com.example.hellospring 아래 service 패키지 생성
service 안에 MemberService라는 java class 생성
(알고 지나갈 요구사항 - 같은 이름을 가진 회원은 가입 불가)
private final MemberRepository memberRepository = new MemoryMemberRepository();
⬆️ 코드를 추가한 다음 ⬇️ 아래의 코드도 입력 (회원가입)
// 회원가입
public Long join(Member member) {
// 같은 이름을 가진 중복 회원은 가입 불가
Optional<Member> result = memberRepository.findByName(member.getName());
// 멤버가 존재하는지 확인 후 존재한다면 "이미 존재하는 회원입니다." 출력
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
memberRepository.save(member);
return member.getId();
}
ifPresent 이런 것도 optional로 감싸주었기 때문에 사용할 수 있었던 것. 참고로 orElseGet() (값이 있으면 꺼내고 없으면 이러한 메소드를 실행해라) 이런걸 주로 많이 씀.
‼️ 물론 코드를 작성하며 빨간 줄이 뜨면 import는 기본적으로 해주어야 함 ‼️
📌 (참고) 위의 코드를
// 같은 이름을 가진 중복 회원은 가입 불가 Optional<Member> result = memberRepository.findByName(member.getName()); // 멤버가 존재하는지 확인 후 존재한다면 "이미 존재하는 회원입니다." 출력 result.ifPresent(m -> { throw new IllegalStateException("이미 존재하는 회원입니다."); });
⬇️ 아래와 같이 간단하게 작성할 수 있음 (같은 기능을 하는 코드)
// 같은 이름을 가진 중복 회원은 가입 불가 memberRepository.findByName(member.getName()) // 멤버가 존재하는지 확인 후 존재한다면 "이미 존재하는 회원입니다." 출력 .ifPresent(m -> { throw new IllegalStateException("이미 존재하는 회원입니다."); });
⬆️ 이 부분을 이렇게 드래그 한 뒤 (mac기준) control+T 단축키를 눌러 ⬆️ method 검색 후 extract method 선택
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
// 멤버가 존재하는지 확인 후 존재한다면 "이미 존재하는 회원입니다." 출력
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
⬆️ 이름은 validateDuplicateMember 로 하면 위와 같은 코드가 생성
⬇️ 그리고 save 윗줄을 아래와 같이 수정해줌
validateDuplicateMember(member);
📌 (참고) repository는 단순히 저장소에 넣었다 뺐다 하는 단순한 이름으로 지었지만 service는 이름을 비즈니스에 가깝게 지음. 보통 service class는 비즈니스에 가까운 이름을 지어야 함. 그래야 개발할 때 개발자든 기획자든 쉽게 매칭시킬 수 있음.
// 전체회원 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
예전에는 test 디렉토리에서 패키지를 만들어 테스트를 진행했었음.
더 편한 방법: 테스트 할 클래스에서 command + shift + T 를 누르면
⬇️ 아래와 같은 창이 뜸. ⬇️ 클릭하고 이렇게 체크박스를 선택해 준 뒤 OK ⬇️ 그럼 이렇게 자동으로 패키지와 파일이 생성
📌 테스트 코드는 과감하게 한글로 작성하여도 됨(직관적)
given - when - then 주석을 적극 활용하도록
코드가 길어질수록 유용함.
⬇️ 알아서 생성된 파일(코드)에 아래의 코드 추가
MemberService memberService = new MemberService();
⬇️ 그리고 join을 회원가입이라고 수정해 코드를 작성
@Test
void 회원가입() {
// given (무언가가 주어지고)
Member member = new Member();
member.setName("hello");
// when (이것을 실행했을 때)
Long saveId = memberService.join(member);
// then (결과는 이것이 나와야 한다)
Member findMember = memberService.findOne(saveId).get();
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}
➡️ 이렇게 코드를 작성하고 Assertions. 여기서 (mac기준) option+return 을 눌러 해당 옵션 선택.
⬇️ 그러면 아래와 같이 그 줄의 코드가 다음과 같이 바뀔 것임.
assertThat(member.getName()).isEqualTo(findMember.getName());
⬇️ 결과 (코드를 잘 짰기 때문에 성공할 수밖에 없음)
📌 test는 성공 케이스도 중요하지만 예외가 잘 작동하는지 확인하는 것이 중요
@Test
// 중복회원 검증 로직이 잘 작동하는가 확인
public void 중복_회원_예외() throws Exception {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
// 일부러 멤버2도 똑같이 이름을 설정
member2.setName("spring");
// when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// then
}
(설명) ⬇️
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
⬆️ 는 memberService.join(member2)를 했을 때 오는 예외를 받은 것이 e.
⬇️ 결과
‼️ 이제 이도 마찬가지로 clear 과정이 필요함!!
MemoryMemberRepository memberRepository = new MemoryMemberRepository(); @AfterEach public void afterEach() { memberRepository.clearStore(); }
⬆️ 위의 코드 추가해주기
📌 (참고) control + R 키를 누르면 저번에 실행했던 것을 실행시킬 수 있음
🧐 여기서 test 케이스에 있는 MemoryMemberRepository memberRepository = new MemoryMemberRepository(); 이것과 MemoryMemberRepository에 있는 private static Map<Long, Member> store = new HashMap<>(); 와 각각 인스턴스가 되어 문제가 생길 수 있음. (지금이야 static으로 되어있지만 그게 아닐경우 완전히 다른 DB 가 되어버림)
즉, 어찌됐든 같은 것으로 테스트하는 것이 맞는건데 다른 repository가 테스트 되고 있다는 것.
✔️ 이제 같은 인스턴스를 사용할 수 있도록 코드를 수정해주자.
MemberService.java에서
private final MemberRepository memberRepository = new MemoryMemberRepository();
⬆️ 이 코드를 아래처럼 바꾼뒤 constructor를 생성시키면 public 저 코드가 자동 생성됨.
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
➡️ 이렇게 되면 새로운 memberrepository를 직접 생성하는 것이 아니라 외부에서 넣어주도록 코드를 바꿔준 것이 됨.
이제 다시 MemberServiceTest.java로 가서
MemberService memberService;
MemoryMemberRepository memberRepository;
⬆️ 이렇게 코드를 수정해주고
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
⬆️ 이 코드를 추가 작성해주면 됨
📌 결론: 같은 memory repository 를 사용하도록 해주었음
package com.example.hellospring.service;
import com.example.hellospring.domain.Member;
import com.example.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 {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
void 회원가입() {
// given (무언가가 주어지고)
Member member = new Member();
member.setName("hello");
// when (이것을 실행했을 때)
Long saveId = memberService.join(member);
// then (결과는 이것이 나와야 한다)
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
// 중복회원 검증 로직이 잘 작동하는가 확인
public void 중복_회원_예외() throws Exception {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
// 일부러 멤버2도 똑같이 이름을 설정
member2.setName("spring");
// when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
와 벌써 내용이 어렵네