728x90
반응형
비즈니스 요구사항 정리
- 데이터 : 회원ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않았다고 가정
일반적인 웹 어플리케이션 계층 구조
컨트롤러 | 웹 MVC의 컨트롤러 역할 |
서비스 | 핵심 비즈니스 로직 구현 |
리포지토리 | 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리 |
도메인 | 비즈니스 도메인 객체 ex) 회원, 주문, 쿠폰 등 주로 데이터베이스에 저장하고 관리됨 |
클래스 의존관계
- 아직 데이터 저장소가 선정되지 않아 우선 인터페이스로 구현하여 추후 클래스를 변경할 수 있도록 설계
- 데이터 저장소는 RDB, NoSQL 등 다양한 저장소를 고민 중인 상황으로 가정
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
회원 도메인과 리포지토리 만들기
회원 객체(src/main.java.hello.hellospring/domain/Member)
package hello.hellospring.domain;
public class Member {
private Long id; // 고객이 정한 id가 아닌 시스템이 정한 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;
}
}
회원 리포지토리 인터페이스(src/main.java.hello.hellospring/repository/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); // id로 회원 정보 찾기
Optional<Member> findByName(String name); //name으로 회원 정보 찾기
List<Member> findAll(); // 입력된 모든 회원 정보 반환
}
Optional은 자바8부터 들어간 기능으로써 가져오는 값이 Null일 경우 Optional로 감싸져서 Null값 반환
회원 리포지토리 메모리 구현체
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
/**
* 동시성 문제가 고려되어 있지 않음, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
*/
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); // 저장할 때 sequence값 +1
store.put(member.getId(), member); // 저장소에 데이터 저장
return member;
}
@Override
public Optional<Member> findById(Long id) {
// 값이 Null일 경우 Optional에 감싸져서 반환
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream() // 저장소에 값 내에서 아래 코드를 반복
.filter(member -> member.getName().equals(name)) // 값이 같은 경우만 filter
.findAny(); // 값을 찾으면 반환
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
// 테스트 케이스에 사용
public void clearStore(){
store.clear();
}
}
회원 리포지토리 테스트 케이스 작성
개발한 기능을 실행해서 테스트할 때 자바의 main 메소드를 통해서 실행하거나, 웹 어플리케이션의 컨트롤러를 통해서 해당 기능을 실행하는 것은 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
회원 리포지토리 메모리 구현체 테스트
src/test/java/hello.hellospring/repository/MemoryMemberRepositoryTest
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository 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 사용은 좋은 건 아니지만 test니까 OK
Member result = repository.findById(member.getId()).get();
/**
방법1
System.out.println("result = " + (result == member));
방법2
Assertions.assertEquals(member, result);
*/
assertThat(member).isEqualTo(result);
}
@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();
/*
Assertions.assertThat(member).isEqualTo(result); 후에
(Assertions는 assertj로 해주세요)
Assertions에 alt+enter하면 아래와 같이 바뀌고 import됩니다.
*/
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);
}
}
각각 test 메소드를 실행해서
- 아무 이상이 없으면 초록불
- 문제가 생기면 빨간 불과 함께 오류메시지가 출력된다.
intellij는 Test 코드를 메소드별, 클래스별, 패키지별로 실행이 가능하다.
참고로 테스트 케이스를 만든 후 구현 클래스를 만드는 경우를 TTD(테스트 주도 개발)라고 부른다.
회원 서비스 개발
src/main/java/hello.hellospring/service/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){
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
// 같은 이름이 있는 중복 회원 X
// findByName 반환값이 이미 Optional이니까 굳이 Optional 작성안해도 됨
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);
}
}
회원 서비스 테스트
회원 서비스 코드를 DI(Dependency Injection) 가능하게
MemberService 파일에 코드를 조금 바꿔줍니다.
@RequiredArgsConstructor 어노테이션을 사용하여 DI를 가능하게 할 수 있습니다.
public class MemberService{
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
// 그 외 코드들~~~
}
회원 서비스 테스트
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
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.*;
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(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("이미 존재하는 회원입니다.");
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
TIP!
테스트 코드 작성할 때
given : ~~가 주어졌을 떄
when : ~~한 상황이면
then : ~~한 결과가 나온다
로 작성하면 효율적
TIP!
영문권 사람들과 작업하는 게 아니라면 테스트 코드에는 한글을 써도 괜찮고 어차피 빌드할 때는 포함되지 않는다.
728x90
반응형
'JAVA > [인프런] 스프링 강의들' 카테고리의 다른 글
[인프런] 스프링 입문 강의 Section5. 회원 관리 예제 - 웹 MVC 개발 (0) | 2023.07.04 |
---|---|
[인프런] 스프링 입문 강의 Section4. 스프링 빈과 의존관계 (0) | 2023.07.04 |
[인프런] 스프링 입문 강의 Section2. 스프링 웹 개발 기초 (0) | 2023.07.03 |
[인프런] 스프링 입문 강의 Section1. 프로젝트 환경 설정 (0) | 2023.07.03 |
[서버 개발 올인원 패키지] 7. 생애 최초 배포하기 (0) | 2023.04.16 |