[Spring] 3. 회원 관리 예제 - 백엔드 개발
업데이트:
이번 섹션에서는 회원 관리 예제를 만들어보게 된다.
비즈니스 요구사항 정리
예제 시나리오
- 데이터: 회원ID, 이름
- 기능: 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)
일반적인 웹 애플리케이션 계층 구조
- 컨트롤러: 웹 MVC의 컨트롤러 역할
- 서비스: 핵심 비즈니스 로직 구현
- 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
클래스 의존관계
- 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
- 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
회원 도메인과 리포지토리 만들기
도메인
domain
패키지 생성 후 Member.java
클래스를 생성해 준다.
요구사항에 맞게 id
, name
을 만들어 준다.
hello/hellospring/domain/Member.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package hello.hellospring.domain; 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) { Name = name; } }
레포지토리
repository
패키지 생성 후 MemberRepository.java
인터페이스를 생성해 준다.
요구사항에 맞게 인터페이스를 만들어 준 후, MemoryMemberRepository.java
로 구현까지 해 주자.
hello/hellospring/repository/MemberRepository.java
1 2 3 4 5 6 7 8 9 10 11 12 13 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<Member> findById(Long id); Optional<Member> findByName(String name); List<Member> findAll(); }
hello/hellospring/repository/MemoryMemberRepository.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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; @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()); } }
회원 리포지토리 테스트 케이스 작성
작성한 코드가 올바른지 확인하는 테스트 케이스를 작성해 보자.
주의할 점은 main
이 아닌 test
디렉토리를 사용해야 한다.
test 시에는 @Test
데코러이터를 통해 테스트 함수를 정의해 줄 수 있다.
save
`test/java/hello/hellospring/repository/MemorymemberRepositoryTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package hello.hellospring.repository; import hello.hellospring.domain.Member; import org.junit.jupiter.api.Test; class MemoryMemberRepositoryTest { MemoryMemberRepository repository = new MemoryMemberRepository(); @Test public void save() { Member member = new Member(); member.setName("testname"); repository.save(member); Member result = repository.findById(member.getId()).get(); System.out.println("(result == member) = " + (result == member)); } }
쉬운 방법으로는 위와 같이 콘솔 출력을 통해 테스트를 할 수 있을 것이다.
하지만 이는 눈으로 직접 확인해야 하며, 잘못된 경우에도 그대로 빌드가 진행되기에 좋지 못하다.
따라서 assert
를 통해 잘못된 경우 에러를 발생시키게 되는데, org.assertj.core.api.Assertions
를 사용해 편리하게 한다고 한다.
1 2 3 4 5 6 7 8 9 10 @Test public void save() { Member member = new Member(); member.setName("testname"); repository.save(member); Member result = repository.findById(member.getId()).get(); assertThat(member).isEqualTo(result); }
여러 테스트가 있을 시, repository
에 프로그램 종료시 까지 데이터가 그대로 남아있기에, 다른 테스트에서의 데이터가 남아 맞는 코드임에도 테스트가 틀렸다며 오류가 발생할 수 있다.
따라서 매 테스트마다 초기화를 해 주어야 한다.
이를 위해 레포지토리에 clearStore
를 추가로 만들어준 후, 매 테스트가 끝나면 실행되도록 해 보자.
MemoryMemberRepository.java
1 2 3 public void clearStore() { store.clear(); }
MemoryMemberRepositoryTest.java
1 2 3 4 @AfterEach public void afterEach() { repository.clearStore(); }
위의 코드들을 각각 추가해 주면, @AfterEach
데코레이터로 인해 매 테스트가 끝날 때 마다 afterEach()
가 실행된다.
findByName
1 2 3 4 5 6 7 8 9 10 11 12 13 @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); }
findAll
1 2 3 4 5 6 7 8 9 10 11 12 13 @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); }
회원 서비스 개발
다시 main
으로 돌아가 서비스를 만들어 보자.
회원 가입
hellospring/service/MemberService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package hello.hellospring.service; import hello.hellospring.domain.Member; import hello.hellospring.repository.MemoryMemberRepository; public class MemberService { private final MemoryMemberRepository memberRepository = new MemoryMemberRepository(); /** * 회원 가입 */ public long join(Member member) { // 이름 중복 방지 validateDuplicatedMember(member); memberRepository.save(member); return member.getId(); } private void validateDuplicatedMember(Member member) { memberRepository.findByName(member.getName()) .ifPresent(m -> { throw new IllegalStateException("이미 존재하는 회원입니다"); }); } }
전체 회원 조회
1 2 3 4 5 6 /** * 전체 회원 조회 */ public List<Member> findMembers() { return memberRepository.findAll(); }
단일 회원 조회
1 2 3 public Optional<Member> findOne(Long memberId) { return memberRepository.findById(memberId) }
회원 서비스 테스트
Dependency Injection
먼저 MemberService
에서 MemberRepository
와 테스트 시 MemberRepository
가 같은 인스턴스가 되게 하기 위해 MemberService
를 다음과 같이 리팩토링하자.
hellospring/service/MemberService.java
1 2 3 4 5 private final MemoryMemberRepository memberRepository; public MemberService(MemoryMemberRepository memberRepository) { this.memberRepository = memberRepository; }
이렇게 의존관계를 바꿔 주는 것을 Dependency Injection
이라 한다.
테스트 코드
테스트 코드는 다음과 같다.
test/java/hello/hellospring/service/MemberServiceTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package hello.hellospring.service; // import 생략 class MemberServiceTest { MemberService memberService; MemoryMemberRepository memberRepository; @BeforeEach public void beforeEach() { memberRepository = new MemoryMemberRepository(); memberService = new MemberService(memberRepository); } @AfterEach public void afterEach() { memberRepository.clearStore(); } @Test void 회원가입() { //given Member member = new Member(); member.setName("Spring"); //when Long saveId = memberService.join(member); //then Member findMember = memberService.findOne(member.getId()).get(); assertThat(member.getName()).isEqualTo(findMember.getName()); } @Test void 중복_회원_예외() { //given Member member1 = new Member(); member1.setName("Spring"); Member member2 = new Member(); member2.setName("Spring"); //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("이미 존재하는 회원입니다"); } */ //then } @Test void findMembers() { } @Test void findOne() { } }
댓글남기기