도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
클래스 의존관계
아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정
개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
회원 도메인과 리포지토리 만들기
회원 객체
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;
}
}
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
/* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려 */
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);
// 이렇게 하는 이유는 id를 시스템에서 정해주기 때문(클래스 변수)
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
// null이 반환될 가능성이 있다면 Optional로 감싸서 반환하면 클라이언트에서 이에 맞춰 처리해준다
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
// 찾아서 하나라도 있으면 반환해줌. 끝까지 돌렸는데 없으면 optional에 null이 포함되어 반환된다.
}
public void clearStore() {
store.clear();
}
}
회원 리포지토리 테스트 케이스 작성
메인 메서드를 실행허거나, 웹 application의 컨트롤러를 통해서 해당 기능을 실행하면 준비/실행 시간이 오래걸리고, 반복 실행하기 어렵고, 여러 테스트를 한 번에 실행하기 어렵다는 단점이 존재함
JUnit 프레임워크 사용
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 java.util.Optional;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
//given
Member member = new Member();
member.setName("spring");
//when
repository.save(member);
//then
Member result = repository.findById(member.getId()).get(); // Optional에서 값을 꺼냄(.get())
// Assertions.assertEquals(member, null);--> 기대했던 값과 실제 값이 달라 오류 발생
assertThat(result).isEqualTo(member);
}
@Test
public void findByName() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
// 위 세줄을 복붙하고, shift + f6 단축키를 이용하여 rename하자
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
Member result = repository.findByName("spring1").get();
//then
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
List<Member> result = repository.findAll();
//then
assertThat(result.size()).isEqualTo(2);
}
}
그냥 코드를 실행시키면 오류가 발생함
테스트의 순서는 보장이 안됨
순서에 의존적으로 설정하면 절대 안된다
이대로 전체 Test를 돌리면 findAll 실행 후 저장되어있던 spring1과 spring2가 findByName에서 이전에 저장했던 객체가 나오게 되는 것
afterEach를 통해 테스트가 한 번 끝날 때 저장소를 지우자
회원 서비스 개발
hello.hellospring에 service 패키지 생성 후 MemberService 클래스 작성
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.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) {
//find By name의 리턴타입이 optional이라서 아래처럼 사용 가능
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);
}
}
command + option + v : 반환 타입 + 변수 = 값 형태로 만들어줌
ex) memberRepository.findByName(member.getName()); 상태에서 저 단축키를 쓰면
Optional byName = memberRepository.findByName(member.getName()); 로 바꿔줌
서비스는 보통 비즈니스에 의존적으로 설계를 한다
리포지토리는 서비스보다 단순히 기계적으로 개발에 가까운 용어들을 선택한다.
회원 서비스 테스트
중복 회원 비지니스 로직이 제대로 작동하는지 테스트 케이스를 작성해보자
클래스를 만들고 커맨드 쉬프트 T --> Create New Test --> Junit5, 원하는 클래스 선택하면 껍데기를 만들어줌
커맨드 옵션 / --> 주석처리
테스트 함수는 한글로 만들어도 됨
given when then 문법
given : 뭔가가 주어짐(데이터)
when : 이거를 실행했을 때
then : 결과가 이게 나와야 해
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
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;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
// 따로 만드는 것보다 이렇게 만드는게 좋다
// 외부에서 메모리 멤버 리포지토리를 넣어주고 있는데, 이런 것을 Dependency Injection이라고 함
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
public void 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("hello");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
@Test
public void 중복_회원_예외() throws Exception {
//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("이미 존재하는 회원입니다.");
}
}
BeforeEach : 각 테스트 실행 전에 호출되어 테스트가 서로 영향받지 않도록 새로운 객체를 생성하고, 의존관계도 맺어준다
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
... }