[Spring]회원 관리 - 백엔드 개발
목차
- 비즈니스 요구사항 정리
- 회원 도메인과 저장소 만들기
- 회원 저장소 테스트 케이스 작성
- 회원 서비스 개발
- 회원 서비스 테스트
📎 비즈니스 요구사항
- 데이터 : 회원 ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소가 정해지지 않음(가상의 시나리오)
📎 회원 도메인과 저장소 만들기

우선, 현재 진행중인 프로젝트 디렉토리 안에 domain이라는 패키지를 하나 만들어 준다. 이 패키지 안에 사용자 정보를 담을 수 있는 자바 클래스 Member.java를 만들어 준다.
여기에는 아래와 같이 필요한 변수를 선언하고 setter과 getter를 추가 해준다.
package Jihoo.hello_spring.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;
}
}
참고로 여기서의 id는 회원가입 당시 회원이 정하는 id가 아니라, 데이터베이스에 있는 회원 정보들중 회원을 쉽게 찾아내기 위해 만들어진 id 이다.
다음으로, 회원 저장소를 담당할 repository라는 패키지를 하나 더 만들어주고, 그 안에 다음 이미지와 같이 MemberRepository 라는 Interface를 추가 해준다.

이제 MemberRepository 인터페이스에는 아래와 같이 코드를 작성 해준다.
package Jihoo.hello_spring.repository;
import Jihoo.hello_spring.domain.Member;
import java.util.Optional;
import java.util.List;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(Long id);
List<Member> findAll();
}
오랜만이다! 자바의 인터페이스. 처음 자료구조 수업을 들을 때 인터페이스에 대해 헷갈렸었는데, 까먹은 내용을 다시 기억하고자 자바의 인터페이스에 대해 정리해 보고자 한다.
자바의 인터페이스(Interface)란,
추상 메서드의 집합을 정의하는 추상 타입이다. MemberRepository 인터페이스를 보면 모든 메서드가 body 없이 선언만 되어 있는 걸 볼 수 있다. 이게 인터페이스의 핵심이다.
인터페이스의 주요 특징은 다음과 같다:
- 추상 메서드만 포함
- 다중 상속 가능
- 느슨한 결합 제공
- 구현 클래스를 위한 계약 정의
MemberRepository 인터페이스를 보면 save(), findById(), findByName(), findAll() 메서드가 선언되어 있다. 이 메서드들은 Member 객체를 다루는 기본적인 CRUD 연산을 정의하고 있다.
인터페이스를 사용하면 실제 구현과 명세를 분리할 수 있어서 코드의 유연성과 확장성이 높아진다. 예를 들어, MemberRepository의 실제 구현을 데이터베이스나 파일 시스템 등 다양한 방식으로 할 수 있다. 그래도 인터페이스를 사용하는 코드는 변경할 필요가 없으니 편리하다.
이렇게 인터페이스를 사용하면 코드의 구조를 더 깔끔하게 만들 수 있고, 나중에 유지보수하기도 쉬워진다.
이제, 이 인터페이스를 가진 구현체를 만들어 보자.
src/repository 안에 MemoryMemberRepository 라는 자바 클래스를 하나 만들어 준다.
package Jihoo.hello_spring.repository;
import Jihoo.hello_spring.domain.Member;
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);
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());
}
}
먼저, 회원들을 저장할 저장소 store를 선언해주고, id를 담을 sequence라는 변수를 선언 해준다.
이제, Override한 메서드들을 구현 해줄 차례인데, 먼저
- save : 리턴 타입은 Member 객체이고, 입력값으로 받은 member의 아이디를 설정해주고, 저장소에 회원을 담아준다.
- findById : findById 메서드는 저장소(store)에서 입력받은 id에 해당하는 Member 객체를 찾아 반환한다. Optional을 사용해 null 처리를 더 안전하게 한다. 이렇게 하면 NullPointerException을 방지하고, 메서드를 호출하는 쪽에서 값의 존재 여부를 쉽게 확인할 수 있다.
- findByName : store는 Map 타입의 컬렉션이다. values() 메서드를 호출하면 store에 저장된 모든 값(Member 객체)을 포함하는 Collection을 반환한다. stream() 메서드는 이 Collection을 스트림으로 변환한다. 스트림은 Java 8에서 도입된 기능으로, 컬렉션 데이터를 함수형 스타일로 처리할 수 있게 한다.
- findAll : store라는 Map 컬렉션에 저장된 모든 Member 객체를 List로 반환한다. store는 Map<Long, Member> 타입의 컬렉션 이다. store.values()를 호출하면 store에 저장된 모든 값(Member 객체)을 포함하는 Collection<Member>을 반환한다.
이 Collection<Member>는 Map의 값들만을 포함하는 뷰다. new ArrayList<>(store.values())는 store의 값들을 포함하는 새로운 ArrayList<Member> 객체를 생성한다. 이 과정에서 store.values()로부터 반환된 Collection의 모든 요소가 ArrayList에 복사된다.
이렇게 하면 원본 store의 값들에 영향을 주지 않고, 독립적인 리스트를 반환할 수 있다.
자 이렇게, 구현체를 구현했다. 이제 구현체를 작성했으니 이 구현체가 동작하는지 알아보기 위해 테스트 케이스를 작성 해야한다.
📎 회원 저장소 테스트 케이스 작성
자바의 가장 유명한 테스트 케이스 프레임워크인 JUnit 이라는 프레임워크를 사용 할것이다. 먼저, src/test/java 하위 폴더에 테스트 케이스를 작성한다. 테스트 케이스들을 쓸 수 있는 디렉토리가 따로 있다. 테스트 케이스는 꼭 여기에다가 작성 해야 한다는것을 잊지 말자!

아래는 작성한 테스트 케이스 코드이다.
package Jihoo.hello_spring.repository;
import Jihoo.hello_spring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@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);
}
@Test
public void findByName() {
Member m1 = new Member();
m1.setName("spring1");
repository.save(m1);
Member m2 = new Member();
m2.setName("spring2");
repository.save(m2);
Member result = repository.findByName("spring1").get();
Assertions.assertEquals(result, m1);
}
}
- @AfterEach: 각 테스트 메서드가 실행된 후에 실행되는 메서드를 지정한다. 어제 배운 어노테이션이 생각난다.
- afterEach(): 각 테스트가 끝난 후 repository의 저장소를 초기화하는 메서드이다. 이를 통해 각 테스트 간의 데이터를 독립적으로 유지할 수 있다.
- @Test: 이 메서드가 테스트 메서드임을 나타낸다.
- save(): MemoryMemberRepository의 save 메서드를 테스트한다.
- Member 객체를 생성하고 이름을 설정한다.
- repository.save(member)를 호출하여 Member 객체를 저장소에 저장한다.
- repository.findById(member.getId()).get()를 호출하여 저장된 Member 객체를 조회한다.
- Assertions.assertEquals(member, result)를 사용하여 저장된 객체가 원래 객체와 동일한지 검증한다.
아래는 테스트 케이스를 실행한 후의 콘솔 모습이다. 콘솔에 따로 출력되는 내용은 없으며, 아래 이미지와 같이 케이스 통과시 초록색 체크표시가 뜨면서 테스트를 마무리 한다.

📎 회원 서비스 개발
이제, 회원 서비스를 구현할 차례이다. 우선, 프로젝트 디렉토리에 새로운 패키지인 'service'라는 패키지를 만들어 준다.

이렇게 service라는 패키지를 하나 만들고, 그 service라는 패키지 안에 MemberService라는 클래스를 하나 만들어 준다.
아래는 MemverService 클래스이다.
package Jihoo.hello_spring.service;
import Jihoo.hello_spring.domain.Member;
import Jihoo.hello_spring.repository.MemberRepository;
import Jihoo.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
/**
* 회원 가입
*/
public Long join(Member member) {
//같은 이름이 있는 중복 회원 x
Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
memberRepository.save(member);
return member.getId();
}
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
회원 서비스는 기본적으로 3가지 기능이 있다.
- 회원 가입 (join)
- 모든 회원 조회하기(findMembers)
- 입력값으로 멤버 Id를 던져주면 그 Id에 맞는 회원 1명 찾기(findOne)
코드를 좀 더 살펴보자.
- 먼저, 회원가입을 할때, 중복된 이름이 있으면 회원가입이 되지 않도록 하는 코드를 작성해야 한다.
result 라는 변수에 입력값으로 member가 주어지면 저장소에서 그 멤버의 이름을 검색해 이미 같은 이름이 존재한다면(result.ifPresent()), 예외 처리를 하여 회원 가입을 하지 못하도록 한다.
만약, 예외가 발생하지 않는다면(중복되는 이름이 없다면), memberRepository에 member를 저장하고, 그 멤버의 멤버 id를 리턴한다.
나머지 메서드들은 직관적이라 설명을 생략 하겠다.
이런 식으로 회원 서비스를 구현 해주었다. 이제 회원 서비스 로직을 구현했으니, 다음은 이 회원 서비스 로직을 테스트 할 수 있는 테스트 케이스를 작성 해야한다. 아까 언급한 내용처럼 Spring의 테스트 케이스는 모두 src/test 디렉토리에 작성 해주어야 한다.
📎 회원 서비스 테스트
아까와 달리 테스트 케이스를 좀 더 편리하게 해주는 단축키가 있다. 테스트를 하려는 클래스(여기서는 MemberService.java)에서
(Ctrl + Shift + T)를 누르면 아래와 같이 테스트 케이스 클래스를 바로 만들어주는 툴이 등장한다. 이걸로 테스트 케이스를 작성 해보자.

package Jihoo.hello_spring.service;
import Jihoo.hello_spring.domain.Member;
import Jihoo.hello_spring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
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 join() {
// given
Member m = new Member();
m.setName("hello");
// when
Long saveId = memberService.join(m);
// then
Member findMember = memberService.findOne(saveId).get();
assertEquals(m.getName(), findMember.getName());
}
@Test
void findMembers() {
// given
Member m1 = new Member();
m1.setName("member1");
memberService.join(m1);
Member m2 = new Member();
m2.setName("member2");
memberService.join(m2);
// when
List<Member> members = memberService.findMembers();
// then
assertEquals(2, members.size());
assertTrue(members.contains(m1));
assertTrue(members.contains(m2));
}
@Test
void findOne() {
// given
Member m = new Member();
m.setName("uniqueMember");
Long saveId = memberService.join(m);
// when
Optional<Member> findMember = memberService.findOne(saveId);
// then
assertTrue(findMember.isPresent());
assertEquals(m.getName(), findMember.get().getName());
}
}
@BeforeEach는 각 테스트 메서드 실행 후에 호출된다. MemoryMemberRepository의 저장소를 초기화하여 테스트 간의 데이터가 격리되도록 한다. 이렇게 추가하면 각 테스트 메서드가 실행될 때마다 새로운 MemberService와 MemoryMemberRepository 인스턴스가 생성되고, 테스트가 끝날 때마다 저장소가 초기화되어 다른 테스트에 영향을 미치지 않도록 한다.
이처럼 오늘은 스프링으로 회원 서비스 로직을 구현 해보았다. 테스트 케이스는 작성되는 디렉토리가 다르고, 테스트 케이스는 build 당시 소스 코드 내부에 포함되지 않는다는 점을 기억하자.