회원 관리 예제 2 - 리포지토리
이 포스트는 김영한님의 ‘스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술’을 수강하고 작성하였습니다.
일반적인 웹 어플리케이션 계층 구조
- 컨트롤러: 웹 MVC의 컨트롤러 역할
- 서비스: 도메인을 이용하여 핵심 비즈니스 로직 구현
- 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인: 비즈니스 도메인 객체, DB에 저장하고 관리
- 예) 회원, 쿠폰, 주문 등등
클래스 의존관계
- 인터페이스로 구현 클래스를 변경할 수 있도록 설계
- 가벼운 개발을 위해 초기 개발 단계에서는 가벼운 메모리 기반의 데이터 저장소 사용
회원 도메인과 리포지토리 만들기
깃허브 리포지토리 주소: https://github.com/nyj001012/springboot_tutorial
도메인 만들기
package hello.hellospring.domain;
import java.util.concurrent.atomic.AtomicLong;
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;
}
}
리포지토리 만들기
리포지토리 인터페이스
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
메모리 리포지토리
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class MemoryMemberRepository implements MemberRepository {
private static ConcurrentHashMap<Long, Member> store = new ConcurrentHashMap<>();
private static AtomicLong sequence = new AtomicLong(0);
@Override
public Member save(Member member) {
sequence.incrementAndGet();
member.setId(sequence.longValue());
store.put(sequence.longValue(), 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<Member>(store.values());
}
public void clearStore() {
store.clear();
sequence = new AtomicLong(0);
}
}
원래 강의에서는 AtomicLong
대신 Long
을 사용하였는데, 강의에서 “실무에서는 동시성 문제 때문에 AtomicLong
을 사용하지만, 여기서는 간단하게 Long
을 사용하겠다.”는 말을 듣고 궁금해서 사용했다. 이 부분에 대해서는 따로 포스트를 작성하도록 하겠다.
AtomicLong
과 마찬가지로 ConcurrentHashMap
또한 같은 이유에서 사용했다.
테스트 코드 작성
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.*;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
void afterEach() {
repository.clearStore();
}
@Nested
class TestSave {
@Test
void save() {
Member yejin = new Member();
yejin.setName("예진");
Member saveResult = repository.save(yejin);
assertThat(saveResult).isEqualTo(yejin);
assertThat(saveResult.getId().longValue()).isEqualTo(1);
}
}
@Nested
class TestExcludeSave {
@BeforeEach
void beforeEach() {
Member yejin = new Member();
Member yena = new Member();
yejin.setName("예진");
yena.setName("예나");
repository.save(yejin);
repository.save(yena);
}
@Test
void findById() {
Optional<Member> findResult = repository.findById(1L);
Member member = findResult.orElse(null);
assertThat(member).isNotNull();
assertThat(member.getName()).isEqualTo("예진");
}
@Test
void findByIdFail() {
Optional<Member> findResult = repository.findById(-2L);
assertThat(findResult.isEmpty()).isTrue();
}
@Test
void findByName() {
Optional<Member> findResult = repository.findByName("예진");
Member member = findResult.orElse(null);
assertThat(member).isNotNull();
assertThat(member.getId()).isEqualTo(1);
}
@Test
void findByNameFail() {
Optional<Member> findResult = repository.findByName("고구마");
assertThat(findResult.isEmpty()).isTrue();
}
@Test
void findAll() {
List<Member> memberList = repository.findAll();
assertThat(memberList.size()).isEqualTo(2);
}
}
}
테스트 코드도 강의보다는 조금 더 자세하게 작성했다.
@BeforeEach
로 각 테스트 케이스가 시작되기 전에, repository
에 데이터를 저장하는 일을 했다.
save()
함수 테스트 케이스 동작 시 기존 repository
에 데이터를 저장하는 작업이 필요 없어서, @Nested
어노테이션을 이용하여 미리 repository
에 데이터를 저장해야하는 케이스들과 아닌 케이스들로 나누었다.
@AfterEach
로는 repository
를 초기화하는 역할을 하기 때문에, 모든 @Nested
클래스에게 똑같이 적용되도록 중첩 클래스 바깥에 정의해두었다.
댓글남기기