1. 비즈니스 요구사항 정리
2. 회원 도메인과 리파지토리 만들기
1. MemberRepository 인터페이스 생성
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
2. 인터페이스를 구현하는 MemoryMemberRepository 생성
public class MemoryMemberRepository implements MemberRepository {
// key=id, value=Member 형식으로 저장
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@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));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream().filter(member -> member.getName().equals(name)).findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
3. 테스트 케이스 작성
개발한 기능을 실행해서 테스트할 때 자바의 main 메소드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고, 여러 테스트를 한 번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
TDD(Test-Driven Development): 테스트 주도 개발
소프트웨어 개발 방법론 중 하나로, 테스트를 개발하는 것부터 시작하여 개발을 진행하는 방식
작성한 코드를 통과시키기 위해 개선 작업을 진행하며, 이를 반복하면서 소프트웨어를 점진적으로 개발해 나간다. TDD를 통해 개발하면 테스트 커버리지를 높이고, 코드의 품질을 향상시킬 수 있으며, 유지보수와 리팩토링이 용이해지는 장점이 있다.
내가 구현한 방식은 객체를 생성하고 테스트를 진행했기 때문에 TDD 방식은 아니다. TDD 방식의 개발 순서는 아래와 같다.
-
더보기테스트 작성 (Write a Test)
먼저 구현하려는 기능에 대한 테스트를 작성한다. 이 테스트는 해당 기능이 어떤 동작을 가져야 하는지를 명시적으로 정의한다. 테스트는 실패할 것으로 예상한다. -
더보기테스트 실행 및 실패 확인 (Run the Test and Verify Failure)
작성한 테스트를 실행하고, 해당 테스트가 실패하는 것을 확인한다. 이는 아직 구현되지 않은 기능이기 때문에 예상한 결과가 나오지 않을 것이다. -
더보기기능 구현 (Implement the Feature)
테스트를 통과시키기 위해 해당 기능을 구현한다. 이때, 최소한의 코드만 작성하여 테스트를 통과시킬 수 있는 상태로 구현한다. -
더보기테스트 실행 및 성공 확인 (Run the Test and Verify Success)
구현한 기능에 대해 다시 테스트를 실행하고, 테스트가 성공하는지 확인한다. 이때, 테스트가 성공한다면 해당 기능이 올바르게 구현되었다는 것을 의미한다. -
더보기리팩토링 (Refactor)
테스트가 성공한 후에는 코드를 개선하고, 중복을 제거하거나 가독성을 높이는 등의 리팩토링 작업을 수행한다. 이때, 테스트를 통과하는지를 항상 확인하면서 리팩토링을 진행해야 한다. -
더보기반복 (Repeat)
위의 과정을 반복하여 새로운 기능을 추가하거나 기존 기능을 개선해 나간다. 매번 작은 단계로 개발을 진행하면서 테스트 주도로 애플리케이션을 점진적으로 완성시킨다.
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);
Member result = repository.findById(member.getId()).get();
assertThat(member).isEqualTo(result); // member == result이면 test pass
}
@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);
}
}
@AfterEach: 테스트가 끝날 때마다 실행되도록 설정
4. 회원 서비스 개발
서비스단에선 Repository에서보다 비즈니스적인 네이밍을 권장
public class MemberService {
private final MemberRepository repository = new MemoryMemberRepository();
// 회원가입 (중복 이름 불가)
public Long join(Member member) {
validateDuplicateMember(member); // 중복 이름 체크
repository.save(member);
return member.getId();
}
// ctrl + alt + shift + T (메소드로 빼는 단축키)
private void validateDuplicateMember(Member member) {
repository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 이름입니다.");
});
}
// 전체 회원 조회
public List<Member> findMembers() {
return repository.findAll();
}
// id로 조회
public Optional<Member> findOne(Long id) {
return repository.findById(id);
}
}
5. 테스트 케이스 작성
ctrl + shift + T: 테스트 코드 자동 작성 단축키
테스트 케이스는 과감하게 한국어 네이밍을 해도 괜찮음
class MemberServiceTest {
MemberService service = new MemberService();
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("hello");
// when
Long saveId = service.join(member);
// then
Member findMember = service.findOne(saveId).get();
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
service.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> service.join(member2));
// assertThrows: 예외가 발생하는지 확인하는 메소드
assertThat(e.getMessage()).isEqualTo("이미 존재하는 이름입니다.");
// try {
// service.join(member2); // 중복
// } catch (IllegalStateException e) {
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// }
// then
}
assertThrows: 예외가 발생하는지 확인하는 메소드
DI 의존성 주입
위의 코드에서 보면 new로 repository를 새 객체로 생성함 -> Service에 있는 repository랑 테스트 코드에 있는 repository랑 서로 다른 인스턴스임
새로 인스턴스를 만들어 쓸 이유가 없고 객체의 불변성이 보장되지 않음
코드 수정
1. MemberService에 Repository를 주입하는 생성자 추가
public MemberService(MemberRepository repository) {
this.repository = repository;
}
2. Service Test에서 BeforeEach를 이용해 매개변수로 repository 주입 (DI)
MemberService service;
MemoryMemberRepository repository;
@BeforeEach
public void beforeEach() {
repository = new MemoryMemberRepository();
service = new MemberService(repository);
}
-> 이렇게 함으로써 하나의 repository만으로 테스트를 할 수 있게 됨
'인프런 강의 > 김영한 Spring' 카테고리의 다른 글
[Spring Boot] AOP (0) | 2023.12.19 |
---|---|
[Spring Boot] 회원 예제 만들기 (3) - 스프링 DB 접근 기술 (0) | 2023.11.25 |
[Spring Boot] 회원 예제 만들기 (2) - 웹 MVC 개발 (1) | 2023.11.25 |
[Spring Boot] 스프링 빈과 의존 관계 (0) | 2023.11.25 |
[Spring MVC] 정적 컨텐츠, 템플릿 엔진, API (0) | 2023.11.24 |