02. 회원 관리 예제 - 백엔드 개발
개발 순서는 다음과 같다.
- 비즈니스 요구사항 정리
- 회원 도메인과 리포지토리 만들기
- 회원 리포지토리 테스트 케이스 작성
- 회원 서비스 개발
- 회원 서비스 테스트
1. 비즈니스 요구사항 정리
데이터: 회원ID, 이름
기능: 회원 등록, 조회
갸상 시나리오: 아직 데이터 저장소가 선정되지 않음
- 컨트롤러: 웹 MVC의 컨트롤러 역할
- 서비스: 핵심 비즈니스 로직 구현 (ex: 회원의 이름은 중복될 수 없다.)
- 리포지토리: DB에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인: 회원, 주문, 쿠폰 등 주로 DB에 저장하고 관리되는 비즈니스 도메인 객체
- 아직 데이터 저장소가 선정되지 않았기 때문에 먼저 인터페이스로 구현 클래스를 변경할 수 있도록 설계한다.
- 데이터 저장소는 RDB, NoSQL 등 다양한 저장소를 고민중인 상황으로 가정한다.
- 개발 진행을 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터저장소를 사용할 것이다.
2. 회원 도메인과 리포지토리 만들기
회원 객체
// DB에 저장 및 관리되는 비즈니스 도메인 객체
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;
}
}
회원 리포지토리 인터페이스
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
// 회원이 저장소에 저장됨
Member save(Member member);
// Optional: Java 8부터 들어간 기능
// 요즘은 null을 그대로 반환하는 것 대신, Optional로 감싸서 반환하는 방식을 선호
// 회원 ID, NAME을 찾을 수 있음
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
// 현재까지 저장된 모든 회원 리스트들을 반환
List<Member> findAll();
void clearStore();
}
회원 리포지토리 메모리 구현체
import hello.hellospring.domain.Member;
import java.util.*;
//DB에 접근, 도메인 객체를 DB에 저장하고 관리
public class MemoryMemberRepository implements MemberRepository {
//Map<key, member> key는 회원의 id, 값은 member (자세한 내용 임베드 확인)
//sequence: key값 0,1,2 같은 것들을 생성해주는 객체
private static Map<Long, Member> store = new HashMap<>();
private static Long sequence = 0L;
@Override
public Member save(Member member) {
//(1) 회원ID를 세팅하고
member.setId(++sequence);
//(2) Map에 저장
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
//회원ID 값이 NULL일 경우, Optional로 감싸서 반환
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
//(1) 루프를 돌리면서, 회원이름과 파라미터로 넘어온 이름이 같은지 확인
//(2) 같은 경우가 있을 경우 반환
return store.values().stream()
.filter(member->member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
//store에 저장된 회원들을 모두 반환
return new ArrayList<>(store.values());
}
//테스트가 실행되고 끝날 때마다 저장소를 지우는 메소드
public void clearStore(){
store.clear();
}
}
//동작이 잘 되는지 확인하기 위해서 테스트 케이스를 작성
java HashMap usage
[Java] 자바 HashMap 사용법 & 예제 총정리
HashMap 이란? HashMap은 Map 인터페이스를 구현한 대표적인 Map 컬렉션입니다. Map 인터페이스를 상속하고 있기에 Map의 성질을 그대로 가지고 있습니다. Map은 키와 값으로 구성된 Entry객체를 저장하는
coding-factory.tistory.com
3. 회원 리포지토리 테스트 케이스 작성
개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 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 static org.assertj.core.api.Assertions.*;
public class MemoryMemberRepositoryTest {
MemberRepository repository = new MemoryMemberRepository();
//모든 테스트 코드는 순서와 상관 없이 설계
//∴ 테스트가 끝나면 데이터를 깔끔하게 클리어 해줘야 함
@AfterEach
public void afterEach(){
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
//이름 셋팅
member.setName("Spring");
//리포지토리에 이름을 저장
repository.save(member);
//Optional<>로 묶여있을 땐 .get()으로 값을 꺼내올 수 있음 (자주 쓰는 방법은 아님)
Member result = repository.findById(member.getId()).get();
//결과 확인하는 방법 3가지
//(1) 저장한 값과 DB에서 꺼낸 값이 똑같으면 true 반환
System.out.println("result = " + (result == member));
//매번 시각화하여 확인할 수 없기 때문에 다른 방법 사용
//(2) Assertions (org.junit.jupiter)
Assertions.assertEquals(member, result);
//(3) Assrtions (org.assertj.core.api)
//static import-Alt + Enter
assertThat(member).isEqualTo(result);
}
@Test
public void findByName(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
//rename-shift+f6
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
Member result = repository.findByName("spring1").get();
Member result2 = repository.findByName("spring2").get();
assertThat(member1).isEqualTo(result);
assertThat(member2).isEqualTo(result2);
}
@Test
public void findAll(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member1.setName("spring2");
repository.save(member2);
List<Member> result = repository.findAll();
//데이터 개수 확인
assertThat(result.size()).isEqualTo(2);
}
}
- @AfterEach : 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이렇게 되면 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다. @AfterEach 를 사용하면 각 테스트가 종료될 때 마다 이 기능을 실행한다. 여기서는 메모리 DB에 저장된 데이터를 삭제한다.
- 테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다.
4. 회원 서비스 개발
회원 서비스 개발
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
//회원 서비스 핵심 비즈니스 로직 구현
//보통 비즈니스에 의존적인 용어를 사용
//테스트 코드 만들기-ctrl+shift+t
public class MemberService {
private final MemberRepository memberRepository;
//DI(Dependency Injection)-내가 직접 생성하지 않고, 외부에서 memberRepository를 넣어준다.
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
//회원가입-회원ID만 반환한다는 설정
public Long join(Member member) {
//같은 이름이 있는 중복 회원은 안되는 상황
//Optional로 객체를 감싸서 반환하기 때문에 여러 메서드를 사용할 수 있음
//변수 추출-ctrl+alt+v
/* Optional<Member> result = memberRepository.findByName(member.getName()); */
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
//자동화 리팩토링-Shift+Ctrl+Alt+T
private void validateDuplicateMember(Member member) {
//result에 값이 있으면 로직이 동작
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
//전체 회원 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
//테스트 코드에서 회원ID 값을 찾아주는 메서드
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
5. 회원 서비스 테스트
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
- 기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하게 했다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
- 위의 회원 리포지토리의 코드가 회원 서비스 코드를 DI 가능하게 변경한다.
회원 서비스 테스트
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.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.*;
//이전 실행을 재실행-Ctrl + Shift + F10
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
//테스트가 동작하기 전, 의존관계 주입 (DI: Dependency Injection)
@BeforeEach
public void beforeEach() {
//(1)MemoryMemberRepository를 만들고
memberRepository = new MemoryMemberRepository();
//(2)memberRepository를 memberService에 넣어준다.
memberService = new MemberService(memberRepository);
}
//테스트 코드 실행 후 저장소 클리어
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
// 정상 플러그
@Test
void 회원가입() {
//given (상황)
Member member = new Member();
member.setName("hello");
//when (실행)
Long saveId = memberService.join(member);
//then (결과)
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
// 예외 플러그(중요)
// 회원 이름이 중복되었을 때의 로직
@Test
public void 중복회원예외() {
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
//변수추출-ctrl+alt+v
//memberService.join(member2)를 넣을 때 IllegalStateException이 발생해야 한다.
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
//오류메시지가 "이미 존재하는 회원입니다."인지 확인
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
- @BeforeEach : 각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 새로 맺어준다.
참고: 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (https://inf.run/PWvM)
'Study' 카테고리의 다른 글
[STUDY] 스프링 입문 기록(4) - 코드로 배우는 스프링부트, 웹 MVC, DB 접근 기술 (0) | 2023.03.10 |
---|---|
[STUDY] 스프링 입문 기록(3) - 코드로 배우는 스프링부트, 웹 MVC, DB 접근 기술 (0) | 2023.03.01 |
[STUDY] 스프링 입문 기록(1) - 코드로 배우는 스프링부트, 웹 MVC, DB 접근 기술 (0) | 2023.02.23 |
[디자인 패턴] MVC, MVP, MVVM (0) | 2022.07.26 |
[STUDY] JAVA 웹 개발 코스 - 개발 언어 (0) | 2022.06.30 |