mojo's Blog
회원 관리 - 백엔드 개발 본문
비즈니스 요구사항 정리
- 데이터 : 회원ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)
※ 일반적인 웹 애플리케이션 계층 구조
- 컨트롤러 : 웹 MVC의 컨트롤러 역할
- 서비스 : 핵심 비즈니스 로직 구현
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인 : 비즈니스 도메인 객체 ex) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
※ 클래스 의존관계
- 아직 데이터 저장소가 선정되지 않았기 때문에 인터페이스로 먼저 구현 클래스를 변경할 수 있도록 설계
- 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정
- 개발 진행을 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소를 사용
회원 도메인과 리포지토리 만들기
1. hello.hellospring 폴더 아래에 domain 폴더를 형성하고 Member 클래스를 만든다.
Member 클래스의 코드는 아래와 같이 작성한다.
package hello.hellospring.domain;
public class Member {
private Long id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
- 회원의 id와 name을 Long, String 형으로 선언하였다.
- id, name에 대한 getter/setter 를 만들어준다.
2. hello.hellospring 폴더 아래에 repository 폴더를 형성한 후 MemberRepository 인터페이스를 형성한다.
MemberRepository 인터페이스는 아래와 같이 구현한다.
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();
}
3. repository 폴더 아래에 MemoryMemberRepository 클래스를 형성한다.
MemoryMemberRepository 클래스의 코드는 아래와 같이 작성한다. (MemberRepository 인터페이스를 구현)
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());
}
}
- sequence 는 회원의 아이디를 1, 2, 3, ... 순차적으로 부여하기 위한 용도로 사용된다.
- save() : 파라메터로 받아온 객체 member의 id를 정적 변수 sequence를 통해 설정하며 store에 해당 id에 대한 Member 객체를 저장한 후 해당 객체를 반환하는 함수이다.
- findById() : store를 통해 해당 id가 존재하는지 확인하며 null 값일 경우(store에 해당 id가 없을 경우) null을 감싸서 반환할 수 있도록 한다.
- findByName() : 람다를 사용하여 구현하였으며 filter를 통해 member가 파라메터로 받아온 name과 동일한 경우 반환하도록 한다.
- findAll() : store에 저장된 Member 객체들을 ArrayList 형태로 만들어서 반환하도록 한다.
Optional 은 NullpointerException 을 해결하기 위해 나타난 것이다.
Java Optional 사용법 - 아빠프로그래머의 좌충우돌 개발하기! (daddyprogrammer.org) 를 참고하면
Optional 에 대해 좀 더 알아갈 수 있다!
회원 리포지토리 테스트 케이스 작성
개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다.
이러한 방법은 준비하고 실행하는데 오래 걸리고 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다.
자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
1. [test] -> [java] -> [hello.hellospring] 폴더 아래에 repository 폴더를 형성한다.
그 후에 MemoryMemberRepositoryTest 클래스를 형성한 후 다음과 같이 테스트 케이스 코드를 작성한다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.Test;
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));
}
}
- 이전에 작성하였던 코드가 제대로 돌아가는지 검증을 하기 위해서 위와 같이 코드를 작성하여 원하는 결과가 나오는지를 확인하는 것이다.
2. save() 를 실행하면 다음과 같은 결과가 나오는 것을 알 수 있다.
- result = true : 코드를 잘 구현한 것임을 알 수 있다.
- result = false 이거나 에러가 난 경우 : 이전에 작성한 코드에서 문제가 생겼다는 것을 알 수 있다.
3. System.out.println 을 사용하여 출력하여 검증하는 방법을 대신하여 Assertions 를 활용하는 방법이 있다.
다음과 같이 코드를 수정하도록 한다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
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();
Assertions.assertEquals(member, result);
}
}
- Assertions.assertEquals(expect, actual) : 기대하고 있는 값은 expect 이며 테스트 할 값은 actual 이다.
4. 테스트 할 값(actual)이 기대하고 있는 값(expect)와 동일할 경우 다음과 같이 나타난다.
만약 동일하지 않을 경우 다음과 같이 나타난다.
5. findByName, findAll 에 대한 테스트 코드를 추가로 만들어 본다.
아래와 같이 코드를 작성한다.
@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);
}
findByName() 에 대한 테스트 결과
findAll() 에 대한 테스트 결과
6. class MemberRepositoryTest 왼쪽에 실행 버튼을 눌러서 전체 테스트 코드가 잘 돌아가는지 확인한다.
음... 아까는 잘 돌아갔는데 딱 하나만 통과한 모습이다. 왜 그럴까?
우선 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);
}
repository 객체에 member1, member2 를 저장하여 총 사이즈가 2인 상태이다.
이때 나머지 findByName(), save() 메서드를 실행하게 되면 repository 객체가 사이즈가 2인 상태로 진행이 되기 때문에 에러가 나는 것이다.
테스트는 메서드들의 순서와 상관없이 실행된다.
따라서 이를 해결하기 위해선 메서드가 끝날때마다 repository를 초기화하도록 하는 메서드를 만들어 줘야한다.
우선 MemoryMemberRepository 클래스에서 다음과 같이 코드를 추가해준다.
public void clearStore(){
store.clear();
}
그리고 MemoryMemberRepositoryTest 클래스에서 다음과 같이 코드를 추가해준다.
@AfterEach
public void afterEach(){
repository.clearStore();
}
- @AfterEach : 예를 들어 findAll(), findByName(), save() 메서드 순으로 테스트가 돌아간다고 할 때 findAll() 메서드가 끝나고 afterEach() 메서드를 실행시키고 findByName() 메서드가 끝나고 afterEach() 메서드를 실행시키고 save() 메서드가 끝나고 afterEach() 메서드를 실행시키도록 한다. (중요한 애너테이션임을 기억하기)
실행하면 다음과 같은 결과가 나타난다.
회원 서비스 개발
1. hello.hellospring 폴더 아래에 service 폴더를 형성한 후 MemberService 클래스를 형성한다.
2. MemberService 클래스의 코드를 아래와 같이 작성한다.
package hello.hellospring.service;
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
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member){
memberRepository.findByName(member.getName())
.ifPresent(member1 -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/*
* 전체 회원 조회
*/
public List<Member> findMembers(){
return memberRepository.findAll();
}
/*
* 특정 회원 조회
*/
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
}
- validateDuplicateMember : memberRepository 객체에서 파라메터로 받아온 member의 이름이 존재하는 경우 예외 처리를 하도록 한다.
회원 서비스 테스트
1. [ctrl] + [shift] + [t] 를 동시에 누르면 테스트 코드를 쉽게 작성할 수 있다.
동시에 누를 경우 아래와 같이 Create New Test... 가 뜨는데 클릭한다.
2. JUnit5 를 선택하고 Member 아래에 메서드들을 전부 선택한 후 OK 버튼을 누른다.
3. test 폴더 아래에 service 폴더에 MemberServiceTest 클래스가 생성된 것을 확인할 수 있다.
4. MemberService에서 생성자를 통해 파라메터로 받은 객체를 현재 클래스의 객체에 할당해주는 방식으로 다음과 같이 수정한다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
그 후에 MemberServiceTest 클래스의 코드를 다음과 같이 작성한다.
package hello.hellospring.service;
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.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
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("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);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
/*memberService.join(member1);
try {
memberService.join(member2);
fail("예외가 발생해야 합니다.");
} catch(IllegalStateException e){
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}*/
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
- @beforeEach : memberService, memberRepository 객체를 새로 형성하는 초기화 작업이 이뤄진다.
- @afterEach : memberRepository 객체를 clear 해주도록 한다.
afterEach가 없어도 에러가 일어나지 않을거 같다. 과연 그럴까?
에러가 발생한다.
MemberRepository 를 구현한 MemoryMemberRepository 클래스의 코드를 다시 한번 살펴보도록 한다.
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
...
}
static 으로 선언된 store, sequence 가 프로그램이 계속 진행되고 있는 한 현 상태를 유지하고 있다는 것을 유의해야 한다.
즉, 새로운 객체를 받아올지라도 이전에 store 객체에 put 했던 Member 객체들이 아직 남아있게 된다.
따라서 @afterEach 애너테이션을 통해 store를 clear 해줘야 한다. (이때 sequence는 안해도 무방)
[추가실습 - 1]
memberRepository 에 대한 테스트 코드 작성해보기
public class MemoryMemberRepositoryTest {
MemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save(){
Member member[] = new Member[100];
for (int i = 0; i < member.length; i++) {
member[i] = new Member();
member[i].setName("[" + i + "]");
repository.save(member[i]);
}
for (int i = 0; i < member.length; i += 10) {
Member result = repository.findByName("[" + i + "]").get();
Assertions.assertEquals(member[i], result);
}
}
@Test
public void findByName(){
}
@Test
public void findAll(){
Member member[] = new Member[100];
List<Member> memberList;
for (int i = 0; i < member.length; i++) {
member[i] = new Member();
member[i].setName("[" + i + "]");
repository.save(member[i]);
}
memberList = repository.findAll();
Assertions.assertEquals(100, memberList.size());
}
}
[추가실습 - 2]
memberService 에 대한 테스트 코드 작성해보기
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 join() {
// given
Member member1 = new Member();
member1.setName("자바");
Member member2 = new Member();
member2.setName("코틀린");
// when
member1.setId(memberService.join(member1));
member2.setId(memberService.join(member2));
// then
Member result1 = memberService.findOne(member1.getId()).get();
Assertions.assertEquals(member1, result1);
Member result2 = memberService.findOne(member2.getId()).get();
Assertions.assertEquals(member2, result2);
}
@Test
public void validateDuplicateMember(){
// given
Member member1 = new Member();
member1.setName("자바");
Member member2 = new Member();
member2.setName("자바");
// when
memberService.join(member1);
// then
try {
memberService.join(member2);
fail("이미 존재하는 회원입니다.");
} catch(IllegalStateException e) {
Assertions.assertEquals("이미 존재하는 회원입니다.", e.getMessage());
}
}
@Test
void findMembers() {
int expect = 2;
int actual = 0;
// given
Member member1 = new Member();
member1.setName("자바");
Member member2 = new Member();
member2.setName("코틀린");
// when
member1.setId(memberService.join(member1));
member2.setId(memberService.join(member2));
// then
List<Member> memberList = memberService.findMembers();
for (Member member : memberList) {
if (member.getName().equals("자바") || member.getName().equals("코틀린"))
actual++;
}
Assertions.assertEquals(expect, actual);
}
@Test
void findOne() {
// given
Member member1 = new Member();
member1.setName("자바");
Member member2 = new Member();
member2.setName("코틀린");
// when
member1.setId(memberService.join(member1));
member2.setId(memberService.join(member2));
// then
Assertions.assertEquals(memberService.findOne(member1.getId()).get(), member1);
Assertions.assertEquals(memberService.findOne(member2.getId()).get(), member2);
}
}
'Spring' 카테고리의 다른 글
회원 관리 예제 - 웹 MVC 개발 (0) | 2022.01.13 |
---|---|
스프링 빈과 의존관계 (0) | 2022.01.12 |
스프링 웹 개발 기초 (0) | 2022.01.11 |
IntelliJ로 프로젝트 생성 및 view 환경설정 (0) | 2022.01.10 |
스프링 기반 뉴스 기사 관리 웹 서비스 (0) | 2022.01.07 |