- 비즈니스 요구사항 정리
- 회원 도메인과 리포지토리 만들기
- 회원 리포지토리 테스트 케이스 작성
- 회원 서비스 개발
- 회원 서비스 테스트
비즈니스 요구사항 정리
- 데이터 : 회원 ID, 이름
- 기능 : 회원 등록, 조회
- 데이터 저장소가 선정되지 않음.
일반적인 웹 애플리케이션 계층 구조
- 컨트롤러 : 웹 MVC의 컨트롤러 역할
- 서비스 : 핵심 비즈니스 로직 구현
- 리포지토리 : 데이터베이스 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인 : 비즈니스 도메인 객체
ex) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
클래스 의존관계
- 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
- 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민 중인 상황으로 가정
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
- 향후에 RDB, JPA 등 DB접근 방식을 바꾸기 위해 인터페이스가 필요.
회원 도메인과 리포지토리 만들기
domain 패키지 생성 -> Member 클래스 생성
repository 패키지 생성 -> MemberRepositry 인터페이스 생성
Optional은 객체가 Null일 때 반환하는 방법 중 하나.
구현체 생성 -> repository 패키지에 MemoryMemberRepository 클래스 생성
실무에서는 store처럼 공유되는 변수일 때 concurrenthashmap을 써야 하는데 예제니까 패스.
sequence는 0, 1, 2 ... key 값을 생성하는 변수.
공유되는 변수일 때 동시성 문제를 고려하여 atomicLong을 써야 하는데 패스.
member를 save 할 때 sequence값을 하나 올려 주고, store에 저장.
store에서 id를 꺼내면 되는데 null이 반환될 가능성이 있다.
Optional.ofNullable()로 감싸서 반환하면 Client에서 추가 동작을 할 수 있다.
store에서 parameter에서 넘어온 name과 같으면 filter가 되고 findAny()는 하나라도 찾는 것.
찾으면 Optional로 반환 -> Map에서 하나라도 찾으면 반환,
끝까지 탐색해서 없으면 Optional로 name을 포장해서 null이 포함되어 반환.
회원 리포지토리 테스트 케이스 작성
개발한 기능을 실행해서 테스트할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한 번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
test 폴더에 respository 패키지 생성
respository 패키지에서 테스트하려는 클래스 이름 + "Test" 클래스 생성 -> 관례
테스트하려는 메서드 작성하고 @test import 하여 작성.
save() 메서드 테스트
방법 1. 단순 비교
member에 name을 spring으로 set.
repository에 member를 저장.
result에 respository에서 member의 id와 같은 것을 꺼낸다.
member 객체와 db인 repository에서 꺼낸 것과 같으면 true.
방법 2. org.junit.jupiter.api의 Assertions
단순하게 result==member를 출력하여 확인할 수 있다.
그런데 글자로 계속 볼 수 없으니 다른 방법을 사용.
org.junit.jupiter.api의 Assertions를 사용
Assertions.assertEquals(expected, actual)
expected와 actual이 같은지 확인.
방법 3. Assertions org.assertj.core.api
요즘에 많이 사용하는 것은 Assertions org.assertj.core.api
Assertions.assertThat(member).isEqualTo(result);
문장 그대로 member가 result와 같으면 테스트 pass
Assertions에서 alt + enter를 눌러 org.assertj.core.api.Assertions.* 를 import 할 수 있다.
assertThat을 바로 사용 가능.
실무에서 build tool과 엮어서 테스트 케이스를 통과하지 못하면 다음 단계로 넘어가지 못한다.
findByName() 메서드
복사하여 변수명을 고쳐야 할 때 shift + F6을 누르면 rename이 된다.
save() 메서드에서 했던 것과 마찬가지로 respository에서 spring1을 get 하여 test.
findAll()
전체 테스트
클래스 전체를 실행하면
어떤 메서드는 pass 하고 어떤 메서드는 fail 한다.
메서드 실행 순서를 보면 가장 아래에 작성했던 findAll()이 실행됐다.
메서드를 테스트할 때 순서가 보장이 안된다.
모든 테스트는 순서와 상관없이 메서드가 따로 동작하게 설계해야 한다.
findAll()에서 다른 객체가 이미 저장되었기 때문에 다른 메서드가 fail 한다.
그래서 테스트가 하나 끝나면 data를 clear 해야 해줘야 한다.
테스트 대상 클래스인 MemoryMemberRepository에 위 코드를 추가한다.
대상 클래스에 코드를 추가하고 테스트가 끝날 때마다 repository를 비워주는 코드를
MemoryMemberRepositoryTest 클래스에서 작성한다.
@AfterEach는 메서드가 끝날 때마다 동작하는 annotation.
테스트는 서로 순서와 관계없이 의존 관계없이 실행되어야 한다.
하나의 테스트가 끝날 때마다 data를 지워줘야 한다.
회원 서비스 개발
회원 repository와 domain을 활용하여 비즈니스 로직을 개발.
서비스 클래스는 기획자든 개발자든 한눈에 알아볼 수 있는 용어를 사용한다.
서비스는 비즈니스에 의존적으로 설계하고 리포지토리는 기계적으로 개발스러운 용어를 사용한다.
회원 가입 구현
hello.hellospring 폴더에서 service 패키지 생성 -> MemberService 클래스 생성
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/**
* 회원 가입
*/
public Long join(Member member) {
// 같은 이름이 있는 중복 회원X
Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(m->{
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
memberRepository.save(member);
return member.getId();
}
}
memberRepository.findByName(member.getName()); 을 return 할 변수를 만들 때
관련된 메서드 findByName에 커서를 위치하고 ctrl + alt + v -> findByName의 return 형 자동완성
result에 만약 값이 있으면 exception 발생시키기.
result가 Optional로 한번 감싸져 있다. (Optional<member>)
과거에는 if( ~ == null) 이런 식으로 코딩했다.
현재는 Optional과 같은 것을 감싸서 표현하면
ifpresent와 같은 메서드를 사용할 수 있다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/**
* 회원 가입
*/
public Long join(Member member) {
// 같은 이름이 있는 중복 회원X
memberRepository.findByName(member.getName())
.ifPresent(m->{
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
memberRepository.save(member);
return member.getId();
}
}
Optional<member> result -> Optional을 바로 반환하는 것은 좋은 방법이 아니다.
때문에 바로 ifpresent( ~~); 식으로 작성할 수 있다.
그런데 보기에 깔끔하지도 않고 코드가 길기 때문에 메서드로 구성하는 것이 좋다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/**
* 회원 가입
*/
public Long join(Member member) {
// 같은 이름이 있는 중복 회원X
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m->{
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
}
ctr + alt + m 단축키를 입력하여 길어진 코드를 메서드로 추출할 수 있다.
전체 회원 조회
// 전체 회원 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
// 회원 ID로 조회
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
회원 서비스 테스트
회원 서비스가 제대로 작동하는지 테스트를 해봐야 한다.
테스트할 MemberService 클래스에서 ctrl + shift + T를 누르면
테스트 클래스를 편하게 생성할 수 있다.
test 폴더에서 service라는 패키지가 생성되어 그곳에 테스트 클래스가 생성된다.
테스트는 한글로 바꿔서 진행해도 된다.
테스트 코드를 제외한 실제 동작하는 코드는 한글로 적지 않는 것이 좋지만
테스트는 한글로 많이 적기도 한다.
테스트 코드는 빌드될 때 실제 코드에 포함되지 않는다.
테스트는 given(무언가 주어져서) when(이것을 실행했을 때) then(결과가 이래야 한다)으로 주석을 표시하여 진행하면
나중에 다시 볼 때 어떤 상황인지 인지하기 편하다.
처음엔 이런 식으로 주석을 달아서 진행하되 상황에 따라 맞춰가는 것이 좋다.
회원가입 테스트
테스트는 정상 flow도 중요하지만 예외 flow도 매우 중요하다.
join()의 핵심은 저장도 있지만, 중복 회원 검증 예외가 제대로 작동하는 것이 중요하다.
member1과 member2가 같은 이름을 가지고 있어서
두 멤버를 회원 가입했을 때 예외가 제대로 작동하는지 확인해야 한다.
try - catch문을 사용하여 member2가 회원 가입했을 때
join 메서드에서 발생하는 에러 메시지가 제대로 작동하는지 확인하기 위해
e 객체의 메시지를 비교하여 테스트한다.
이 부분 때문에 try-catch를 넣는 것이 애매하다고 생각할 수 있다.
그래서 아래와 같이 테스트할 수 있다.
그런데 회원가입 테스트에서 member의 이름을 spring으로 했을 때
테스트가 실패할 것이다.
회원 가입에 대한 spring이 DB에 먼저 저장되어 있을 수도 있고 아닐 수도 있다.
메서드 순서에 상관없이 테스트되어야 하니까 clear를 해야 한다.
MemoryMemberRepository를 가져와서 clear 해야 한다.
MemberService에 있는 MemoryMemberRepository와
테스트에서 사용하는 MemoryMemberRepository가 다른 인스턴스이다.
즉, 다른 Repository로 테스트를 하고 있다.
그런데 같은 인스턴스를 쓰도록 하려면 MemberService에서 new 하여 인스턴스를 생성하지 않고
생성자를 이용한다. memberRepository를 외부에서 넣어주도록 한다.
외부에서 넣어주는 방법
MemberServiceTest 클래스에서 MemberService에 MemoryMemberRepostiory를 넣어준다.
동작하기 전에 넣어줘야 하므로
@BeforeEach 사용.
: 각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고,
의존관계도 새로 맺어준다.(Dependency Injection)