반응형
- [비즈니스 요구사항 정리]
- [회원 도메인과 리포지토리 만들기]
- [회원 리포지토리 테스트 케이스 작성]
- [회원 서비스 개발]
- [회원 서비스 테스트]
비즈니스 요구사항 정리
- 데이터 : 회원ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않음(가상의 시나리오) - 그러므로 리포지토리는 인터페이스로 만든다
- 컨트롤러 : 웹 MVC의 컨트롤러 역할
- 서비스 : 핵심 비즈니스 로직 구현
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인 : 비즈니스 도메인 객체 / 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
- 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
- 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민중인 상황으로 가정
- 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
- [비즈니스 요구사항 정리]
- [회원 도메인과 리포지토리 만들기]
- [회원 리포지토리 테스트 케이스 작성]
- [회원 서비스 개발]
- [회원 서비스 테스트]
회원 도메인과 리포지토리 만들기
회원 도메인 객체
package hello.hellospring.domain;
public class Member {
private Long id; //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;
}
}
회원 리포지토리 인터페이스
// repository/MemberReposotpry
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< > 이란?
- 개발을 할 때 가장 많이 발생하는 예외 중 하나가 바로 NPE(NullPointerException)이다. NPE를 피하려면 null 여부를 검사해야 하는데, null 검사를 해야하는 변수가 많은 경우 코드가 복잡해지고 번거롭다.
- Optional<T>는 null이 올 수 있는 값을 감싸는 Wrapper 클래스로, 참조하더라도 NPE가 발생하지 않도록 도와준다.
- Optional 클래스는 value에 값을 저장하기 때문에 값이 null이더라도 바로 NPE가 발생하지 않으며, 클래스이기 때문에 각종 메소드를 제공해준다.
- Optional 정리
- Optional은 null 또는 값을 감싸서 NPE(NullPointerException)로부터 부담을 줄이기 위해 등장한 Wrapper 클래스이다. Optional은 값을 Wrapping하고 다시 풀고, null 일 경우에는 대체하는 함수를 호출하는 등의 오버헤드가 있으므로 잘못 사용하면 시스템 성능이 저하된다. 그렇기 때문에 메소드의 반환 값이 절대 null이 아니라면 Optional을 사용하지 않는 것이 좋다. 즉, Optional은 메소드의 결과가 null이 될 수 있으며, null에 의해 오류가 발생할 가능성이 매우 높을 때 반환값으로만 사용되어야 한다. 또한 Optional은 파라미터로 넘어가는 등이 아니라 반환 타입으로써 제한적으로 사용되도록 설계되었다. (출처 - https://mangkyu.tistory.com/70)
구현체 만들기 - MemeryMemberRepository
// repository/MemoryMemberRepository
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<>(); // 메모리를 저장하기 위한 Map
private static long sequence = 0L; // ID값 생성을 위한 sequence
@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());
}
}
- 실무에서는 동시성 문제가 있을 수 있어서 위의 예제처럼 공유되는 변수일 경우에는 ConcurrnetHashMap을 사용해야 한다. 여기서는 간단한 예제이기 때문에 HashMap을 사용했다.
- sequence 객체 : 순차적으로 정수값을 자동으로 생성하는 객체 ex) 0, 1, 2 이런식으로 키값을 생성해준다.
- 이 또한 실무에서는 동시성 문제가 있을 수 있어서 AtomicLong을 사용한다.
맵(Map)에 대해 잘 모르겠다면 참고해보자
2023.05.11 - [분류 전체보기] - 맵(Map)이란 무엇일까
맵(Map)이란 무엇일까
맵(Map) "사람"을 예로 들면 누구든지 "이름" = "홍길동", "생일" = "몇 월 며칠" 등으로 구분할 수 있다. 자바의 맵(Map)은 이러한 대응관계를 쉽게 표현할 수 있게 해 주는 자료형이다. 이것은 요즘 나
blogimadetosee.tistory.com
- [비즈니스 요구사항 정리]
- [회원 도메인과 리포지토리 만들기]
- [회원 리포지토리 테스트 케이스 작성]
- [회원 서비스 개발]
- [회원 서비스 테스트]
회원 리포지토리 테스트 케이스 작성
개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나. 웹 애플리케이션의 컨트롤러를 통해
서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번
에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
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() {
// given
Member member = new Member();
member.setName("spring");
// when
repository.save(member);
// then
Member result = repository.findById(member.getId()).get();
assertThat(member).isEqualTo(result);
}
@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);
}
}
위 코드에서 아래 코드가 없다면 오류가 난다.
@AfterEach
public void afterEach() {
repository.clearStore();
}
- 테스트는 순서를 보장하지 않는다. ex) 테스트를 실행했는데 실행 순서가 findAll() → findByName() → save() 순서로 되면 오류가 난다.
- 그러므로 테스트가 하나 끝나고 나면 데이터를 깔끔하게 clear 해줘야 한다.
- → 메소드 별로 따로 동작하게 설계해야한다.
- MemoryMemberRepository에도 다음 메소드를 추가해준다.
public void clearStore() {
store.clear();
}
- @Test : 본 어노테이션을 붙이면 Test 메서드로 인식하고 테스트 한다.
- @AfterEach : 하나의 메소드의 동작이 끝날 때마다 실행하도록 하는 메소드이다.
- - ex) save()를 실행하고 afterEach()실행 → findByName()를 실행하고 afterEach()실행 → fidnAll()를 실행하고 afterEach()실행
- @Assert : 유연한 테스트 인터페이스를 제공하며 테스트코드 작성에 있어서 높은 가독성과 쉬운 유지보수성를 지향한다.
참고
TDD란
- 테스트 주도 개발(test-driven development, TDD)은 소프트웨어 개발 방법론 중의 하나로, 선 개발 후 테스트 방식이 아닌 선 테스트 후 개발 방식의 프로그래밍 방법을 말한다. 다시 말해 먼저 자동화된 테스트 코드를 작성한 후 테스트를 통과하기 위한 코드를 개발하는 방식의 개발 방식을 말한다.
- 위 예제에서는 TDD가 아닌 일반적인 순서의 방법으로 작성하였다.
- [비즈니스 요구사항 정리]
- [회원 도메인과 리포지토리 만들기]
- [회원 리포지토리 테스트 케이스 작성]
- [회원 서비스 개발]
- [회원 서비스 테스트]
회원 서비스 개발
- 회원 도메인과 회원 리포지토리를 활용해서 실제 비즈니스 로직을 작성한다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
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();
}
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);
}
}
- IllegalStateException : 부정 또는 올바르지 않은 때에 메소드가 불려 간 것을 나타낸다.
- [비즈니스 요구사항 정리]
- [회원 도메인과 리포지토리 만들기]
- [회원 리포지토리 테스트 케이스 작성]
- [회원 서비스 개발]
- [회원 서비스 테스트]
회원 서비스 테스트
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
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 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("hello");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), 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 = assertThrows(IllegalStateException.class,() -> memberService.join(member2));//예외가 발생해야 한다.
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
- 테스트는 메서드명을 한글로 작성해도 된다. 실무에서도 영어권 사람들과 같이 협업하는게 아닌 이상 한글로 작성해서 많이 사용한다. + 빌드될 때 테스트 코드는 실제 코드에 포함되지 않는다.
- @BeforeEach : 본 어노테이션을 붙인 메서드는 테스트 메서드 실행 이전에 수행된다.
- 테스트 만드는 단축기 : Shift+Ctrl+T
- 회원가입에선 예외가 정말 중요하다.
출처 : 인프런 - 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 김영한
'SPRING > 스프링 입문 (김영한 강사님)' 카테고리의 다른 글
AOP (2) | 2023.05.10 |
---|---|
스프링 DB 접근 기술 (0) | 2023.05.10 |
회원 관리 예제 - 웹 MVC 개발 (1) | 2023.05.03 |
스프링 빈과 의존관계 (1) | 2023.05.03 |
스프링 웹 개발 기초 (0) | 2023.05.02 |