스프링 입문 - 회원 관리 예제
1. 비즈니스 요구사항 정리
- 데이터는 회원의 id / 이름
- 기능은 회원 등록 및 조회
- 데이터 저장소 db가 선정되지 않았을 경우를 가정. (성능이 중요한 db로 할지, 일반적인 관계형 데이터베이스로 할지, NoSQL로 할지 ,,)
- 일반적인 웹 애플리케이션의 계층 구조
- 컨트롤러 Controller : 웹 MVC 에서의 컨트롤러 역할
- 서비스 Services : 도메인을 가지고 핵심 비즈니스 로직 구현 ( ex. 회원은 중복 가입이 안된다... 등등)
- 리포지토리 Repository : DB에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인 Domain : 비즈니스 도메인 객체 (ex. 회원, 주문, 쿠폰 등등 db 에 저장되고 관리됨. )
- 데이터 저장소가 없으므로, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계. (나중에 정해지면 대체 가능하므로)
- 개발 초기이므로 메모리 기반의 데이터 저장소를 임시로 사용.
2. 회원 도메인과 리포지토리 만들기
* 일반적인 자바 생성자 (ex. public User(String name, int age, String hobby) 말고,
Getter & Setter 를 이용하는 방법 ( getUser / setUser ) 의 구조를 알고 있어야함.
* 생성자 / Getter Setter 등등 맥북에서의 생성 단축키는 Command + N 이다.
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;
}
- domain / repository 패키지를 생성. ( repository는 회원 객체를 저장하는 저장소)
- Repository 내에, MemberRepository 라는 인터페이스 생성
* optional ~~ 도 알아야 한다.
가져오려는 데이터가 Null 일때, 처리하는 방법 중 하나인데 최근 선호되는 방법이다. (Null을 그대로 반환하는 방법이 아닌, optional 이라는 걸로 감싸는 방법. Java 8에 들어가있는 방법.)
Member save(Member member); //회원이 저장소에 저장됨
Optional<Member> findById(Long id); //id로 회원 조회
Optional<Member> findByName(String name); //이름으로 회원 조회
List<Member> findAll(); //저장된 모든 회원 리스트를 반환
- repository 패키지 내에, MemoryMemberRepository라는 클래스를 구현한다.
- 생성자 옆에 implements 입력후 맥북기준 option + Enter키 누르면 메서드 자동생성됨.
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,,, 의 키값을 생성해주는 친구
@Override
public Member save(Member member) {
member.setId(++sequence); // id는 시스템이 정해주는 ID임.
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) { //그냥 store에서 꺼내면 됨
return Optional.ofNullable(store.get(id)); // 가져올 값이 null이어도 optional 으로 감쌀 수 있음.
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny(); // Map 에서 루프를 돌면서 같은 값이 찾아지면 반환
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
이런 식으로 MemoryMemberRepository를 작성한 후, 잘 돌아가는지 검증을 해야 한다.
검증하는 방식은 Test Case 를 이용하는 방법.
3. 회원 리포지토리 테스트 케이스 작성
내가 작성한 코드가 정상적으로 작성할까 ? 를 테스트하는 방법.
자바의 JUnit이라는 프레임워크로 테스트를 실행한다.
test -> java -> hello.hellospring 에 repository라는 패키지 생성하고,
관례적으로 MemoryMemberRepositoryTest 처럼 뒤에 "Test" 를 붙인다.
# Save 테스트하기
@Test
public void save(){
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
// System.out.println("result= " + (result == member)); // 이렇게 출력해서 볼수도 있음.
// Assertions.assertEquals(member, result); // Assertions 를 이용해서 검증가능.
// Assertions.assertEquals(member, null); // 이런식으로 Null 값 넣으면 오류가 남을 알수있다.
assertThat(member).isEqualTo(result); // AssertJ 에서 제공하는 프레임워크로도 작성가능.
}
Save 를 테스트하는 방법. "spring"이라는 이름을 가진 회원 생성 후 Assertions 내 기능을 이용해 같은 값이 잘 저장되었는지 확인.
(isEqualTo 활용)
# findByName, findById 테스트하기
@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);
}
하지만 MemoryMemberRepositoryTest 클래스를 실행시키면 오류가 나는데, 이유는
세 가지의 테스트 기능들은 순서를 보장하지 않기 때문에, findById <-> findByName 간의 member 생성에서 충돌이 일어난다.
해결 방법은,
@AfterEach //메서드가 끝날때 마다 해당함수를 실행 (여기서는 member clear)
public void afterEach(){
repository.clearStore();
} // Test는 순서와 관계없이 설계되어야 해서, test 세번 실행할 때 마다 clear 필요
해당 방법으로 세 번의 테스트가 실행될 때 마다 clearStore() 가 실행되도록 하면 된다.
clearStore() 는 MemoryMemberRepository 클래스 내에서,
public void clearStore(){
store.clear();
}
이렇게 단순하게 clear 함수를 이용해 생성만 해주면 된다.
* TDD ? |
- Test Driven Development - Test 를 먼저 작성해서 틀을 만든 후에, 만들고자 하는 main method 를 후에 작성하는 방법. |
4. 회원 서비스 개발
package hello.hellospring;
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
validateDuplicatedMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicatedMember(Member member) {
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);
}
}
* control + T : 리팩토링과 관련된 단축키 (여기서는 validateDuplicatedMember를 리팩토링함)
Repository는 단순한 네이밍 (save, findAll 같은 기능들)
Service는 비즈니스 모델 구현과 관련된 네이밍 ( join, validateDuplicatedMember 처럼)
으로 보통 네이밍한다.
5. 회원 서비스 테스트
** Test를 원하는 class (여기서는 MemberService) 를 드래그하고 맥북기준 "command + shift + T" 를 하면,
쉽게 Test Case 를 작성가능하다.
** command + B : 해당 클래스 혹은 변수로 바로이동
** command + option + v : 변수 추출하기
** control + R : 가장 최근에 실행했던 것을 다시 실행
** 테스트 케이스 Name 은 한글로 적어도 무관하다.
테스트 케이스를 작성할 때, given / when / then 이런 단계로 주석처리해놓고 구성하면 나중에 쉽게 해석이 가능하다.
예외처리에 관한 test 를 작성할 때, try - catch 문법도 가능하지만,
IllegalStateException e = assertThrows(IllegalStateException.class, ()->memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
이런 식으로 assertThrows 를 활용한 문장도 활용도가 높다.
또한, test class 에
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@BeforeEach 를 작성하면 , 테스트를 실행할 때 마다 독립적으로 해당함수가 실행된다.