이 글은 김영한 님의 스프링 입문 강좌 수강 후에 정리한 글입니다.
(https://www.inflearn.com/course/스프링-입문-스프링부트/dashboard)
회원관리 예제
● 비즈니스 요구 사항
데이터 : 회원 ID, 회원 이름
기능 : 회원 등록, 조회
DB : 아직 데이터 저장소가 선정되지 않음 (우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계)
● 일반적인 웹 애플리케이션 계층 구조
컨트롤러 : 웹 MVC의 컨트롤러 역할
서비스 : 핵심 비즈니스 로직 구현
리포지토리 : DB에 접근, 도메인 객체를 DB에 저장하고 관리
도메인 : 비즈니스 도메인 객체 (ex. 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨)
● 회원 객체 생성 (도메인)
package hello.hellospring.domain;
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와 회원 이름을 변수로 설정한다. 그리고 변수들을 get과 set할 수 있게 각각 getter와 setter 메소드를 만들어준다.
● 회원 리포지토리 인터페이스 (리포지토리)
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);
Optional<Member> findByName(String name);
List<Member> findAll();
void clearStore();
}
회원 도메인 객체를 DB에 저장하고 관리할 수 있는 다양한 메서드를 만든다.
리포지토리 구현체가 사용하게 될 메서드를 선언한다.
※ Optional<T> : JAVA8에서 등장한 새로운 클래스로 'T'타입 객체를 감싸는 wrapper 객체이다. 이를 사용함으로써 예상하지 못한 null값을 참조하여 NPE(NullPointerException)가 발생하는 경우를 방지할 수 있다.
● 회원 리포지토리
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
public class MemberMemoryRepository 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();
}
}
위에서 작성된 회원 리포지토리를 implements 한다.
그리고 이를 위해 회원 정보를 저장할 Map 타입 store와 회원 아이디를 지정할 Long 타입 sequence를 선언한다.
1. save 메소드
우선 저장할 member의 id를 sequence 변수를 활용하여 자동으로 0, 1, 2,...로 증가되면서 설정되게 한다.
그 후 id가 설정된 member를 store에 저장한다.
마지막으로 이 결과를 반환한다.
2. findById 메소드
해당하는 id로 저장된 member가 존재하는지 store에서 찾은 후 결과를 반환한다.
null값이 나올 수 있으므로 이를 대비하여 Optional.ofNullable()을 사용한다.
3. findByName 메소드
해당하는 name으로 저장된 member가 존재하는지 store에서 찾은 후 결과를 반환한다.
※ stream() : JAVA8에서 등장한 기능으로 컬렉션에 저장된 요소들을 하나씩 하나씩 처리하게 해 준다.
※ filter() : 해당 배열의 요소들을 조건에 따라 걸러주는 작업을 수행한다.
4. findAll 메소드
store에 저장된 모든 member들을 List로 반환한다.
5. clearStore 메소드
이후에 개발한 기능들을 테스트할 때를 대비하여 member들을 저장해놓은 store를 비워준다.
● 회원 리포지토리 테스트 케이스 작성
다양한 기능들을 개발한 후에 테스트를 진행할 때 main 메소드 상에서 진행하게 되면 준비하고 실행하는데에 오랜 시간이 걸리고, 반복 실행이 어렵고, 여러 테스트를 한번에 실행하기 어렵다는 단점이 존재한다.
이를 보완하기 위해 JUnit이라는 프레임워크로 테스트를 진행한다.
package hello.hellospring.repository;
import hello.hellospring.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.assertThat;
class MemoryMemberRepositoryTest {
MemberRepository 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);
}
}
1. afterEach 메소드
테스트를 진행하게 되면 이전의 테스트의 결과가 DB에 그대로 남아 예상치 못하게 이후의 테스트가 실패할 수 있다.
이를 방지하기 위해 @AfterEach와 clearStore 메소드를 활용하여 매번 테스트가 종료될 때마다 store를 비워주도록 한다.
2. save, findByName, findAll 메소드
Test 어노테이션이 있으므로 위 메소드들은 테스트 대상이 된다.
테스트를 진행할 땐 given(어떠한 상황이 주어짐), when(이를 실행하였을 때), then(이때 결과가 ~이 나와야 함)의 단계를 거쳐 진행될 수 있도록 한다.
assertThat(실제값).isEqualTo(기댓값)을 활용하여 '실제값 == 기댓값' 인지를 확인함으로써 기능들이 의도한 대로 잘 개발되었는지 테스트를 진행하면 된다.
● 회원 서비스 개발
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){
validateDuplicatedMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicatedMember(Member member) {
memberRepository.findByName(member.getName()).ifPresent(m->{
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
//전체 회원 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
//회원 1명 조회
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
}
회원가입, 조회와 같은 회원 관리 서비스를 구현하였다.
1. join 메소드
회원가입 기능을 하는 메소드이다.
validateDuplicatedMember 메소드를 사용하여 이미 존재하는 회원 이름인지를 검증한 후 만약 같은 이름을 가진 회원이 있다면 exception을 throw해준다.
중복된 회원이 존재하지 않는다면 DB에 회원을 저장함으로써 회원 가입을 완료한다.
2. findMembers 메소드
이미 구현한 findAll 메소드를 사용하여 전체 회원을 List로 반환한다.
3. findOne 메소드
이미 구현한 findById 메소드를 사용하여 특정 memberid를 가진 회원을 반환한다.
없는 회원 ID를 조회하여 null값이 반환될 수 있으므로 Optional<Member>를 사용하여 이를 대비한다.
회원 관리 예제 테스트
● 회원 서비스 테스트
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
우선 회원 리포지토리의 코드를 다음과 같이 변경하여 DI가 가능하도록 한다. 위 코드처럼 생성자를 이용하여 DI를 구현할 수도 있고, 메소드(특히 Setter)를 활용하여 구현할 수도 있다.
DI를 사용하는 이유에는 여러 이유들이 존재하지만, 위 코드에서 DI를 사용한 이유는 변경 전에 MemberService와 MemberServiceTest에서 생성한 memberRepository가 다른 객체기 때문이다. 원래는 둘 다 같은 DB여야 하는데 다른 DB가 돼버리는 것이다. 이를 방지하기 위해서 memberRepository를 직접 생성하는 것이 아닌 파라미터로 받아온 memberRepository를 넣어주는 방식을 사용한다.
※ DI(Dependency Injection) : 객체가 의존하는 또 다른 객체를 외부에서 선언하고 이를 주입받아 사용하는 것이다. 이를 활용하게 되면 의존성이 줄어들게 되고, 코드 재사용성이 높아지며, 테스트가 편해지고, 가독성이 높아진다는 장점이 있다. (출처: https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/)
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
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.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
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("hello");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
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));
//then
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
1. beforeEach 메소드
각 테스트가 실행하기 전에 실행되는 메소드이다. 같은 memoryRepository를 사용하기 위해서 생성한 memberRepository를 memberService의 memberRepository에 주입한다.
2. afterEach 메소드
테스트를 진행하게 되면 이전의 테스트의 결과가 DB에 그대로 남아 예상치 못하게 이후의 테스트가 실패할 수 있다.
이를 방지하기 위해 @AfterEach와 clearStore 메소드를 활용하여 매번 테스트가 종료될 때마다 store를 비워주도록 한다.
3. 회원가입 메소드
테스트 메소드이기 때문에 한국어로 메소드명을 정해도 문제가 없다. assertThat과 isEqaulTo를 사용해서 join 기능이 올바르게 구현되었는지 확인한다.
4. 중복_회원_예외 메소드
마찬가지로 테스트 메소드라서 한국어로 메소드명을 정해도 문제가 없다. member2가 회원가입을 시도하는 순간 예외가 발생하게 되는데 이때 이 예외가 중복 회원 예외인지 assertThat과 isEqualTo를 사용해서 확인함으로써 중복 회원 예외 기능이 올바르게 구현되었는지 확인한다.
※ assertThrows() : 두 가지 예외 처리 관련 기능을 할 수 있는 메소드이다.
1. 발생한 예외의 세부 사항을 확인하게 해 준다.
2. 메소드의 첫 번째 인자인 예외 타입과 두 번째 인자의 실행 결과 나타나는 예외 타입이 같은지(혹은 상속관계인지) 검사해준다.
'개발 > 스프링 입문' 카테고리의 다른 글
(스프링 입문) 6. 스프링 DB 접근 기술 (0) | 2022.09.24 |
---|---|
(스프링 입문) 5. 회원 관리 예제 (웹 MVC 개발) (0) | 2022.09.24 |
(스프링 입문) 4. 스프링 빈과 의존관계 (0) | 2022.09.24 |
(스프링 입문) 2. 스프링 웹 개발 기초 (0) | 2022.09.24 |
(스프링 입문) 1. 프로젝트 환경 설정 (0) | 2022.09.06 |