[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
글에서 나오는 모든 코드와 사진들은 김영한님의 인프런 스프링 입문 강의에서 가져온 것임을 미리 알립니다.
회원 관리 예제 - 백엔드 개발
비지니스 요구사항 정리
비지니스 요구사항은 가장 쉽게 구성되어있다. 그래서 데이터, 기능 모두 간단하게 구성되어있다.
DB는 아직 정해지지 않은 상황
- 컨트롤러: 웹 MVC의 컨트롤러 역할
- 서비스: 핵심 비즈니스 로직 구현
- 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨
회원 정보를 저장하는 MemberRepository는 인터페이스로 설계되어있음
=> 왜? 아직 데이터 저장소가 선정되지 않았기 때문에 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소를 사용하고, 추후에 DB가 정해지는 경우 DB에 맞춰서 바꿔서 사용할 수 있도록
회원 도메인과 리포지토리 만들기
회원 객체(Member.java)
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;
}
}
domain package를 생성해서 회원 정보를 담는 객체를 생성해준다.
회원 리포지토리 인터페이스 (MemberRepository.java )
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();
}
repository 패키지를 생성하고 회원의 정보를 저장할 리포지토리 인터페이스를 생성해준다. 4가지 기능 선언해준다.
여기서 Optional이란? 자바 8에 들어간 기능으로 반환값이 null인 경우가 있는데 요즘에는 null이 아닌 Optional로 null을 감싸서 반환해서 사용한다고 한다.
같은 패키지 안에 구현체를 생성해준다.
회원 리포지토리 메모리 구현체 (MemoryMemberRepository)
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.stereotype.Repository;
import java.util.*;
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); //멤버를 저장할 때 일련번호 값을 1 증가 시켜주기. id setting
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); //만약 null을 반환할 경우를 대비해서 Optional을 사용해서 감싸준다.
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name)) //같은 name을 가지고 있는 객체를 찾으면 반환. 없으면 null반환
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore(){
store.clear();
}
}
회원 리포지토리 테스트 케이스 작성
내가 구현한 코드가 정상적으로 동작하는지 확인하기 위해서 테스트케이스를 작성한다. 자바는 JUnit이라는 프레임 워크로 테스트 코드를 실행한다.
기존에 작업했던 main 폴더가 아닌 test 폴더더에 새로운 패키지를 선언해준다.
테스트 파일은 보통 테스트하고자 하는 파일명+Test를 붙여서 생성하기 때문에 MemoryMemberRepositoryTest 클래스를 생성해준다.
테스트는 @Test 라는 어노테이션을 사용하면 된다. 그리고 테스트하고자 하는 내용을 입력해준다.
save 메소드 테스트 예시
@Test
public void save(){
Member member = new Member();
member.setName("spring"); //이름 설정
repository.save(member); //멤버 저장
Member result = repository.findById(member.getId()).get(); //저장되었는지 아이디로 멤버를 찾기
//Assertions.assertEquals(member, result); //[검증 1] 두개의 객체가 동일한지
assertThat(member).isEqualTo(result); //[검증 2] 두 객체가 동일한지 확인
}
테스트가 성공하면 다음과 같이 초록불이 뜬다.
만약 실패한다면 다음과 같이 에러가 발생한다. null를 넣어서 의도적으로 실패가 뜨도록 한 결과이다.
테스트 클래스를 실행하면 전체 test 메소드를 실행할 수 있다. 실행 결과는 모두 성공한다면 다음과 같이 나온다.
그러나 여기서 주의해야할 점은 전체를 실행할 경우 이 메소드의 실행순서는 코드가 작성된 순서로 실행되는것을 보장하지 않는다. 실행되는 메소드의 순서가 랜덤이다.
그래서 테스트 메소드 한 개가 끝나고 나면 데이터를 초기화를 시켜줘야지 올바르게 테스트가 가능하다.
초기화를 시켜주는 방법은 @AfterEach 어노테이션을 사용해서 구현할 수 있다.
Test클래스 안에 다음과 같이 선언해준다.
@AfterEach
public void afterEach(){
repository.clearStore(); //각 테스트 종료시에는 store를 clear해야함
}
대신에 변수 repository의 클래스인 MemoryMemberRepository 클래스에는 clear를 해주는 함수가 있어야한다.
public void clearStore(){
store.clear();
}
이렇게 코드를 작성하면 하나의 테스트가 끝날때마다 afterEach함수를 실행해서 테스트하는데 문제가 발생하지 않는다.
@AfterEach 어노테이션을 사용하면 하나의 테스트가 끝날 때마다 실행하는 콜백 메소드를 지정할 수 있다.
지금은 구현 => 테스트 순서로 진행했는데 만약 이 순서를 바꿔서 테스트 => 구현 순서로 진행한다면 이 과정을 TDD라고 부른다.
test 클래스 전체 코드
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach(){
repository.clearStore(); //각 테스트 종료시에는 store를 clear해야함
}
@Test
public void save(){
Member member = new Member();
member.setName("spring"); //이름 설정
repository.save(member); //멤버 저장
Member result = repository.findById(member.getId()).get(); //저장되었는지 아이디로 멤버를 찾기
//Assertions.assertEquals(member, result); //[검증 1] 두개의 객체가 동일한지
assertThat(member).isEqualTo(result); //[검증 2] 두 객체가 동일한지 확인
}
@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(); //spring1 이라는 이름으로 객체 찾기
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); //전체 목록의 개수가 총 2개인지 확인
}
}
회원 서비스 개발
service 패키지를 생성하고 서비스 클래스를 만들어준다.
MemberService 클래스 안에 내용은 다음과 같다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
//service클래스에는 비지니스에 가까운 기능을 구현하기 때문에 비지니스와 관련된 용어를 사용
public class MemberService {
private final MemberRepository memberRepository;
//다음과 같이 외부에서 객체를 생성해서 넣어주는 것을 Denpendency Injection(DI)라고 함.
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
/**
* 회원가입
*/
public Long join(Member member){
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m->{ //값이 있는 경우(Optional이라서 가능)
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
/**
* 전체 회원 조회
*/
public List<Member> findMember(){
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
}
회원 서비스 테스트
아까와 같은 방법으로 테스트를 하려고 한다. 이번에는 테스트 클래스를 단축키를 사용해서 만들어보려고 한다.
단축키는 window 기준으로 ctr+shift+t 를 누르면 테스트 클래스를 만들 수 있는 창이 생긴다.
Create New Test를 눌러서 창이 나오면 이름을 입력하고 테스트 클래스를 만들어준다. 그러면 아까 테스트 파일 생성했던 그 폴더에 자동으로 service 폴더가 생성되고 그 안에 테스트 클래스가 생성되어있을 것이다.
테스트 코드는 한글로 메소드를 작성해도 무방하다!
테스트 코드를 작성할 때는 given//when//then 주석을 미리 세팅하고 작성하면 테스트 코드를 작성하기 쉬워진다.
예시로 회원가입 하는 테스트 코드를 작성해준다.
@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("이미 존재하는 회원입니다."); //예외 메시지
//try-catch를 이용한 예외
// try {
// memberService.join(member2);
// fail();
// }catch (IllegalStateException e){
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// }
//then
}
예외 처리는 try-catch문으로 작성할 수 있지만 assertThrows를 사용해서 작성해줄 수도 있다.
'개발 공부 > Spring' 카테고리의 다른 글
[Spring] 인프런 스프링 입문 강의 정리 #5 (0) | 2022.06.26 |
---|---|
[Spring] 인프런 스프링 입문 강의 정리 #4 (0) | 2022.06.25 |
[Spring] JAVA에서 Spring으로 변환 정리 (개인 공부 정리용) (0) | 2022.04.22 |
[Spring] 인프런 스프링 입문 강의 정리 #2 (0) | 2022.04.17 |
[Spring] 인프런 스프링 입문 강의 정리 #1 (0) | 2022.03.04 |