-
스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (3) 회원 관리 예제 - 백엔드 개발Back-End/Spring 2022. 8. 14. 22:19
회원 관리
1) 비즈니스 요구사항 정리
2) 회원 도메인과 리포지토리 만들기
3) 회원 리포지토리 테스트 케이스 작성
4) 회원 서비스 개발
5) 회원 서비스 테스트
1) 비즈니스 요구사항 정리
- 데이터 : 회원ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않음 (가상의 시나리오)
- 컨트롤러 : 웹 MVC 컨트롤러 역할 (컨트롤러 또는 API 정의)
- 서비스 : 핵심 비즈니스 로직 구현 (중복 가입 불가 등) > 비즈니스 도메인 객체를 이용해 로직 구현
- 도메인 : 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등 주로 데이터베이스에 저장하고 관리됨
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- alt + enter : interface 자동 implements
- alt + insert : 파일 생성(class, interface 등)
2) 회원 도메인과 리포지토리 만들기
회원 도메인 : Member.java
package hello.hellospring.domain; public class Member { private Long id; // system이 회원을 구분하기 위해 부여하는 식별자 id private String name; // 회원이 직접 입력하는 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; } }
리포지토리
interface : MemberRepository.java
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 : method의 반환값이 null일 경우 처리하는 방법 중 하나 Optional<Member> findById(Long id); // repo에 저장된 회원을 id로 검색 Optional<Member> findByName(String name); // repo에 저장된 회원을 name으로 검색 List<Member> findAll(); // 모든 회원을 list로 반환 }
리포지토리 구현 : MemoryMemberRepository.java
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을 써야 하지만 예제이므로 pass private static long sequence = 0L; // 실무에서는 동시성 문제로 인해 long이 아닌 다른 타입을 사용하지만 예제이므로 pass @Override public Member save(Member member) { // 멤버를 repo에 저장 member.setId(++sequence); // id를 부여 store.put(member.getId(), member); // 저장 return member; } @Override public Optional<Member> findById(Long id) { return Optional.ofNullable(store.get(id)); // id로 회원을 찾아 반환 } @Override public Optional<Member> findByName(String name) { return store.values().stream() // stream을 열어 name과 일치하는 회원을 찾으면 반환 .filter(member -> member.getName().equals(name)) .findAny(); // 가장 처음 발견된 것을 반환 } @Override public List<Member> findAll() { return new ArrayList<>(store.values()); // 회원 list 반환 } }
3) 회원 리포지토리 테스트 케이스 작성
- main 메서드를 통해 실행
- 웹 어플리케이션의 컨트롤러를 통해 실행준비, 실행에 필요한 시간 多, 반복 실행의 어려움, 여러 테스트 한 번에 실행 어려움
▶ JUnit 프레임워크로 테스트 실행
Test : MemoryMemberRepositoryTest.java
Test 1. save()
package hello.hellospring.repository; import hello.hellospring.domain.Member; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class MemoryMemberRepositoryTest { MemberRepository repository = new MemoryMemberRepository(); @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, null); // 기대값, 실제 값 }
실행 결과 : Failed
@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); // 기대값, 실제 값 }
실행 결과 : Success
좀 더 직관적인 코드를 사용하자!
Assertions.assertThat(member).isEqualTo(result);
alt + enter을 눌러 해당 클래스를 import 하면 더욱 편하게 사용 가능 (static)assertThat(member).isEqualTo(result);
shift + F6 : rename - 일일히 이름을 고칠 필요없이 한 번에 고쳐진다.Test 2. findByName()
@Test public void findByName() { Member member1 = new Member(); member1.setName("spring1"); repository.save(member1); Member member2 = new Member(); member2.setName("spring2"); repository.save(member2); // Optional<Member> result = repository.findByName("spring"); Member result = repository.findByName("spring1").get(); // get()으로 Optional 없이 값을 얻을 수 있음 System.out.println("condition2 : result = " + (result == member1)); assertThat(result).isEqualTo(member1); }
result = member1 이므로 true
실행 결과 : Success
@Test public void findByName() { Member member1 = new Member(); member1.setName("spring1"); repository.save(member1); Member member2 = new Member(); member2.setName("spring2"); repository.save(member2); // Optional<Member> result = repository.findByName("spring"); Member result = repository.findByName("spring2").get(); // get()으로 Optional 없이 값을 얻을 수 있음 System.out.println("condition2 : result = " + (result == member1)); assertThat(result).isEqualTo(member1); }
result = member2 이기 때문에 false
실행 결과 : False
Test 3. findAll()
@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); }
실행 결과 : Success
@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(3); }
실행 결과 : Failed
전체 테스트 코드 run
실행 결과 : Failed
- 원인: test 코드의 실행 순서 보장 X
findAll() ▶ findByName() ▶ save() 순서로 실행되었는데, findAll()에서 추가된 데이터가 다음 코드 실행에 영향을 주었기 때문에 failed
사용한 데이터를 clear 해주어야 한다!
1) clear() 추가
- src/main/java/hello.hellospring.repository/MemoryMemberRepository.java
public void clearStore() { store.clear(); // repo clear }
2) test code에 afterEach() 추가
- test method가 실행되어 종료될 때마다 clear() 메서드를 실행시키는 메서드
@AfterEach public void afterEach() { repository.clearStore(); }
실행 결과 : Success
회원 서비스 개발
- 비즈니스 도메인 객체를 이용하여 실제 비즈니스 로직을 구현
Service class에서는 비즈니스 로직 관련 naming 필요
▶ 비즈니스 로직에 의존 (비즈니스 로직을 처리하는 부분이기 때문)
ctrl + alt + v : 자동 완성 반환
ctrl + alt + shift + T : refactoring 관련 기능
ctrl + alt + M : Extract Method
실행 결과
service 1. join(member) : 회원가입
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("이미 존재하는 회원입니다."); }); }
service 2. findMembers() : 전체 회원 조회
public List<Member> findMembers() { return memberRepository.findAll(); }
service 3. findOne(memberId) : memberId로 회원 조회
public Optional<Member> findOne(Long memberId) { return memberRepository.findById(memberId); }
회원 서비스 테스트
간단하게 test class 생성하는 방법
1) ctrl + shift + T : create new test
2) test class 설정
3) test class 생성
tip!
test code의 경우 method 이름을 직관적으로 바꾸어 사용해도 좋다!
test method의 구조
- given
- when
- then
Assertions
import api
- org.assertj.core.api 선택
Test 1. 회원 가입
@Test void 회원가입() { // given Member member = new Member(); member.setName("hello"); // when long saveId = memberService.join(member); // then Member findMember = memberService.findOne(saveId).get(); System.out.println("member = join member : " + member.getName().equals(findMember.getName())); assertThat(member.getName()).isEqualTo(findMember.getName()); }
실행 결과 : Success
Test 1-1. 중복 회원 예외 발생여부 확인
@Test public void 중복_회원_예외() { // given Member member1 = new Member(); member1.setName("spring"); Member member2 = new Member(); member2.setName("spring"); // when memberService.join(member1); try { memberService.join(member2); fail(); } catch (IllegalStateException e) { // then assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); } }
실행 결과 : Success
try-catch 를 사용하지 않고 test
@Test public void 중복_회원_예외() { // given Member member1 = new Member(); member1.setName("spring"); Member member2 = new Member(); member2.setName("spring"); // when memberService.join(member1); assertThrows(IllegalStateException.class, () -> memberService.join(member2)); // member2 join 시 IllegalStateException이 throw 되어야 한다! }
실행 결과 : Success
만약, NullPointerException을 기대한다면
@Test public void 중복_회원_예외() { // given Member member1 = new Member(); member1.setName("spring"); Member member2 = new Member(); member2.setName("spring"); // when memberService.join(member1); assertThrows(NullPointerException.class, () -> memberService.join(member2)); }
실행 결과 : Failed
예상되는 Exception이 아닌 다른 type의 Exception이 throw 되었음💡 service test에서도 모든 test method를 실행했을 때 error가 발생하지 않아야 한다.
: MemberService의 DI 분리 필요
1) MemberService class
생성자를 호출하여 repository를 외부에서 생성 가능하게 한다.
private final MemberRepository memberRepository; public MemberService(MemoryMemberRepository memberRepository) { this.memberRepository = memberRepository; }
2) MemberServiceTest class
각 test method를 실행하기 전 repository를 새로 만들어 사용한다.
MemberService memberService; MemoryMemberRepository memberRepository; @BeforeEach public void beforeEach() { // 각 test를 실행할 때마다 새로운 repository를 생성하여 사용 가능 memberRepository = new MemoryMemberRepository(); memberService = new MemberService(memberRepository); }
'Back-End > Spring' 카테고리의 다른 글
스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (7) AOP (0) 2022.08.21 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (6) 스프링 DB 접근 기술 (0) 2022.08.21 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (5) 회원 관리 예제 - 웹 MVC 개발 (0) 2022.08.14 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (4) 스프링 빈과 의존관계 (0) 2022.08.14 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (2) 스프링 웹 개발 기초 (0) 2022.08.14