[Spring Boot] 2. 회원 관리 예제 구현하기
by Rev_목표
우리는 먼저 회원ID(id)와 이름(name)을 지니는 Member 클래스를 통해 여러가지 기능을 구현해보고자 한다.
기능에는 회원 등록, 회원 조회 기능을 구현해 볼 것이다.
진행 순서는 다음과 같다.
- 회원 도메인&리포지토리 개발
- 회원 리포지토리 테스트
- 회원 서비스 개발
- 회원 서비스 테스트
대략적으로 하나의 기능을 만들고 -> 테스트 하는 방식으로 흘러간다.
구성
MemberService ----> MemberRepository(interface) <---- MemoryMemberRepository
클래스 의존 관계는 위와같다.
아직 데이터 저장소가 선정되지 않았다고 가정하였기 때문에, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계하였다. 또한 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소를 사용한다.
또한 MemoryMemberRepository와 MemberService에는 각각 테스트 케이스를 작성한다.
개발한 기능을 실행해서 테스트할 때 자바의 main 메소드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어려우며, 여러 테스트를 한번에 실행하기 힘들다. 그래서 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
Member
package hello.hellospring.domain;
// 회원ID와 이름 필요
// 회원 도메인 Member 클래스
public class Member {
private Long id; // 고객이 정하는 id가 아니라 시스템이 정하는 id
private String name;
// id와 name이 private이기 때문에 getter setter 생성
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;
}
}
Member.java
id와 name 변수를 각각 private로 생성해주었다.
우리는 아이디와 이름을 가진 멤버 객체를 만들 것이다.
또한 private에 접근하기 위해서 getter setter를 생성해주었다.
MemberRepository
package hello.hellospring.repository;
// repository 패키지 -> 회원 정보를 저장하는 저장소
import hello.hellospring.domain.Member; // Member.java
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
// 1. save()를 통해 회원 저장
Member save(Member member); // 회원 저장
// 2. ID로 저장된 회원 찾을 수 있음
Optional<Member> findById(Long id); // ID
// 3. 이름으로 저장된 회원 찾을 수 있음
Optional<Member> findByName(String name); // 이름
// 4. 저장된 모든 회원 리스트를 불러올 수 있음
List<Member> findAll(); // 모든 회원 list
}
MemberRepository.java
회원을 저장하고, ID나 이름으로 회원을 찾고, 모든 회원 리스트를 불러오기 위한 인터페이스를 구현하였다.
여기서 Optional이란 무엇일까?
findById 또는 findByName으로 가져올 때 만일 null 값일 때 처리하는 방법 중 하나이다. 즉, NPE(NullPointException)을 방지하기 위해 반환값을 Optional로 감싸서 가져온다.
옆에 붙어있는 <>은 무엇일까?
<>은 제네릭(Generics)이라고 하며, 객체의 타입을 지정해준다. 제네릭의 장점은 타입의 안정성을 지니며, 불필요한 형변환을 줄여 코드의 간결함을 나타낸다.
interface에 대한 자세한 설명은 따로 글을 작성해두었다.
[Java] 인터페이스(Interface)
인터페이스란? 자바에는 클래스 외에 인터페이스가 있다. 이는 자바에서 클래스들이 구현 해야하는 동작을 지정하는 용도로 사용되는 추상 자료형이다. class 대신에 interface라는 키워드를 이용
hirev.tistory.com
MemoryMemberRepository
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
// implements 를 통해 interface 사용
// Alt + Enter 단축키를 통해 interface 메소드 모두 가져오기 가능
public class MemoryMemberRepository implements MemberRepository {
// HashMap에 <key, value> 를 <Long, Member> 타입으로 저장
// 데이터를 저장하기 위한 store 이라는 데이터 저장 HashMap 생성
private static Map<Long, Member> store = new HashMap<>();
// key 값을 생성해주는 역할
private static long sequence = 0L;
// 오버라이드를 통해 interface에 있는 메소드 가져옴
// 기능 구현
@Override
public Member save(Member member) {
// sequence: id값, 멤버를 저장할 때마다 순차적으로 값을 증가하면서 저장
member.setId(++sequence);
store.put(member.getId(), member); // HashMap 삽입
return member;
}
// Optional: NPE 방지
// store 라는 저장 객체에서 id 값을 얻음
// key 값으로 찾기
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
// name 데이터 찾기
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
// store에 있는 모든 값 찾기
// store에 있는 value들(member) 반환
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
// Test 할 때마다 store를 비워주기 위함
public void clearStore() {
store.clear();
}
}
MemoryMemberRepository.java
멤버 저장소를 구현하는 코드이다. 이 클래스는 MemberRepository 인터페이스를 참조하기 때문에 인터페이스에 있는 모든 메소드를 불러와 해당 클래스에서 구현할 수 있다.
메소드를 하나씩 살펴보도록 하자.
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
제일 먼저 멤버들을 저장할 저장소가 필요하다. HashMap을 사용하였는데, 이는 <key : value>의 형태를 지닌다. 여기서는 id를 key로 사용해주고, value로 멤버 객체를 저장하기 위해 Member 타입을 지정해주었다.
사실 앞서 Member.java에서는 두 개의 변수 Long 타입 id와 String 타입 name이 저장되어 있었는데, 왜 이를 저장할 저장소를 만들어 줄 때는 <Long, Member> 형태인지 의아했었다.
해당 강의 Q&A에서 찾아본 결과, id인 key를 토대로 value인 Member 객체의 조회를 쉽게 하기 위함이었다.
이 궁금증에 대한 설명은 이 답변이 아주 이해가 잘 되게 설명 해주셨다!
https://www.inflearn.com/questions/228133
map<Long,Member>를 넣은 이유가 궁금합니다! - 인프런 | 질문 & 답변
Member class에는 long id, String name을 가지고 있어서 id값에 Long을 주는건 이해가 되는데 name값을 넣어주려면 Member가 아니라 String이 들어가야되는게 아닌가요?? 잘 이해가 안되서 설명 부탁드리겠습
www.inflearn.com
@Override
public Member save(Member member) {
// sequence: id값, 멤버를 저장할 때마다 순차적으로 값을 증가하면서 저장
member.setId(++sequence);
store.put(member.getId(), member); // HashMap 삽입
return member;
}
먼저 save() 메소드이다.
HashMap에 저장된 id와 member 객체를 넣고 member를 반환해준다. 단순히 HashMap인 store에 멤버를 save 해주는 용도이다.
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
id로 멤버를 조회하는 메소드이다. get()은 key를 통해 value를 찾을 수 있다.
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
name으로 멤버를 조회하는 메소드이다.
파라미터로 넘어온 name과 member 객체에 있는 같은 이름을 찾는다.
여기서 findAny()는 filter 조건에 일치하는 element 1개를 Optional로 리턴한다.
비슷한 것으로 findFirst()도 있는데, 기능은 findAny()와 동일하나 이는 여러 요소가 조건에 부합해도 가장 Stream의 순서에서 앞에 있는 요소를 리턴한다. 반면 findAny()는 순서와 관계없이 가장 먼저 찾은 요소를 리턴한다. 만일 찾은 값이 없다면 null 값을 반환한다.
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
store에 있는 모든 값을 찾는 메소드이다.
이는 value로 member 객체가 저장되어 있기 때문에 value만 불러주면 되서 간단하다.
MemoryMemberRepositoryTest
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.*;
public class MemoryMemberRepositoryTest {
// 테스트를 위하여 MemoryMemberRepository 객체 생성
MemoryMemberRepository repository = new MemoryMemberRepository();
// 테스트 하나 돌릴 때마다 초기화를 위하여 clearStore() 사용
// 보통 Test 메소드가 사용되고 난 후 종료되어야 할 리소스를 처리할 때 사용
@AfterEach
public void afterEach() {
repository.clearStore();
}
// Test 어노테이션은 테스트를 만드는 모듈 역할을 한다.
// @Test가 붙어있는 메소드는 main 메소드처럼 IDE로 직접 실행할 수 있는 메소드
@Test
public void save() {
Member member = new Member(); // 멤버 객체 생성
member.setName("spring"); // 이름에 spring 입력
repository.save(member);
Member result = repository.findById(member.getId()).get();
assertThat(member).isEqualTo(result);
// => 테스트 성공적: 데이터를 잘 저장하고 잘 받아오고 있음
}
@Test
public void findByName() {
// 객체 생성
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1); // ID와 멤버 저장
// 객체 생성
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2); // ID와 멤버 저장
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("spring1");
repository.save(member2);
// 저장되어 있는 데이터들 확인
List<Member> result = repository.findAll();
// 저장된 데이터의 수가 2가 맞는지 확인
assertThat(result.size()).isEqualTo(2);
}
}
MemoryMemberRepositoryTest.java
테스트를 위한 코드이다.
MemoryMemberRepository 객체를 생성하여 테스트하도록 한다.
이 테스트 코드에서 실제로 Member 객체를 생성하여 값이 올바르게 저장되고 찾을 수 있는지 테스트를 해보면 된다.
@Test
public void save() {
Member member = new Member(); // 멤버 객체 생성
member.setName("spring"); // 이름에 spring 입력
repository.save(member);
// Optional에서 값을 꺼낼 때 get()을 통하여 바로 꺼낼 수 있음
Member result = repository.findById(member.getId()).get();
assertThat(member).isEqualTo(result);
}
먼저 save()에 대한 테스트 코드이다.
객체를 생성하여 이름을 "spring"으로 하고 저장소에 저장해주었다.
findById()를 통해 id값으로 조회하여 value인 member 객체를 result 변수에 저장해주었다.
테스트를 할 때 JUnit의 assertThat 구문을 활용하였는데, 두 값이 일치하는지 확인하여 준다.
@Test
public void findByName() {
// 객체 생성
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1); // ID와 멤버 저장
// 객체 생성
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2); // ID와 멤버 저장
Member result = repository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
findByName()에 대한 테스트 코드이다.
member1과 member2 2개의 객체를 생성해보았다.
name으로 멤버 객체를 찾는 역할을 한다.
@Test
public void findAll() {
// 객체 생성
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
// 객체 생성
Member member2 = new Member();
member2.setName("spring1");
repository.save(member2);
// 저장되어 있는 데이터들 확인
List<Member> result = repository.findAll();
// 저장된 데이터의 수가 2가 맞는지 확인
assertThat(result.size()).isEqualTo(2);
}
findAll()에 대한 테스트코드이다.
객체 2개를 생성하고 저장된 데이터가 수가 2가 맞는지 확인하였다.
여기서 주의할 점은 각각의 테스트 케이스에서 만일 같은 이름의 객체를 생성하였다면, 정보가 중복으로 저장될 수 있음에 주의해야한다. 따라서 테스트가 끝날 때마다 비워주는 코드를 작성하는 것이 좋다.
@AfterEach
public void afterEach() {
repository.clearStore();
}
@AfterEach는 테스트가 끝난 후 실행하라는 것인데, 보통 Test 메소드가 사용되고 난 후 종료되어야 할 리소스를 처리할 때 사용한다.
clearStore() 함수는 MemoryMemberRepository.java에 구현해놓았다.
MemberService
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;
// Test 파일 만들 때 단축키: 클래스 안에서 Ctrl + Shift + T
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// 회원 가입
// 만일 같은 이름의 회원 가입을 불가능하다고 가정하였을 때(중복 member X)
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
// result.get() : 그냥 단순하게 값을 꺼내고 싶다면 get() 사용 but 권장하지는 않음
memberRepository.save(member); // 멤버 이름 저장 (ID는 자동 생성)
return member.getId();
}
// 전체 회원 조회 메소드
public List<Member> findMembers() {
return memberRepository.findAll();
}
// 회원 조회 메소드
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
// 단축키를 이용해 특정 부분 메소드 만들기: Ctrl + Alt + Shift
// 중복 회원 검증 메소드
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> { // ifPresent(): 만일 값이 있다면 동작
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
}
서비스에는 회원 가입, 전체 회원 조회, ID로 회원 조회 기능을 제공한다.
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member); // 멤버 이름 저장 (ID는 자동 생성)
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> { // ifPresent(): 만일 값이 있다면 동작
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
첫 번째로 회원 가입 기능이다.
여기서 우리는 같은 이름은 회원 가입할 수 없다는 가정을 했으므로, 따로 validateDuplicateMember() 메소드를 구현하였다. 이 메소드는 중복 회원을 검증하는 역할을 한다.
ifPresent()를 사용하여 name 값이 있을 경우 지정된 상세 메세지를 가지는 IllegalStateException을 구축한다. 여기서 throw는 고의로 예외를 발생시키고자 할 때 사용한다.
그리고 중복을 검증한 후 member를 저장하도록 한다.
public List<Member> findMembers() {
return memberRepository.findAll();
}
findMembers()는 전체 회원을 조회한다.
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
findOne()은 해당 id에 맞는 member를 찾는다.
MemberServiceTest
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.Assertions;
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();
}
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("hello"); // member 객체에 이름 hello 저장
// 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);
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
// then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
MemberServiceTest.java
MemberService에 대한 테스트 코드이다.
실제 main 코드에서 한글로 된 메소드명을 쓰면 문제가 생길 수 있지만 Test 코드에서는 괜찮다.
[References]
김영한님의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
'개발 > Spring' 카테고리의 다른 글
[Spring Boot] 5. JPA 구현 (0) | 2022.08.24 |
---|---|
[Spring Boot] 4. 순수 JDBC 구현 (0) | 2022.08.23 |
[Spring Boot] 3. Spring Bean과 의존 관계 (0) | 2022.08.16 |
[Spring Boot] 1. 정적 컨텐츠, MVC, API (0) | 2022.08.08 |
[Spring Boot] 0. intelliJ에서 Spring 개발 환경 설정하기 (0) | 2022.07.25 |
블로그의 정보
Hi Rev
Rev_