컨트롤러 : 웹 MVC의 컨트롤러
서비스 : 핵심 비즈니스 로직
리포지토리 : 데이터베이스에 접근
도메인 : 비즈니스 도메인 객체
요구사항1. 회원 도메인에는 회원 ID와 이름 데이터가 존재
요구사항2. 회원 등록과 조회(ID조회, 이름 조회)의 기능
요구사항3. 데이터 저장소가 선정되지 않았다는 전제(JPA, JDBC 등등...)
요구사항1 반영 완료!
package hello.hello_spring.domain;
public class Member {
private Long id;
private String name;
public Member(Long id, String name) {
this.id = id;
this.name = 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;
}
}
요구사항2 반영 완료!
요구사항3에 따라 인터페이스를 만들고 이를 구현하는 식으로 작성
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member); // 회원 저장
Optional<Member> findById(Long id); // 회원 조회(ID조회)
Optional<Member> findByName(String name); // 회원 조회(이름 조회)
List<Member> findAll(); // 회원 목록 보여주기
}
package hello.hello_spring.repository;
import hello.hello_spring.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) {
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());
}
}
테스트 코드를 작성하려는 클래스에 커서를 두고 Ctrl + Shift + T
를 누르면 자동으로 테스트 클래스를 자동으로 생성할 수 있다.
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
class MemoryMemberRepositoryTest {
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
public void save() {
Member member = new Member("TEST");
memberRepository.save(member);
Member findMember = memberRepository.findById(member.getId()).get();
Assertions.assertThat(findMember).isEqualTo(member);
}
@Test
public void findByName() {
Member member1 = new Member("TEST1");
memberRepository.save(member1);
Member member2 = new Member("TEST2");
memberRepository.save(member2);
Member result = memberRepository.findByName("TEST2").get();
Assertions.assertThat(result).isEqualTo(member2);
}
@Test
public void findAll() {
Member member3 = new Member("TEST3");
memberRepository.save(member3);
Member member4 = new Member("TEST4");
memberRepository.save(member4);
Member member5 = new Member("TEST5");
memberRepository.save(member5);
List<Member> result = memberRepository.findAll();
Assertions.assertThat(3).isEqualTo(result.size());
}
}
@AfterEach
어노테이션이 붙은 테스트 코드를 테스트 하나하나 끝날 때마다 실행되는 메서드이다.
테스트는 순서가 없이, 의존관계가 없이 작성이 되어야 한다. 따라서 하나의 테스트가 끝날 때마다 저장소와 데이터를 깔끔하게 비워야 한다.
public void clearStore() {
store.clear();
}
package hello.hello_spring.service;
import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Long join(Member member) {
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
package hello.hello_spring.service;
import hello.hello_spring.domain.Member;
import hello.hello_spring.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 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 join() {
// given(무언가가 주어졌는데...)
Member member = new Member("TEST");
// when(실행했을 때...)
Long saveId = memberService.join(member);
// then(결과가 이렇게 나와야 한다...)
Member findMember = memberService.findOne(saveId).get();
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
void duplicate() {
Member member1 = new Member("TEST");
memberService.join(member1);
Member member2 = new Member("TEST");
/*
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
*/
assertThrows(IllegalStateException.class, () -> memberService.join(member2));
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
회원 서비스 테스트 코드를 작성할 때 회원가입, 중복 로직 등의 테스트를 수행하려면 리포지토리가 있어야 했다. 그런데 MemberService
의 기존 코드를 보면 아래와 같이 new
연산자를 사용해서 객체를 생성하는 것을 볼 수 있다.
private final MemberRepository memberRepository = new MemoryMemberRepository();
또 회원 서비스 테스트 코드에서 역시 리포지토리를 필요로 하여 new
연산자를 사용해서 객체를 생성하면 두 객체는 같은 객체가 아니라 서로 다른 객체라고 생각할 수 있기 때문에 서로 다른 리포지토리 객체를 사용하는 것 아닌가.. 라는 생각이 들었다. 그래서 이런 문제를 해결하기 위해 DI(Dependency Injection, 의존관계 주입) 개념이 도입되었다.
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
회원 서비스 코드에 있는 중복 가입 불가 로직을 처리하는 서비스 코드이다. ifPresent()
를 사용하여 만약 해당 회원의 이름이 존재한다면 예외를 발생시키게끔 한 것이다.
예외는 잡거나 던질 수 있는데 이 때, try ~ catch
구문을 사용할 수 있다.
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
try
블럭 안에 예외가 발생할 수 있는 코드를 작성한다. 그리고 해당 예외가 발생하면 catch
문의 에러와 일치하는지 비교 후 일치하면 catch
문에서 예외를 처리할 수 있다.
assertThrows(IllegalStateException.class, () -> memberService.join(member2));
try ~ catch
문을 사용할 수도 있지만 위와 같이 작성할 수도 있다.
assertThrows()
를 사용해서 두 번째 인자의 상황이 발생하면 해당 클래스 타입의 예외가 발생하게끔 한 것이다.