비즈니스 요구사항 정리
- 데이터 : 회원ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소 (DB) 가 선정되지 않았다는 가상의 시나리오
- 일반적인 웹 애플리케이션 계층 구조
컨트롤러 : 웹 MVC의 컨트롤러 역할
서비스 : 핵심 비즈니스 로직 구현 - 예) 회원은 중복가입이 안된다.
리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
도메인 : 비즈니스 도메인 객체 - 예) 회원, 주문, 쿠폰 등 주로 데이터베이스에 저장하고 관리됨
- 클래스 의존관계
아직 데이터 저장소가 선정되지 않아서, 우선 interface로 구현 클래스를 변경할 수 있도록 설계
개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
→ 향후 데이터 저장소를 선정하고 나서 바꿔 끼우기 위해서 인터페이스가 필요하므로 인터페이스를 정의하는 것
회원 도메인과 리포지토리 만들기
- 회원 객체 (도메인)
// main/java/hello/hellospring/domain/Member.java
public class Member {
private Long id; // 데이터 구분을 위해 시스템이 정한 id
private String name;
// Getter, Setter
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;
}
}
- 회원 리포지토리 인터페이스
// main/java/hello/hellospring/repository/MemberRepository.java
public interface MemberRepository {
// Repository의 4가지 기능
Member save(Member member); // 회원 저장
Optional<Member> findById(Long id); // id로 회원 찾기
Optional<Member> findByName(String name); // name으로 회원 찾기
List<Member> findAll(); // 모든 회원 리스트
}
- 회원 리포지토리 메모리 구현체
// main/java/hello/hellospring/repository/MemoryMemberRepository.java
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에 저장하기 전에 멤버의 id 값을 세팅하고
store.put(member.getId(), member); // store에 저장 (Map에 저장)
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); // null도 반환될 수도 있도록 Optional 사용
}
@Override
public Optional<Member> findByName(String name) {
// 람다를 사용해 루프로 돌리면서 파라미터로 넘어온 name과 같은지 확인하여 필터링하여 반환
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
// store에 있는 values인 Member들을 반환
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear(); // store를 clear
}
}
회원 리포지토리 테스트 케이스 작성
- main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 개발 기능을 실행하는 방법은
준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있어
JUnit이라는 프레임워크로 테스트를 실행해 단점 해결 - 회원 리포지토리 메모리 구현체 테스트
// test/java/hello/hellospring/repository/MemoryMemberRepositoryTest.java
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
// 테스트 메서드 하나가 끝난 후 데이터를 clear
@AfterEach
public void afterEach() {
repository.clearStore();
}
// 회원 저장 테스트
@Test
public void save() {
// given
Member member = new Member();
member.setName("spring");
// when
repository.save(member); // 저장
// then
Member result = repository.findById(member.getId()).get(); // 저장 시 셋팅된 아이디를 가져와
// System.out.println("result = " + (result == member)); // result = true 글자로 출력
// Assertions.assertEquals(result, member); // 같은지 비교하는 방법 1)
assertThat(result).isEqualTo(member); // 같은지 비교하는 방법 2)
}
// name으로 회원 찾기 테스트
@Test
public void findByName() {
// given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
// when
Member result = repository.findByName("spring1").get();
// then
assertThat(result).isEqualTo(member1);
}
// 모든 회원 리스트 가져오기 테스트
@Test
public void findAll() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
// when
List<Member> result = repository.findAll();
// then
assertThat(result.size()).isEqualTo(2); // 갯수 비교를 통해 검증
}
}
- 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있게 되면 다음 테스트가 실패할 가능성이 있음!
@AfterEach 를 사용하면 각 테스트가 종료될 때 마다 이 기능을 실행함 (여기서는 메모리 DB에 저장된 데이터를 삭제)
테스트는 각각 독립적 (순서 상관없이) 으로 실행되어야 하며, 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아님을 명심!
회원 서비스 개발
- 회원 리포지토리와 도메인을 활용해서 실제 비즈니스 로직을 작성
비즈니스적인 용어로 개발 (join, findMembers 등으로 기획자가 이해가능하도록)
// main/java/hello/hellospring/service/MemberService.java
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// 회원 가입
public Long join(Member member) {
/* Ctrl + Alt + V 단축키
Optional<Member> result = memberRepository.findByName(member.getName()); // 같은 이름이 있는 중복 회원이 불가능하도록
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}); */
// Member member1 = result.get(); // 만약 같은 이름의 멤버가 있어서 바로 꺼낸다면 (또는 orElseGet()을 사용해 값이 있을 경우 꺼내고 아니면 else)
/* 위를 간단하게 줄인 후 이를 Ctrl + Alt + M 단축키로 메소드로 뽑아내기
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}); */
validateDuplicateMember(member); // 중복 회원 검증 메소드
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
// 전체 회원 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
회원 서비스 테스트
- 테스트를 할 클래스에서 Ctrl + Shift + T 단축키를 사용한 후, Create New Test - JUnit5를 선택하여 자동으로 생성
// test/java/hello/hellospring/service/MemberServiceTest.java
class MemberServiceTest {
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
// 테스트 메서드 하나가 끝난 후 데이터(메모리)를 clear
@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);
/*
try {
memberService.join(member2);
// fail("예외가 발생해야 합니다.");
} catch (IllegalStateException e) {
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.123"); // 실패
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); // 성공
}
*/
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
- MemberService의 MemoryMemberRepository와
MemberServiceTest의 MemoryMemberRepository는 서로 다른 인스턴스이므로 다른 DB가 될 수 있으므로
같은 리포지토리로 테스트하기 위해서 (같은 인스턴스를 쓰게 만들기 위해서) MemberService, MemberServiceTest 수정
→ 즉, MemberServiceTest에서 테스트 실행 전에 MemoryMemberRepository를 생성하고
MemberService가 new로 생성하는 것이 아닌, 외부에서 같은 리포지토리를 사용하도록 넣어줌
(Dependency Injection)
// main/java/hello/hellospring/service/MemberService.java
public class MemberService {
// private final MemberRepository memberRepository = new MemoryMemberRepository();
// MemberService의 MemoryMemberRepository와 MemberServiceTest의 MemoryMemberRepository는 인스턴스를 같게 하기 위해서
private final MemberRepository memberRepository;
// 직접 내가 new로 생성하는 것이 아니라 외부에서 넣어주도록 바꿔줌
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
// test/java/hello/hellospring/service/MemberServiceTest.java
class MemberServiceTest {
/* MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
*/
MemberService memberService;
MemoryMemberRepository memberRepository;
// 테스트 실행 전에 각각 실행하여 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 새로 맺어줌
@BeforeEach
public void beforeEach() {
// MemoryMemverRespository를 MemberService의 MemberService에 넣어주어 같은 리포지토리를 사용하도록 함
// 즉 MemberService 입장에서는 직접 new하는 것이 아니라, 외부에서 memberRepository를 넣어주므로 Dependency Injection (DI)
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
...
}
- 기존에는 MemberService가 MemoryMemberRepository를 직접 생성하게 했었던 것을
MemberRepository의 코드가 MemberService 코드를 DI (Dependency Injection) 가능하게 변경
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
↓
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
참고 영상
'Java-Spring > 스프링 입문' 카테고리의 다른 글
[스프링 입문] 스프링 DB 접근 기술 (0) | 2022.02.09 |
---|---|
[스프링 입문] 회원 관리 예제 - 웹 MVC 개발 (0) | 2022.01.21 |
[스프링 입문] 스프링 빈과 의존관계 (0) | 2022.01.20 |
[스프링 입문] 스프링 웹 개발 기초 (0) | 2022.01.16 |
[스프링 입문] 프로젝트 환경설정 (0) | 2022.01.16 |