비즈니스 요구사항 정리
비즈니스 요구사항은 단순하게 할 것
데이터: 회원ID, 이름
기능: 회원 등록, 조회
상황: 아직 DB 선정이 안됐는데 개발을 해야함
웹 어플리케이션 구조
컨트롤러: 웹 MVC의 컨트롤러 역할
서비스: 핵심 비즈니스 로직 구현(회원은 중복 가입이 안된다는 코드 구현 등)
리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
클래스 의존관계
회원은 interface로 설계(DB가 선정되지 않았기 때문), 구현체는 메모리 모드로 단순하게 저장 -> 향후에 구체적인 DB가 선정된다면 바꿔뀌기 위해 인터페이스로 구현
회원 도메인과 리포지토리 구현
domain package 만들고, Member Class 만들기
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;
}
}
Id, name, Getter Setter까지 만듦
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
repository package만든 후, interface로 MemberRepository 만들기
기능은 save, findById, findByName, findAll이 있음
Optional은 DB에서 가져올때, Null이 올 수 있는데, 이때 Optional로 감싸서 가져옴
package hello.hello_spring.repository;
import hello.hello_spring.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);
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());
}
public void clearStore() {
store.clear();
}
}
이후 구현체 만들기
Class MemoryMemberRepository에서 implememts한 후에 구현
private static으로 공통 변수를 사용, 실전에서는 동시성 문제가 있을 수 있어서 automic 등을 사용해야함
save에서는 store에 member를 저장함
find~ 메소드는 store에서 가져옴
이때, null이 반환될 수 있으므로 optional로 감싸서 반환
findByName에서는 lamda(stream)를 써서, loof를 돌림, findAny는 하나 있으면 바로 반환
이 동작이 잘 되는지 검증하기 위해 test case를 작성해야함
회원 리포지토리 테스트 케이스 작성
main 함수를 실행해보거나 하는 테스트 방법은 준비하는데 오래 걸리고, 반복 실행이 어려운 단점이 있다.
그래서 자바는 JUnit 프레임워크로 테스트를 실행한다.
src가 아니라, test 쪽에 기존과 같은 package이름으로 만든 후, Test Class를 만든다.
@Test 어노테이션으로 지정하면, 메소드를 실행할 수 있음
기존에 서버를 실행시켰던건 종료하고, Test code를 실행함
ctrl + shift + enter로 한 문단 중간에서 다음 문단으로 넘어갈 수 있음
memoryMember에서 get으로 가져온 값이랑, 우리가 넣어준 값이랑 일치한지 확인
-> println으로 계속 로그 추적을 할 순 없으니까, Assertions 기능을 이용함
-> 출력되는 건 없지만, 실행 결과가 녹색임
기대값과 다르게 null을 넣으면, 오류가 남
assertj의 Assertions 사용하면 더 편하게 사용할 수 있음
assertThat으로 사용함
shift+F6으로 rename이 가능
Test Case의 장점은 package 내의 TestCode를 모두 실행시킬 수 있음(순서와 상관 없이, 메소드 실행 순서는 랜덤함)
-> 모든 메소드는 순서 상관없이 실행되어야 함
findAll에서 이미 save로 memoryRepository에 저장을 했기 때문에 다른 메소드에서 get했을 때 다른 객체가 나와버려서 equal에서 오류남
-> 테스트 코드마다 테스트가 끝나면 clear를 해줘야 함
-> @AfterEach 사용, 메소드 동작이 끝날때마다 해당 메소드를 실행함
test Class를 먼저 작성한 다음에, 실제 구현체를 작성할 수도 있음
-> 테스트 주도 개발(TDD)
테스트가 만약에 수십, 수백개라면? gradle에서 test를 다 해줌
전체 코드
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@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();
assertThat(result).isEqualTo(member);
}
@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);
}
}
회원 서비스 개발
service: 비즈니스 로직 담당
service를 만드려면 repository에서 가져와야 함,
회원가입
save 호출, member.id를 반환
로직 중에, 같은 회원 이름이 있으면 안된다는 룰이 있다면,
findByName으로 만약 name이 이미 있다면 회원가입을 못하게 해야함
ctrl+V로 return 형식으로 문장 변형 가능 (하지만 Optional을 바로 반환하는 건 예쁘지 않음)
-> chain(.)으로 바로 ifPresent 사
ifPresent는 값이 있으면 로직이 동작함
-> Optional이기 때문에 사용할 수 있는 함수
이러한 로직을 작성했을 경우, 로직들은 메소드로 뽑아내는 게 좋음(ctrl + m)
전체 회원 조회
service 클래스는 비즈니스에 의존적으로, 메소드 이름을 비즈니스 용어로 사용해야 역할의 느낌을 낼 수 있음
회원 서비스 테스트
MemberService class에서 ctrl + shift + T를 하면 create new test 창이 뜨고, create하면 똑같은 패키지에 만들어지는 것을 확인할 수 있음
테스트는 한글로 바꿔도 됨, 빌드될 때 테스트 코드는 포함되지 않음
given, when, then 문법
주석으로 given, when, then 구분
예외 flow가 훨씬 중요함
중복 회원 검증 로직이 동작하는지도 검증
try, catch로 가능
하지만 다른 방법이 있음
assertThrows(IllegalStateException.class, 로직)
메시지 검증은 return 타입으로 나옴
memberService 안에 memberRepository가 이미 new로 선언 되어 있는데, 테스트 코드에서도 new로 생성해놓음
두 개를 생성하는 게 내용물이 달라질 수 있음, DB가 달라지기 때문
같은 인스턴스를 사용하게 하려면, MemberService 안의 new 객체 생성을 constructor 안에 넣어주면 됨
-> 외부에서 넣어주는 방식으로, beforeEach에서 진행
beforEach라 테스트를 생성할 때마다 각자 service에 repository를 넣어줌(독립적 실행 위함)
-> 같은 service를 바라보게
-> dependency injection
전체 코드
package hello.hello_spring.service;
import hello.hello_spring.domain.Member;
import hello.hello_spring.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;
public class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memoryMemberRepository;
@BeforeEach
public void beforeEach() {
memoryMemberRepository = new MemoryMemberRepository();
memberService = new MemberService(memoryMemberRepository);
}
@AfterEach
public void afterEach() {
memoryMemberRepository.clearStore();
}
@Test
public void 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memoryMemberRepository.findById(saveId).get();
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() throws Exception {
//Given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
IllegalStateException e = org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class,
() -> memberService.join(member2)); // 예외가 발생해야 한다.
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
'Spring > 스프링 입문' 카테고리의 다른 글
스프링 웹 개발 기초 (0) | 2025.02.27 |
---|---|
View 환경설정 (0) | 2025.02.18 |
라이브러리 (0) | 2025.02.18 |
Spring 환경 세팅하기 (1) | 2025.02.17 |