본 게시물은 김영한님의 “스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술” 강의를 바탕으로 작성했습니다.
회원 관리 예제 - 백엔드 개발
비즈니스 요구사항 정리
회원 관리 예제 프로젝트를 만들기 위해서 몇 가지 요구사항을 먼저 정리해봅시다. 각 회원은 ID와 이름이라는 데이터를 갖고 있습니다. 회원을 등록하고 조회하는 기능도 필요합니다. 그리고 아직 데이터를 저장할 데이터베이스가 선정되지 않았다는 가상 시나리오를 바탕으로 진행합니다.
일반적인 웹 애플리케이션 계층 구조
일반적인 웹 애플리케이션 계층 구조는 컨트롤러, 서비스, 리포지토리, 도메인, DB로 구성되어 있다.

- 컨트롤러는 웹 MVC를 컨트롤러 역할과 API를 만드는 컨트롤러 역할을 합니다.
- 서비스에서는 핵심 비즈니스 로직을 구현합니다.
- 리포지토리는 DB에 접근, 도메인 객체를 DB에 저장하고 관리하는 역할을 합니다.
- 도메인은 회원, 주문, 쿠폰 등 주로 DB에 저장하고 관리되는 비즈니스 도메인 객체입니다.
클래스 의존관계

- 회원 관리의 비즈니스 로직을 다루는 MemberService가 있습니다.
- MemberRepository는 interface로 설계합니다. 아직 DB가 선정되지 않았기 때문에 단순히 메모리에 데이터를 저장합니다. DB가 선정되면 메모리에 저장된 데이터를 DB로 옮겨야하기 때문에 interface가 필요합니다.
- MemoryMemberRepository는 MemberRepository 인터페이스를 구현한 구현체입니다.
회원 도메인과 리포지토리 만들기
회원 도메인
회원 도메인을 만들기 위해 src/main/java/hello.hello_spring 에 domain이라는 패키지를 추가하고 Member 클래스를 생성합니다.
회원 ID와 이름과 이에 대한 Getter와 Setter를 작성합니다.
package hello.hello_spring.domain;
public class Member {
private Long id;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
회원 리포지토리 interface
위에서 만든 회원 객체를 저장할 리포지토리를 만듭니다. src/main/java/hello.hello_spring 폴더에 repository라는 패키지를 추가하고 MemberRepository라는 interface 파일을 생성합니다.
회원 저장 및 조회 기능을 추가합니다.
package hello.hello_spring.repository;
import hello.hello_spring.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();
}
- Optional은 Java 문법으로 값을 반환할 때 반환값이 null인 경우를 대비하기 위해 사용합니다.
회원 리포지토리 메모리 구현체
interface를 만들었으니 구현체를 만듭니다. interface와 같은 패키지 안에 MemoryMemberRepository를 추가합니다. 위에서 만든 interface를 구현하기 위해 implements 메서드를 사용합니다.
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>(); // Java의 자료구조 중 하나로 key:value 형태로 데이터를 저장합니다.
private static long sequence = 0L; // id로 사용할 값입니다.
@Override
public Member save(Member member) {
member.setId(++sequence); // id를 1 증가시키고 설정합니다.
store.put(member.getId(), member); // id와 객체를 함께 저장합니다.
return member;
}
@Override
public Optional<Member> findById(Long id) {
// 해당하는 id를 가진 회원이 없는 경우(null)를 대비해 Optional.ofNullable()로 감쌉니다.
return Optional.ofNullable(store.get(id)); // id로 store에서 꺼내서 return합니다.
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream() // store의 values를 stream을 통해 돌면서
.filter(m -> m.getName().equals(name)) // member의 이름이 name과 같은지 filtering합니다.
.findAny(); // filter를 통과한 첫 번째 요소를 return합니다.
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values()); // store의 values를 List형태로 return합니다.
}
public void clearStore() {
store.clear(); // store를 초기화합니다.
}
}
회원 리포지토리 테스트 케이스 작성
개발한 기능이 의도한대로 동작하는지 확인하기 위해서 테스트 케이스를 작성합니다. Java에서는 JUnit이라는 프레임워크로 테스트를 실행합니다.
회원 리포지토리 메모리 구현체 테스트
src/test/java 폴더에 repository라는 패키지를 생성하고 MemoryMemberRepositoryTest 클래스를 만듭니다.
package hello.hello_spring.repository;
import hello.hello_spring.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 { // public일 필요가 없으므로 지워줍니다.
// Test를 위한 저장소를 생성합니다.
MemoryMemberRepository memoryMemberRepository = new MemoryMemberRepository();
@AfterEach // 각각의 테스트 이후에
public void afterEach() {
memoryMemberRepository.clearStore(); // 저장소를 초기화합니다.
// 파일 단위로 테스트를 실행할 경우 필요합니다.
}
@Test
public void save() {
// given
// name이 "spring"인 member 객체를 생성합니다.
Member member = new Member();
member.setName("spring");
// 저장소에 member를 저장합니다.
memoryMemberRepository.save(member);
// when
// 저장소에서 id를 통해 member를 저장소에서 찾습니다.
Member result = memoryMemberRepository.findById(member.getId()).get();
// then
// 저장한 값과 찾은 값을 비교합니다.
assertThat(member).isEqualTo(result);
}
@Test
public void findByName() {
// given
// name이 "spring1"과 "spring2"인 member 객체를 생성하여 저장소에 저장합니다.
Member member1 = new Member();
member1.setName("spring1");
memoryMemberRepository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
memoryMemberRepository.save(member2);
// when
// name이 "spring1"인 member를 저장소에서 찾습니다.
Member result = memoryMemberRepository.findByName("spring1").get();
// then
// 저장한 값과 찾은 값을 비교합니다.
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
// given
// name이 "spring1"과 "spring2"인 member 객체를 생성하여 저장소에 저장합니다.
Member member1 = new Member();
member1.setName("spring1");
memoryMemberRepository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
memoryMemberRepository.save(member2);
// when
// 저장소의 모든 member 객체를 찾습니다.
List<Member> result = memoryMemberRepository.findAll();
// then
// 찾은 member의 수가 저장한 member의 수와 같은지 비교합니다.
assertThat(result.size()).isEqualTo(2);
}
}
- 각 함수를 실행하여 테스트를 수행하거나 파일, 패키지 단위로도 테스트를 실행할 수 있습니다.
- ⌥⌘V 으로 return 값의 자료형으로 변수에 저장할 수 있습니다.
- ⌃R 으로 가장 최근 실행했던 함수, 파일 또는 패키지를 다시 실행할 수 있습니다.


좌측 사진 버튼을 눌러 실행하면 우측 사진과 같은 결과를 볼 수 있습니다.
회원 서비스 개발
회원 서비스에는 회원 domain과 회원 repository를 활용하여 비즈니스 로직을 작성합니다. src/main/java/hello.hello_spring 에 service 패키지를 새로 생성하고 그 안에 MemberService 클래스를 생성합니다. 클래스 내부는 아래와 같이 작성합니다.
package hello.hello_spring.service;
import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
// 저장소
private final MemberRepository memberRepository = new MemoryMemberRepository();
public Long join(Member member) { // 회원가입
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member); // 저장소에 저장
return member.getId(); // member id 반환
}
private void validateDuplicateMember(Member member) { // 중복 회원 검증 메소드
memberRepository.findByName(member.getName()) // 저장소에서 이름이 같은 member 객체가
.ifPresent(m -> { // 존재한다면
throw new IllegalStateException("이미 존재하는 회원입니다."); // 예외발생
});
}
public List<Member> findMembers() { // 전체 회원 조회
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) { // id로 회원 조회
return memberRepository.findById(memberId);
}
}
- ⌃T로 Refactoring 관련 항목을 볼 수 있습니다.
- Repository 클래스는 기계적/개발스러운 용어를, Service 클래스는 비즈니스 용어를 사용합니다.
회원 서비스 테스트
MemberService 클래스의 테스트 클래스를 만들어봅시다. MemberService 클래스에 커서를 두고 ⇧⌘T으로 간편하게 Test를 만들 수 있습니다. 자동으로 main과 동일한 패키지/클래스 구조로 만들고 아래 코드를 작성합니다.
package hello.hello_spring.service;
import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService = new MemberService();
@Test
void 회원가입() { // Test 코드는 빌드에 포함되지 않기 때문에 한글을 사용해도 됩니다.
// given
Member member = new Member(); // member 객체 생성
member.setName("spring"); // name을 "spring"으로 설정
// when
// member 객체를 회원가입하고 반환된 id를 저장
Long saveId = memberService.join(member);
// then
// 저장소에서 회원가입한 member의 id가 있으면 객체로 저장
Member findMember = memberService.findOne(saveId).get();
// 회원가입한 member와 저장소에서 가져온 member의 name이 같은지 검증
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() {
// given
// 이름이 같은 두 member 객체 생성
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// when
// member1 회원가입
memberService.join(member1);
// member2가 회원가입할 때 예외 발생 여부
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
// 앞서 발생한 예외의 메시지가 의도한 메시지와 같은지 검증
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
위와 같이 작성하고 실행하면 모든 테스트가 정상적으로 작동합니다.

회원 서비스 테스트 DI
앞서 회원 리포지토리 테스트 케이스 작성처럼 각 메서드의 실행이 끝날 때마다 저장소에 있는 내용을 다 지우는 메서드를 추가해야합니다.
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
@AfterEach // 메서드 실행이 끝날 때마다
public void afterEach() {
memberRepository.clearStore(); // 저장소 초기화
}
그러나 이렇게 작성하면 문제가 발생할 수 있습니다. memberService 객체가 아닌 새로운 memoryMemberRepository 객체이기 때문에 다른 저장소를 초기화할 수 있습니다. 지금은 MemoryMemberRepository의 저장소인 HashMap이 static으로 선언되어 있기에 문제가 없지만, static이 아닌 경우 다른 저장소에 작업을 수행하게 됩니다.
그래서 이런 경우 MemberService에 MemoryMemberRepository를 외부에서 주입하는, **의존성 주입(DI, Dependency Injection)**을 합니다. 아래와 같이 수정합니다.
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
이제 MemberServiceTest를 아래와 같이 수정해보겠습니다.
MemberService memberService;
MemoryMemberRepository memoryMemberRepository;
@BeforeEach // 메서드 시작 전
public void beforeEach() {
memberRepository = new MemoryMemberRepository(); // 새로운 저장소를 만들고
memberService = new MemberService(memoryMemberRepository); // 서비스에 주입
}
@AfterEach // 메서드 종료 후
public void afterEach() {
memberRepository.clearStore(); // 저장소 초기화
}
이제 실행해 보면 문제 없이 실행되는 것을 확인할 수 있습니다.

'Development > Spring' 카테고리의 다른 글
| [Spring] 스프링 입문 - 강의 정리 6 (0) | 2026.02.08 |
|---|---|
| [Spring] 스프링 입문 - 강의 정리 5 (0) | 2026.02.08 |
| [Spring] 스프링 입문 - 강의 정리 4 (0) | 2026.02.08 |
| [Spring] 스프링 입문 - 강의 정리 2 (0) | 2026.02.08 |
| [Spring] 스프링 입문 - 강의 정리 1 (0) | 2026.02.08 |