회원관리 예제를 만들기 위한 전체 과정은 아래와 같다.
일반적인 웹 애플리케이션 계층 구조 (출처 - 인프런)
컨트롤러 : 웹 MVC의 컨트롤러 역할
도메인 : 회원, 주문, 쿠폰처럼 데이터베이스에 저장하고 관리되는 비즈니스 도메인 객체
서비스 : 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직을 구현한 객체 (ex) 중복 가입 불가)
리포지토리 : 데이터베이스에 접근하여 도메인 객체를 DB에 저장하고 관리
회원 비즈니스 로직에는 회원 서비스가 존재한다. 회원 레퍼지토리는 interface로 구현 클래스를 변경할 수 있도록 설계하고, 아직 데이터 저장소가 선정되지 않아 우선 가벼운 메모리 기반의 데이터 저장소 구현체를 만들 것이다.
Member.java - 회원 객체
package hello.hellospring.domain;
public class Member {
private Long id; //우선 시스템에서 임의로 설정한 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;
}
}
MemberRepository.java - 회원 리포지토리 인터페이스
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
//리포지토리 안의 4가지 기능
Member save(Member member); //회원을 저장
Optional<Member> findById(Long id); //id로 회원을 찾음.
Optional<Member> findByName(String name); //name으로 회원을 찾음.
List<Member> findAll(); //지금까지 저장된 모든 회원 리스트를 반환.
}
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; //0,1,2...의 key값을 생성
@Override
public Member save(Member member) {
member.setId(++sequence); //member 저장시 sequence 값을 올려가며 id를 setting.
store.put(member.getId(), member); //member의 id를 저장
return member; //저장된 결과 반환
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); //Optional-null이어도 감싸서 반환.
}
@Override
public Optional<Member> findByName(String name) {
//member의 name과 인자로 받은 name이 일치하는지 비교
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values()); //member들을 반환
}
}
이렇게 작성한 코드가 제대로 동작하는지 확인하기 위해 테스트 케이스를 작성한다. main 메소드나 컨트롤러를 통해 테스트를 하는 방법은 시간이 오래 걸리며 반복 실행을 하는 것과 여러 테스트를 한 번에 실행하기가 어렵다. 이러한 문제 해결을 위해 자바에서는 JUnit이라는 프레임워크로 테스트 코드를 만들고 실행하여 테스트를 진행한다.
테스트 코드는 main이 아닌 test 아래에 작성한다.
아래와 같이 테스트 코드를 작성하여 각 메소드가 정상적으로 실행되는지 테스트한다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@Test //아래 메소드를 실행시킬 수 있음.
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
//검증 (member와 리포지토리에서 꺼낸 값이 같다면 true)
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);
}
}
이때, 테스트는 순서 상관없이 메소드별로 따로 진행한다. 따라서 위 테스트들을 한 번에 진행하여 findAll()이 먼저 실행되는 경우, findByName()에서 이미 생성된 객체를 또 만들고 있으니 오류가 발생한다.
이러한 경우를 방지하기 위해 테스트를 하나 끝내고 나면 data를 clear해줘야 한다. 따라서 main 아래 있는 memory repository에 객체를 비워주는 메소드를 작성하고, 테스트 코드에는 테스트가 끝날 때마다 repository를 지워주는 코드를 추가시켜준다.
MemoryMemberRepository.java
public void clearStore() {
store.clear();
}
MemoryMemberRepositoryTest.java
@AfterEach //하나의 메소드 실행이 끝날 때마다 어떤 동작을 하도록 함.
public void afterEach() {
repository.clearStore();
}
즉, 테스트는 서로 의존관계 없이 설계가 되어야 한다. 그러기 위해선 하나의 테스트가 끝나면 저장소나 공용 data를 깔끔하게 지워줘야 한다.
회원 리포지토리와 도메인을 활용해 실제 비즈니스 로직을 작성해보자. 먼저 service 패키지를 생성 후 그 아래에 MemberService 클래스를 작성한다.
MemberService.java
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
//회원 서비스를 만드려면 회원 리포지토리가 필요하다.
private final MemberRepository memberRepository;
//memberRepository를 외부에서 넣도록 함. (Dependency Injection)
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
//회원가입 (임의로 id를 반환하는 것으로 설정)
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> findMember() {
return memberRepository.findAll(); //findAll()의 리턴 타입:List
}
//아이디로 특정 회원 조회
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
작성했던 회원 서비스 클래스의 정상적인 동작 확인을 위해 테스트 케이스를 작성해보자. 회원 서비스 클래스에서 Ctrl+Shift+T를 누르면 test 아래에 자동으로 테스트를 생성할 수 있다.
참고로, 테스트를 작성할 땐 given(주어진 이 data를 기반으로), when(이 동작을 실행했을 때), then(이 결과가 나와야 함.)으로 나눠서 작성해보자. (나눠지지 않을 땐 스스로 적절히 변형해서 작성하자.)
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.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 static org.assertj.core.api.Assertions.assertThat;
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 중복_회원_예외() {
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//then
}
@Test
void findMember() {
}
@Test
void findOne() {
}
}