회원 관리 예제 - 백엔드개발
필요한 5가지 단계에 대해 생각해보자
- 비즈니스 요구사항 정리
- 회원 도메인과 리포지토리 만들기
- 회원 리포지토리 테스트 케이스 작서
- 회원 서비스 개발
- 회워 서비스 테스트
이어서 회원 관리 예제를 진행할 것인데 비즈니스 요구사항부터 정리해 보자.
데이터 : 회원ID, 이름
기능 : 회원 등록, 조회
DB는 아직 미정
컨트롤러 : 웹 MVC의 컨트롤러 역할
서비스 : 이 영역에서 핵심 비즈니스 로직을 구현한다.
리포지토리 : DB에 접근, 도메인 객체를 DB에 저장하고 관리한다. (여기서 저장이 일어나는듯?)
도메인 : 비즈니스 도메인 객체들 Ex : 회원, 주문, 쿠폰,, DB에 저장되고 관리됨.
클래스 의존 관계는 다음과 같다.
[ MemberService ] ----> [ MemberRepository ] < - - - - - - [ MemoryMemberRepository ]
이 때 MemberRepository를 Interface로 설정하고 구현 클래스를 그때그때 바꿔줄 수 있게 설계한다.
실제 어떤 데이터베이스를 사용할지 선정되지 않은 상태에서 개발할때, 또는 초기 개발 단계에서 가벼운 메모리 기반의 데이터 저장소를 사용하고 이를 차근차근 키워 나갈때 도움이 될 것 같다.
이어서 회원 객체, 회원 리포지터리 인터페이스, 회원 리포지토리 메모리 구현체등을 작성해주자.
인터페이스에는 간단하게 사용될 함수의 선언만 되어있다.
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
이어서 이를 구현한 구현체에 세부 메소드를 구현해주자.
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());
}
public void clearStore(){
store.clear();
}
}
테스트 케이스 작성
개발 기능 테스트에는 자바의 Main 메서드 활용, 웹 컨트롤러 이용이 있는데 해당 방법들은 실행이 오래걸리고 반복 실행이 어렵다는 단점이 있다.
따라서 JUint이라는 프레임워크를 통해 테스트를 진행한다.
작성한 MemoryMemberRepository를 테스트하기 위해 src/test/java에 MemoryMemberRepositoryTest 클래스를 생성한다.
이때 Assertions기능을 활용해서 내가 보고싶은 결과가 사실인지를 확인한다.
Assertions은 Junit에서 제공하는 것, assertj에서 제공하는 두가지가 있으니까 import할때 확인해주자.
요즘은 후자가 더 편리하게 사용된다.
asserThat(member).isEqualTo(result);
또한 테스트클래스의 메소드는 실행 순서가 정해져 있지 않다.
이 과정에서 데이터가 서로 충돌될 수 있다.
따라서, 테스트가 실행되고 끝날때마다 데이터를 Clear해줘야한다.
MemoryMemberRepository에 Clearstore()라는 메소드를 만들어주고
@AfterEach
public void afterEach(){
repository.clearStore();
}
@AfterEach를 사용해서 테스트가 끝날때마다 데이터를 비워준다.
테스트는 각각 독립적으로 실행되어야한다. 테스트 순서에 따라 의존관계가 있는 테스트는 좋은 테스트가 아니다.
회원 서비스 개발
주요 비즈니스 로직인 회원 가입, 멤버 조회를 진행해보자.
회원가입을 진행할때 중복된 회원이름이 존재하면 안된다는 조건이 있다고 하자.
그러면 Member의 Name이 존재할때 '이미 존재하는 회원이다.'라는 걸 반환한다고하면 다음과 같다.
public Long join(Member member){
//같은 이름의 중복회원
memberRepository.findByName(member.getName())
.ifPresent(m->{
throw new IllegalStateException( "이미 존재하는 회원입니다.");
});
memberRepository.save(member);
return member.getId();
}
이렇게 코드를 Lambda를 통해 존재하면 Exception을 발생한다고 하면 된다. 해당 메소드가 길어져서 따로 빼는게 좋으므로 해당 부분을 [Ctrl + T]로 따로 메소드를 빼주고 이름을 정해줘서 다음과 같이 만들자.
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( "이미 존재하는 회원입니다.");
});
}
회원 서비스 테스트
Tip
- 클래스에서 Ctrl + Shift + T를 통해 같은 패키지아래에 똑같은 클래스 틀을 만들어준다.
- 테스트는 한글로 적어도 상관이 없다
- Build시 TestCode는 반영되지 않는다.
테스트 문법
//given
무언가 주어졌을때
//when
이걸 실행했을때
//then
이런 결과가 나와야한다.
주석을 보고 주어진 데이터가 무엇인지 원하는 결과가 무엇인지 어떤 실행을 했을때인지 알 수 있기에 해당 주석을 활용하는 것을 추천한다.
서비스 테스트에서 주어진 기능을 제대로 수행하는지 확인하는지도 중요하지만 그보다 더 중요한게 예외 상황에 대한 처리이다. 이 서비스에선 '중복회원을 방지한다.'라는 조건이 있었기에 해당 기능이 잘 작동하는지 확인하는 테스트가 필요하다.
public void 중복_회원_예외(){
//Given
Member member1 = new Member();
member1.setName("spring1");
Member member2 = new Member();
member2.setName("spring1");
//when
memberService.join(member1);
IllegalStateException e = public void 중복_회원_예외(){
//Given
Member member1 = new Member();
member1.setName("spring1");
Member member2 = new Member();
member2.setName("spring1");
//when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, ()->memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//
// try{
// memberService.join(member2);
// fail();
// }catch(IllegalStateException e){
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.1");
// }
//Then
}
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//
// try{
// memberService.join(member2);
// fail();
// }catch(IllegalStateException e){
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.1");
// }
//Then
}
아래 주석처리된 Try - catch 구문을 통해 IllegalStateException 에러가 발생했을때 발생하는 message가 같은지를 확인할 수 있는데 이 경우에는 해당 메시지가 다르면 예외가 발생해도 처리가 제대로 안된다는 단점이 있다.
가령 이런 상황인 것이다.
Member1 : spring이 있을때 Member2 : spring이 들어올때
IllegalStateException 에러가 발생 --> 이에 대한 안내사항으로 '이미 존재하는 회원입니다.'라는 문구가 발생한다.
결국 에러가 발생하는게 핵심인데 에러는 발생했으나 확인하는 메시지가 다르다는 이유로 에러가 안 잡히거나 테스트가 실패할 수도 있다.
따라서, assertThrows를 통해 다음 람다 메소드가 실행되었을때 IllegalStateException이 발생하는지를 확인할 수 있다.
IllegalStateException e = assertThrows(IllegalStateException.class, ()->memberService.join(member2));
만약 메시지까지 확인하고 싶으면 이어서 아래 코드를 작성하면 된다.
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
MemberService의 Repository랑 Test의케이스에서의 MemberRepository가 서로 다른 인스턴스이면 안된다.
왜..? 한 테스트를 하는데 2개의 인스턴스를 쓰는게 이상하지 않아?
같은 인스턴스를 쓰게 바꿔주자.
MemberService에서 직접 넣어주는 것이 아닌 Constructor를 통해 외부에서 넣어주도록 한다.
private final MemberRepository memberRepository;// = new MemberRepository();가 아님!
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
이어서 테스트에서도 직접 생성하는 코드를 지워주고,
MemberService memberService ; // = new MemberService();
MemoryMemberRepository memberRepository ; // = new MemoryMemberRepository();
@BeforeEach 구문을 통해 테스트가 실행할때마다 MemoryMemberRepository를 넣어주고 그를 바탕으로 서비스를 만들기 때문에 같은 리포지토리를 사용하게 된다.
@BeforeEach
public void beforeEach(){
memberRepository= new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
//이러면 같은 repository를 사용할 수 있음.
}
'BE > Spring - Inflearn 김영한' 카테고리의 다른 글
스프링 핵심 원리 - 기본편 1 (0) | 2023.03.16 |
---|---|
Spring 입문 인프런(무료강의) 김영한5 - DB접근 기술 (0) | 2023.03.13 |
Spring 입문 인프런(무료강의) 김영한4 (0) | 2023.03.10 |
Spring 입문 인프런(무료강의) 김영한3 (0) | 2023.03.10 |
Spring 입문 - 인프런(무료강의) (0) | 2023.01.10 |