티스토리 뷰
[Spring Boot] 입문 - 백엔드 개발 흐름_2 익힉기(도메인/리포지토리/서비스/테스트 케이스)
on1ystar 2022. 12. 29. 18:07스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 김영환 인프런 강의 참고
이전 포스팅 : 2022.12.28 - [프로그래밍 언어 공부/Java] - [Spring] 입문 - 백엔드 개발 흐름_1 익힉기(도메인/리포지토리/서비스/테스트 케이스)
이전 포스팅에 이어서 이번에는 회원 관리 예제의 서비스 로직을 구현해 보겠다.
- 비즈니스 요구사항 정리
- 회원 도메인과 리포티토리 만들기
- 회원 리포지토리 테스트 케이스 작성
- 회원 서비스 개발
- 회원 서비스 테스트
4. 회원 서비스 개발
서비스 클래스는 프로그램의 비즈니스 로직이 구현되는 곳이다.
/service/MemberService.java
package com.example.practice.service;
import java.util.List;
import java.util.Optional;
import com.example.practice.domain.Member;
import com.example.practice.repository.MemberRepository;
import com.example.practice.repository.MemoryMemberRepository;
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);
}
}
어찌보면 리포지토리 로직과 유사하긴 하지만, 메서드 네임이 좀 더 클라이언트에 친화적인 비즈니스 용어다.
코드를 보면 우선 저장소인 MemberRepository
는 함부로 수정되면 안되기 때문에 해당 참조 변수를 final
로 선언했다.
비즈니스 요구사항에 따라 회원가입과 조회 2가지 로직을 구현했으며, 조회에는 전체 회원 조회와 한 명만을 조회하는 2개의 로직으로 나눴다.
추가적으로 회원가입 시 중복 회원 가입을 방지하기 위해 간단한 검증 함수를 만들어 사용했다.
validateDuplicateMember
이 함수는 회원가입 하려는 회원의 이름이 저장소 안에 이미 있으면 IllegalStateException
라는 예외를 던진다. 여기서 ifPresent
메서드는 Optional
클래스 내에 정의된 인스턴스 메서드로, Optional
객체가 감싸고 있는 값이 존재하면 대입된 람다식이 실행되고 없으면(null
이면) 넘어간다.
memberRepository.findByName(member.getName())
가 Optional
객체를 반환하기 때문에, 같은 이름의 회원이 저장소에 있다면 해당 Member
객체가 Optional
에 감싸져서 반환될 것이다. 만약 없으면 null
이 감써져서 반환되어 ifPresent
가 실행되지 않고 넘어갈 것이다.
5. 회원 서비스 테스트
리포지토리와 마찬가지로 구현한 서비스 클래스 역시 테스트를 통해 검증해야 한다.
/test/java/…/service/MemberServiceTest.java
package com.example.practice.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import com.example.practice.domain.Member;
import com.example.practice.repository.MemoryMemberRepository;
public class MemberServiceTest {
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
@AfterEach
public void AfterEach() {
memberRepository.clearStore();
}
@Test
public 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);
//Then
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> memberService.join(member2));
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
@Test
public void 전체_회원_조회() {
//Given
Member member1 = new Member();
member1.setName("spring1");
Member member2 = new Member();
member2.setName("spring2");
memberService.join(member1);
memberService.join(member2);
//When
List<Member> result = memberService.findMembers();
//Then
Assertions.assertThat(result.size()).isEqualTo(2);
}
@Test
public void 회원_한명_조회() {
//Given
Member member1 = new Member();
member1.setName("spring1");
Long memberId = memberService.join(member1);
//When
Member result = memberService.findOne(memberId).get();
//Then
Assertions.assertThat(result).isEqualTo(member1);
}
}
리포지토리 테스트 때와 동일하게 서비스 클래스에 구현된 메서드들을 Assertions와 JUnit 라이브러리를 사용해 간단하게 테스트하도록 구현했다. 이때 테스트 메서드 명을 한글로 했는데, 실제 테스트 코드같은 경우에는 빌드 시 어차피 제외되기 때문에, 개발자들의 편의상 한글로 메서드 명을 작성하는 경우가 있다고 한다.
회원가입같은 경우에는 중복 회원을 검증하는 로직이 있기 때문에, 의도적으로 같은 이름을 가진 회원이 가입하는 상황을 만들어 예외도 테스트하는 것이 좋다.
assertThrows
는 첫 번째 매개변수 값으로 대입된 예외 클래스가 두 번째 매개변수 값으로 대입된 람다식에서 발생한 예외와 같은 지를 검사하는 메서드다. 이 메서드는 예외를 반환하기 때문에 이를 사용해 예외 메세지도 검증할 수 있다.
생각해 볼 문제
위 테스트 예제에서 생각해볼 문제가 하나 있다. 테스트 클래스의 인스턴스 변수를 2개 선언했다.
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
MemberServiceTest
클래스는 테스트 시 새로운 서비스와 리포지토리 인스턴스를 생성하겠다는 의미다. 그런데 위로 올라가서 MemberService
클래스를 보면 여기서도 새로운 MemoryMemberRepository
를 생성하고 있다.
private final MemberRepository memberRepository = new MemoryMemberRepository();
즉, 테스트 코드를 실행하면 MemberService memberService = new MemberService();
에 의해 서비스 인스턴스를 생성 시 내부적으로 리포지토리를 하나 생성한다. 그리고 MemoryMemberRepository memberRepository = new MemoryMemberRepository();
가 실행되면서 새로운 리포지토리를 또 하나 생성해버린다.
테스트에서 사용되고 있는 리포지토리는 서비스 클래스 내에 생성된 리포지토리 인스턴스인데, 개발자 입장에서 테스트 코드에 인스턴스 메서드를 사용해야 하기 때문에 어쩔 수 없이 새로운 리포지토리 인스턴스를 다시 생성해 사용하고 있는 것이다.
그렇다고 MemberRepository memberRepository
를 public
으로 선언하는 것은 가장 기본적인 캡슐화를 어기는 짓이다.
그럼에도 오류가 나지 않은 것은 MemoryMemberRepository
내에서 저장소로 사용되고 있는 Map
인스턴스를 static
변수(클래스 변수)로 선언해놨기 때문에 모든 MemoryMemberRepository
인스턴스가 같은 Map
인스턴스를 공유하고 있기 때문이다.
private static Map<Long, Member> store = new HashMap<>();
그럼 실제로는 문제되지 않는 것이 아니냐 할 수 있는데, 그렇지 않다. 논리상으로도 다른 인스턴스를 가지고 테스트하는 것은 말이 안되고, 로직이 복잡해 지다 보면 다른 인스턴스를 사용함으로써 어떤 문제가 생길지 모른다. 때문에 이를 해결하기 위해서는 코드를 약간 수정해야 한다.
/service/MemberService.java
// ...
public class MemberService {
// private final MemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository;
// DI를 위한 생성자
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
//...
위와 같이 MemberService
클래스에서 MemberRepository
인스턴스를 직접 생성하는 것이 아니라 생성자를 통해 인스턴스를 주입받도록 한다. 이러한 방식을 Dependency Injection(DI)라고 한다.
테스트 코드 역시 약간의 수정이 필요하다.
/test/java/…/service/MemberServiceTest.java
// ...
public class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void BeforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
// ...
@BeforeEach
는 @AfterEach
와 반대 개념인 어노테이션이다. 각각의 테스트 메서드 실행 전에 구현한 로직을 실행시켜 준다.
이로써 같은 리포지토리 인스턴스로 테스트를 수행할 수 있게 됐으며, 각 테스트를 좀 더 독립적으로 수행할 수 있다.
DI에 대한 자세한 내용은 다음 포스팅에서 다루겠다.
References
- 인프런 강의 - https://www.inflearn.com/course/스프링-입문-스프링부트
- Given-When-Then Pattern - https://brunch.co.kr/@springboot/292
- AssertJ 라이브러리 - *https://assertj.github.io/doc/*
'Spring&Spring Boot' 카테고리의 다른 글
[Spring Boot] 입문 - 회원 관리 예제(웹 MVC 개발) (0) | 2023.02.11 |
---|---|
[Spring Boot] 입문 - 스프링 빈과 컨테이너, 그리고 의존 관계(feat. IoC, DI) (0) | 2023.01.01 |
[Spring Boot] 입문 - 백엔드 개발 흐름_1 익힉기(도메인/리포지토리/서비스/테스트 케이스) (0) | 2022.12.28 |
[Spring Boot] 입문 - 스프링 웹 개발 기초 (0) | 2022.12.15 |
[Spring Boot] aws ec2에서 Spring Boot 프로젝트 환경설정(feat. Spring Boot, code-server) (0) | 2022.12.12 |
- Total
- Today
- Yesterday
- 프로그래머스
- Python Cookbook
- Thymeleaf
- Computer_Networking_A_Top-Down_Approach
- git
- git merge
- 선형 회귀
- 쉘 코드
- 방명록 프로젝트
- 운영체제 반효경
- 스프링 테스트
- 김영환
- Do it! 정직하게 코딩하며 배우는 딥러닝 입문
- JPA
- 스프링 mvc
- spring mvc
- 생활코딩 javascript
- 패킷 스위칭
- 파이썬 for Beginner 솔루션
- Spring
- 쉽게 배우는 운영체제
- jsp
- 스프링
- 파이썬 for Beginner 연습문제
- git branch
- 지옥에서 온 git
- Gradle
- 스프링 컨테이너
- Spring Boot
- Spring Data JPA
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |