▼ What ?
이번엔 회원 관리 예제를 만들어보면서 백엔드 개발 과정을 대략적으로 살펴보고 공부해보려고 한다
▼ 비즈니스 요구사항 정리
- 데이터 : 회원ID, 이름
- 기능 : 회원 등록, 조회
- 가상의 시나리오 - 아직 데이터 저장소(DB)가 선정되지 않았다
ex) 성능이 중요한 DB로 할지? 일반적인 RDB로 할지? 아니면 NoSQL?
일반적인 웹 애플리케이션 계층 구조

- 컨트롤러 : 웹 MVC의 컨트롤러 역할
- 서비스 : 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직를 구현한 객체
ex) 회원은 중복 가입 불가 - 리포지토리(Repository) : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인(Domain) : 비즈니스 도메인 객체
ex) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저정하고 관리된
클래스 의존관계

- 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
- 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가성
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
▼ 회원 도메인과 리포지토리 만들기

1. 회원 객체
- getter & setter 단축키 활용하기
package hello.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;
}
}
2. 회원 리포지토리 인터페이스
- 반환할 객체를 Optional 객체로 감싸서 반환하는 이유?
➜ 반환할 값이 null 인지 확인하기 위해서 !
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 class (참고) [JAVA] 람다 (Lamda) & 스트림 (Stream) — Uykm_Note (tistory.com)
[JAVA] 람다 (Lamda) & 스트림 (Stream)
🌑 람다식 (Lamda expression) ✔️ 람다식 ? 🔹 람다식(Lamda expression) : 메서드를 하나의 '식(expression)'으로 표현한 것 class, 메서드, 객체 필요 X ➜ 람다식 자체만으로 메서드의 역할을 수행 ➜ 즉, 메
ukym-tistory.tistory.com
3. 회원 리포지토리 메모리 구현체
- 인터페이스 import 단축키

- 사실 실무에선 동시성의 문제가 있어서 공유되는 변수일 때는 HashMap 대신 ConcurrentHashMap을, Long 대신 AtomicLong을 사용해야 한다
- 실무에선 루프를 돌리기 편한 List를 많이 쓴다
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<>(); // ConcurrentHashMap
private static long sequence = 0L; // AtomicLong
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member); // store에 저장
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)) // 파라미터로 넘어온 name과 같은 것만 필터링
.findAny(); // Map('store')에서 하나 찾아지면 바로 반환,
// 없으면 비어진 Optional 객체 반환
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
+ 람다식 (참고) [JAVA] 람다 (Lamda) & 스트림 (Stream) — Uykm_Note (tistory.com)
[JAVA] 람다 (Lamda) & 스트림 (Stream)
🌑 람다식 (Lamda expression) ✔️ 람다식 ? 🔹 람다식(Lamda expression) : 메서드를 하나의 '식(expression)'으로 표현한 것 class, 메서드, 객체 필요 X ➜ 람다식 자체만으로 메서드의 역할을 수행 ➜ 즉, 메
ukym-tistory.tistory.com
4. 회원 리포지토리 테스트 케이스 작성
- 개발한 기능을 실행해서 테스트할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다
➜ 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다
➜ 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
회원 리포지토리 메모리 구현체 테스트
- src/test/java 하위 폴더(hello.hellospring)에 테스트 패키지 생성
- 테스트해야 할 메서드
➜ 'save()', 'findByName()', 'findAll()' - 테스트가 성공했는지를 'System.out.println("result" = " + (result == member));' 처럼 매번 문자로 확인할 수는 없기 때문에 사용하는 기능이 'Assertions.assertEquals(membe r, result);'이다
➜ 요즘엔 'Assertions.assertThat(member).isEqualTo(result)'을 더 많이 쓴다
( ' ⌥ + Enter (Mac) ' or ' Alt + Enter (Win/Linux) ' 로 Assertions를 static으로 import 해주면 'assertThat()'를 단독으로 사용 가능 )
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
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("sprint");
repository.save(member);
// get() : Optional에서 값을 바로 꺼내는 용도로 사용하지만, 테스트 코드 말고는 사용 권장 X
Member result = repository.findById(member.getId()).get();
// System.out.println("result" = " + (result == member));
// Assertions.assertEquals(member, result); // OK
// Assertions.assertEquals(member, null); // Error
// Assertions.assertThat(member).isEqualTo(result);
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(member2); // Error
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);
}
}
- 테스트를 메서드 하나씩 해볼 수도 있고,
클래스 레벨에 있는 메서드들을 동시에 할 수도 있고,
패키지를 실행시켜서 전체 클래스들을 동시에 실행시킬 수도 있다



- 모든 테스트는 아래처럼 실행 순서가 보장되지 않기 때문에, 순서에 의존적으로 테스트 케이스를 설계하면 안된다 !

- 테스트가 끝날 때마다 메모리 DB에서 해당 테스트에 대한 데이터를 지워줘야 한다(clear) !
➜ 테스트를 해볼 클래스(리포지토리)에 'store.clear()'를 실행할 메서드('clearStore()')를 하나 생성해줘야 한다
➜ ' @AfterEach ' : 각 테스트가 종료될 때마다 이 기능을 실행
: 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다.
이렇게 되면 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다.
여기서는 '@AfterEach'가 붙은 메서드는 메모리 DB에 저장된 데이터를 삭제하는 기능을 한다.
public class MemoryMemberRepository implements MemberRepository {
...
public void clearStore() {
store.clear();
}
}
class MemoryMemberRepositoryTest {
...
// 각 테스트가 종료될 때마다 이 기능을 실행
@AfterEach
public void afterEach() {
repository.clearStore();
}
...
}
- 테스트 주도 개발 (TDD)
➜ 이 예제의 방식과 반대로 테스트를 먼저 만들고 구현 클래스를 만드는 방식을 'TDD'라고 한다 !
5. 회원 서비스 개발
- 회원 리포지토리와 도메인을 활용하여 비즈니스 로직을 작성하는 것
- 회원 서비스(MemberService)를 개발하기 위해선 회원 리포지토리(MemberRepository)가 필요 !
➜ private final MemberRepository memberRepository = new MemoryMemberRepository(); - 앞서 미리 설정한 비즈니스 로직
➜ 같은 이름의 중복 회원은 허용 X - 'Member member1 = result.get();'
➜ 'get()'으로 바로 꺼내도 되지만, 그냥 바로 값을 꺼내고 싶은 경우 말고는 권장 X
'Member member1 = result.orElseGet();'
➜ 'orElseGet()'
: 값이 없을 경우에 어떤 메서드를 실행하거나 default값을 넣어서 꺼내도록 할 수 있다 ( 자주 사용 ) - 객체에 저장된 값을 가져올 때엔 Null이 아닌지 확인하고 가져오자 !
➜ 'memberRepository.findByName(member.getName());'
변수를 추출하는 단축키
➜ Optional<member> result = memberRepository.findByName(member.getName());
[ ⌥ + ⌘ + V (Mac), Ctrl + Alt + V (Win/Linux) ]
( Optional로 객체를 감싸면, 'ifPresent()' 같은 메서드들 사용 가능 ) - 아래 코드를 메서드('validateDuplicateMember')로 따로 빼주자
➜ Extract Method [ Ctrl + T (Max), Ctrl + Alt + Shift + T (Win/Linux) ]
Optional<member> result = memberRepository.findByName(member.getName());
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
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 = new MemoryMemberRepository();
/**
* 회원 가입
*/
public Long join(Member member) {
// 같은 이름이 있는 중복 회원 X
// Extract Method
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
// Service는 비즈니스를 처리하는 역할이기 때문에 비즈니스에 의존적이고, 따라서 비즈니스적인 네이밍을 하자
// Repository는 기계적으로 네이밍을 한다
/**
* 전체 회원 조회
**/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
- Service는 비즈니스 로직을 구현하는 역할이기 때문에 비즈니스에 의존적이고, 따라서 비즈니스적인 네이밍을 하는 편
- Repository는 'memberRepository' 같이 기계적으로 네이밍을 하는 편
- 이렇게 구현한 Sevice도 Repository처럼 제대로 작동하는지 테스트해봐야 한다 !
회원 서비스 테스트
- 테스트 클래스 생성 단축키
[ Shift + ⌘ + T (Mac), Ctrl + Shift + T (Win/Linux) ]

package hello.hellospring.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
@Test
void join() {
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
- given(어떤 상황이 주어졌는데) - when(이것을 실행했을 때) - then(이러한 결과가 나와야 한다) 구조로 테스트 메서드를 작성해보자 !
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
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.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService = new MemberService();
@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
//when
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
- 중복 회원이 가입되었을 때('memberService.join(member2);'), 'validateDuplicateMember()'에 걸려서 예외('IllegalStateException')가 발생했는지 테스트('중복_회원_예외()')해야 한다
➜ 'try - catch' 문으로 예외를 잡아도 되지만, 이것 때문에 'try -catch' 문을 추가하는 것이 애매하다..
➜ 'assertThrows()'로 예외를 잡을 수 있다
예외 메시지는 어떻게 검증해야 할까?
➜ 'assertThat()', 'getMessage()', 'isEqualTo()'을 활용하자
public class MemberService {
...
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
...
}
@Test
public void 중복_회원_예외() {
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
// memberService.join(member2); // 중복 회원 가입
// '() -> memberService.join(member2)'가 수행되면 IllegalStateException가 발생해야 한다
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
/*
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
*/
//then
}
- 이번 테스트도 테스트가 끝날 때마다 메모리 DB에서 해당 테스트에 대한 데이터를 지워주자
➜ @AfterEach
class MemberServiceTest {
// 또다른 인스턴스가 생성된다
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
...
}
- 기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하게 했다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
- 하지만, 위의 코드처럼 작성한다면 테스트할 때 또다른 인스턴스가 더 생성되는 문제가 있다 !
➜ 같은 리포지토리가 아니라 다른 리포지토리를 이용해서 테스트하게 된다 - 회원 리포지토리의 코드가 회원 서비스 코드를 "의존관계 주입(DI)" 가능하게 변경해보자
➜ 테스트할 때도 같은 인스턴스를 사용하게 하려면, 객체를 생성하는 것이 아니라 아래처럼 외부에서 인스턴스를 받아오도록 바꿔준다
public class MemberService {
// private final MemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository;
// 외부에서 인스턴스를 받아온다
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
class MemberServiceTest {
/* 같은 인스턴스, 즉 같은 리포지토리를 이용하여 테스트하는 것이 아닌 다른 리포지토리를 이용하여 테스트하게 된다
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
*/
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository); // 의존관계 주입 (DI)
}
...
}
▼ What ?
이번엔 회원 관리 예제를 만들어보면서 백엔드 개발 과정을 대략적으로 살펴보고 공부해보려고 한다
▼ 비즈니스 요구사항 정리
- 데이터 : 회원ID, 이름
- 기능 : 회원 등록, 조회
- 가상의 시나리오 - 아직 데이터 저장소(DB)가 선정되지 않았다
ex) 성능이 중요한 DB로 할지? 일반적인 RDB로 할지? 아니면 NoSQL?
일반적인 웹 애플리케이션 계층 구조

- 컨트롤러 : 웹 MVC의 컨트롤러 역할
- 서비스 : 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직를 구현한 객체
ex) 회원은 중복 가입 불가 - 리포지토리(Repository) : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인(Domain) : 비즈니스 도메인 객체
ex) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저정하고 관리된
클래스 의존관계

- 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
- 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가성
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
▼ 회원 도메인과 리포지토리 만들기

1. 회원 객체
- getter & setter 단축키 활용하기
package hello.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;
}
}
2. 회원 리포지토리 인터페이스
- 반환할 객체를 Optional 객체로 감싸서 반환하는 이유?
➜ 반환할 값이 null 인지 확인하기 위해서 !
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 class (참고) [JAVA] 람다 (Lamda) & 스트림 (Stream) — Uykm_Note (tistory.com)
[JAVA] 람다 (Lamda) & 스트림 (Stream)
🌑 람다식 (Lamda expression) ✔️ 람다식 ? 🔹 람다식(Lamda expression) : 메서드를 하나의 '식(expression)'으로 표현한 것 class, 메서드, 객체 필요 X ➜ 람다식 자체만으로 메서드의 역할을 수행 ➜ 즉, 메
ukym-tistory.tistory.com
3. 회원 리포지토리 메모리 구현체
- 인터페이스 import 단축키

- 사실 실무에선 동시성의 문제가 있어서 공유되는 변수일 때는 HashMap 대신 ConcurrentHashMap을, Long 대신 AtomicLong을 사용해야 한다
- 실무에선 루프를 돌리기 편한 List를 많이 쓴다
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<>(); // ConcurrentHashMap
private static long sequence = 0L; // AtomicLong
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member); // store에 저장
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)) // 파라미터로 넘어온 name과 같은 것만 필터링
.findAny(); // Map('store')에서 하나 찾아지면 바로 반환,
// 없으면 비어진 Optional 객체 반환
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
+ 람다식 (참고) [JAVA] 람다 (Lamda) & 스트림 (Stream) — Uykm_Note (tistory.com)
[JAVA] 람다 (Lamda) & 스트림 (Stream)
🌑 람다식 (Lamda expression) ✔️ 람다식 ? 🔹 람다식(Lamda expression) : 메서드를 하나의 '식(expression)'으로 표현한 것 class, 메서드, 객체 필요 X ➜ 람다식 자체만으로 메서드의 역할을 수행 ➜ 즉, 메
ukym-tistory.tistory.com
4. 회원 리포지토리 테스트 케이스 작성
- 개발한 기능을 실행해서 테스트할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다
➜ 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다
➜ 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
회원 리포지토리 메모리 구현체 테스트
- src/test/java 하위 폴더(hello.hellospring)에 테스트 패키지 생성
- 테스트해야 할 메서드
➜ 'save()', 'findByName()', 'findAll()' - 테스트가 성공했는지를 'System.out.println("result" = " + (result == member));' 처럼 매번 문자로 확인할 수는 없기 때문에 사용하는 기능이 'Assertions.assertEquals(membe r, result);'이다
➜ 요즘엔 'Assertions.assertThat(member).isEqualTo(result)'을 더 많이 쓴다
( ' ⌥ + Enter (Mac) ' or ' Alt + Enter (Win/Linux) ' 로 Assertions를 static으로 import 해주면 'assertThat()'를 단독으로 사용 가능 )
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
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("sprint");
repository.save(member);
// get() : Optional에서 값을 바로 꺼내는 용도로 사용하지만, 테스트 코드 말고는 사용 권장 X
Member result = repository.findById(member.getId()).get();
// System.out.println("result" = " + (result == member));
// Assertions.assertEquals(member, result); // OK
// Assertions.assertEquals(member, null); // Error
// Assertions.assertThat(member).isEqualTo(result);
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(member2); // Error
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);
}
}
- 테스트를 메서드 하나씩 해볼 수도 있고,
클래스 레벨에 있는 메서드들을 동시에 할 수도 있고,
패키지를 실행시켜서 전체 클래스들을 동시에 실행시킬 수도 있다



- 모든 테스트는 아래처럼 실행 순서가 보장되지 않기 때문에, 순서에 의존적으로 테스트 케이스를 설계하면 안된다 !

- 테스트가 끝날 때마다 메모리 DB에서 해당 테스트에 대한 데이터를 지워줘야 한다(clear) !
➜ 테스트를 해볼 클래스(리포지토리)에 'store.clear()'를 실행할 메서드('clearStore()')를 하나 생성해줘야 한다
➜ ' @AfterEach ' : 각 테스트가 종료될 때마다 이 기능을 실행
: 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다.
이렇게 되면 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다.
여기서는 '@AfterEach'가 붙은 메서드는 메모리 DB에 저장된 데이터를 삭제하는 기능을 한다.
public class MemoryMemberRepository implements MemberRepository {
...
public void clearStore() {
store.clear();
}
}
class MemoryMemberRepositoryTest {
...
// 각 테스트가 종료될 때마다 이 기능을 실행
@AfterEach
public void afterEach() {
repository.clearStore();
}
...
}
- 테스트 주도 개발 (TDD)
➜ 이 예제의 방식과 반대로 테스트를 먼저 만들고 구현 클래스를 만드는 방식을 'TDD'라고 한다 !
5. 회원 서비스 개발
- 회원 리포지토리와 도메인을 활용하여 비즈니스 로직을 작성하는 것
- 회원 서비스(MemberService)를 개발하기 위해선 회원 리포지토리(MemberRepository)가 필요 !
➜ private final MemberRepository memberRepository = new MemoryMemberRepository(); - 앞서 미리 설정한 비즈니스 로직
➜ 같은 이름의 중복 회원은 허용 X - 'Member member1 = result.get();'
➜ 'get()'으로 바로 꺼내도 되지만, 그냥 바로 값을 꺼내고 싶은 경우 말고는 권장 X
'Member member1 = result.orElseGet();'
➜ 'orElseGet()'
: 값이 없을 경우에 어떤 메서드를 실행하거나 default값을 넣어서 꺼내도록 할 수 있다 ( 자주 사용 ) - 객체에 저장된 값을 가져올 때엔 Null이 아닌지 확인하고 가져오자 !
➜ 'memberRepository.findByName(member.getName());'
변수를 추출하는 단축키
➜ Optional<member> result = memberRepository.findByName(member.getName());
[ ⌥ + ⌘ + V (Mac), Ctrl + Alt + V (Win/Linux) ]
( Optional로 객체를 감싸면, 'ifPresent()' 같은 메서드들 사용 가능 ) - 아래 코드를 메서드('validateDuplicateMember')로 따로 빼주자
➜ Extract Method [ Ctrl + T (Max), Ctrl + Alt + Shift + T (Win/Linux) ]
Optional<member> result = memberRepository.findByName(member.getName());
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
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 = new MemoryMemberRepository();
/**
* 회원 가입
*/
public Long join(Member member) {
// 같은 이름이 있는 중복 회원 X
// Extract Method
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
// Service는 비즈니스를 처리하는 역할이기 때문에 비즈니스에 의존적이고, 따라서 비즈니스적인 네이밍을 하자
// Repository는 기계적으로 네이밍을 한다
/**
* 전체 회원 조회
**/
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
- Service는 비즈니스 로직을 구현하는 역할이기 때문에 비즈니스에 의존적이고, 따라서 비즈니스적인 네이밍을 하는 편
- Repository는 'memberRepository' 같이 기계적으로 네이밍을 하는 편
- 이렇게 구현한 Sevice도 Repository처럼 제대로 작동하는지 테스트해봐야 한다 !
회원 서비스 테스트
- 테스트 클래스 생성 단축키
[ Shift + ⌘ + T (Mac), Ctrl + Shift + T (Win/Linux) ]

package hello.hellospring.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
@Test
void join() {
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
- given(어떤 상황이 주어졌는데) - when(이것을 실행했을 때) - then(이러한 결과가 나와야 한다) 구조로 테스트 메서드를 작성해보자 !
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
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.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService = new MemberService();
@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
//when
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
- 중복 회원이 가입되었을 때('memberService.join(member2);'), 'validateDuplicateMember()'에 걸려서 예외('IllegalStateException')가 발생했는지 테스트('중복_회원_예외()')해야 한다
➜ 'try - catch' 문으로 예외를 잡아도 되지만, 이것 때문에 'try -catch' 문을 추가하는 것이 애매하다..
➜ 'assertThrows()'로 예외를 잡을 수 있다
예외 메시지는 어떻게 검증해야 할까?
➜ 'assertThat()', 'getMessage()', 'isEqualTo()'을 활용하자
public class MemberService {
...
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
...
}
@Test
public void 중복_회원_예외() {
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
// memberService.join(member2); // 중복 회원 가입
// '() -> memberService.join(member2)'가 수행되면 IllegalStateException가 발생해야 한다
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
/*
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
*/
//then
}
- 이번 테스트도 테스트가 끝날 때마다 메모리 DB에서 해당 테스트에 대한 데이터를 지워주자
➜ @AfterEach
class MemberServiceTest {
// 또다른 인스턴스가 생성된다
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
...
}
- 기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하게 했다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
- 하지만, 위의 코드처럼 작성한다면 테스트할 때 또다른 인스턴스가 더 생성되는 문제가 있다 !
➜ 같은 리포지토리가 아니라 다른 리포지토리를 이용해서 테스트하게 된다 - 회원 리포지토리의 코드가 회원 서비스 코드를 "의존관계 주입(DI)" 가능하게 변경해보자
➜ 테스트할 때도 같은 인스턴스를 사용하게 하려면, 객체를 생성하는 것이 아니라 아래처럼 외부에서 인스턴스를 받아오도록 바꿔준다
public class MemberService {
// private final MemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository;
// 외부에서 인스턴스를 받아온다
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
class MemberServiceTest {
/* 같은 인스턴스, 즉 같은 리포지토리를 이용하여 테스트하는 것이 아닌 다른 리포지토리를 이용하여 테스트하게 된다
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
*/
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository); // 의존관계 주입 (DI)
}
...
}