1. 비즈니스 요구사항 정리
정말 간단한 기초 예제를 실습하는 것이기 때문에
데이터: 회원ID, 이름
기능: 회원 등록, 조회
아직 데이터 저장소가 선정되지 않음(가상의 시나리오)
로 일단 설정한다.
일반적인 웹 애플리케이션을 그대로 따라갈 것이고,
컨트롤러 -> 서비스 -> 레포지토리 -> DB의 구조를 따라간다.
컨트롤러는 웹 MVC의 컨트롤러 역할을 하고,
서비스는 핵심 비즈니스 로직을 구현하며,
레포지토리는 DB에 접근, 도메인 객체를 DB에 저장하고 관리하는 역할을 한다.
도메인은 비즈니스 도메인 객체로 DB에 저장되고 관리된다.
클래스 의존관계는 MemberService -> interface(MemberRepository) -> Memory MemberRepository
의 구조다.
아직 데이터 저장소가 선정되지 않은 가상의 시나리오이기 때문에
초기 개발 단계에서 구현체로 가벼운 메모리 기반의 DB 저장소를 사용한다.
2. 회원 도메인 & 레포지토리 생성

먼저 회원 객체를 생성했다.
package를 통해 이 파일이 domain이라는 폴더 안에 있다는 것을 자바에게 알려주었다.
이유는 나중에 파일이 많아졌을 때, 종류별로 잘 정리하기 위해서이다.
다음으로, Member라는 public class를 정의했다.
그리고 필드에 id와 name을 private 변수로 선언했다. private으로 선언하여 캡슐화를 했다.
long 대신 Long 클래스를 사용한 이유는 null 상태를 허용하기 위해서이다.
다음으로, Getter와 Setter을 사용하여 메서드를 정의했다.
setId 메서드에서 this.id는 클래스 안에 선언된 진짜 내 데이터고,
id는 방금 괄호 안으로 전달 받은 새로운 파라미터 값을 가리킨다.
name에 대한 메서드도 동일하다.

그다음 회원 레포지토리 인터페이스를 생성했다.
이는 회원 데이터를 저장하고 꺼내오는 규칙을 설계하는 설계도 역할을 한다.
이 파일은 저장소 폴더인 repository폴더 안에 위치하므로
package에 이를 명시한다.
그리고 위에서 만든 Member파일을 import해주고,
데이터를 리스트 목록으로 다룰 수 있게 해주는 List와
옵션을 다룰 수 있게 해주는 Optional도 import해준다.
그다음 MemberRepository라는 interface를 정의했다.
인터페이스는 명세서 같은 껍데기 역할을 한다.
이 interface 안에 4가지 필수 기능을 정의하게 된다.
interface 안에는 이 기능이 어떻게 구체적으로 작동하는지
{ } 안에 들어가는 로직은 작성하지 않고,
오직 입력값(파라미터)과 출력값(반환 타입)만 적게 된다.
먼저 저장 기능인 save를 정의했다.
Member 객체(회원 정보)를 넘겨주면, 저장소에 보관한 뒤
id가 부여된 저장 완료된 Member 객체를 다시 반환하는 로직이다.
그다음, id로 회원 찾기 기능이다.
id를 넘기면 id에 해당하는 회원을 찾아 반환한다.
Optional을 쓰는 이유는 null의 경우때문이다.
해당되는 id가 저장소에 있지 않는 경우에 null을 반환하게 되고, 그러면 에러가 발생한다.
이를 방지하기 위해 Optional을 사용하게 된다.
그다음, 이름으로 회원 찾기 기능이다.
이도 id로 회원 찾기 기능과 동일하다.
마지막으로, 모든 회원 목록 조회 기능이다.
List를 사용하여 리스트 형태로 반환한다.
이때 회원이 0명이면 빈 리스트가 반환된다.
인터페이스를 먼저 만드는 이유는 레포지토리의 변경 가능성 때문이다.
임시 메모리에서 나중에 진짜 DB로 변경하게 될 때,
인터페이스가 먼저 규격으로 정의되어 있으면,
레포지토리만 갈아끼우면 되기 때문이다.

마지막으로 인터페이스를 바탕으로 실제 데이터를 저장하고 꺼내주는 구현체를 완성했다.
이 클래스는 실제 DB에 연결하기 전, 컴퓨터의 메모리(RAM)에 임시 저장소로 사용된다.
이제 클래스를 선언하고, 저장소를 준비한다.
MemoryMemberRepository라는 클래스를 선언하는데,
implements를 통해 아까만든 MemberRepository 인터페이스의 4가지 규칙을 상속받는다.
회원 정보를 저장할 임시 메모리 저장소 store을 딕셔너리 형태로 선언한다.
key에는 회원번호(Long), value에는 회원 객체 전체(Member)를 담는다.
그리고 회원에게 번호를 부여하는 sequence를 선언한다.
store와 sequence는 모두 정적으로 선언하게 된다.
이제 기능 구현을 할 차례이다.
먼저 회원 저장(save) 기능이다.
@override를 통해 부모(인터페이스)가 설계한 설계도대로 구현하게 된다.
sequence에 1을 더한 후, setId로 회원에게 번호를 1부터 차례대로 번호를 부여한다.
그다음 store에 getId로 얻은 회원번호와 회원정보를 딕셔너리로 저장(put)한다.
마지막으로 저장이 마무리된 회원 정보(member)를 반환한다.
그다음은 아이디로 찾기(findById) 기능이다.
마찬가지로 @override로 상속받아주고,
store에서 id에 해당되는 번호를 get으로 얻어준다. 즉, 회원번호를 얻게 되는 것이다.
만약, 찾는 번호의 회원이 없으면 get()은 null을 반환하게 되는데,
이때 ofNullable Optional을 사용하여 null을 안전하게 담는다.
그다음은 전체 찾기(findAll) 기능이다.
store의 value들, 즉 회원 정보들을 얻어준다.
이 정보들을 새로운 리스트 ArrayList인 리스트 형태로 담아 반환해준다.
그다음은 이름으로 찾기(findByName) 기능이다.
store의 values(회원 정보)를 stream()에 올려준다.
그리고 filter을 사용해서 내가 찾는 이름(name 파라미터)가 회원의 이름(getName())과 동일한 경우만 걸러준다.
findAny()를 사용하여 걸러져 나온 회원 정보들 중 가장 먼저 찾아진 정보를 Optional 상자에 담아 반환한다.
마지막으로, 인터페이스에는 없지만 저장소를 비워주는 기능이다.
나중에 테스트 코드를 작성할 때,
테스트가 끝나고, 잔여 데이터를 정리하기 위해 필요한 기능이다.
clear()를 통해 구현한다.
3. 동시성 문제란?
스프링 부트(웹 서버)의 가장 큰 특징은
수십, 수백 명의 사용자가 동시에 접속한다는 것이다.
문제 1: long sequence의 충돌
만약 사용자 A와 B가 정확히 0.0001초의 오차도 없이
동시에 회원 가입 버튼을 누른다면 어떻게 될까?
컴퓨터가 ++sequence를 처리할 때,
둘 다 원래 값인 0을 보고 동시에 1을 더해버리게 된다.
결과적으로 A도 1번 회원, B도 1번 회원이 되는 고유 번호 중복(데이터 오염)이 발생한다.
해결책 (AtomicLong): 실무에서는 long 대신 AtomicLong이라는 특수 타입을 쓴다고 한다.
이건 "내가 번호표 뽑고 있을 땐 아무도 접근하지 마!"
라고 자물쇠를 걸어버려서 중복을 완벽히 막아주게 된다.
문제 2: HashMap의 충돌
HashMap은 애초에 혼자 쓰려고 만든 바구니다.
여러 명(스레드)이 동시에 데이터를 마구 집어넣으면(put),
바구니가 찢어지거나 누군가의 데이터가 허공으로 증발해 버린다.
해결책 (ConcurrentHashMap): 실무에서는 여러 명이 동시에 접근해도 데이터가 꼬이지 않도록
특수하게 설계된 ConcurrentHashMap을 사용해야 한다고 한다.
동시성 문제는 이런 작은 공부용 프로젝트에서는
고려할 일이 없다고는 하지만, 혹시 모르니 알아보았다.
4. 회원 레포지토리 테스트 케이스 작성

개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나,
웹 애플리케이션의 컨트롤러를 통해 서 해당 기능을 실행한다.
이러한 방법은 준비하고 실행하는데 오래 걸리고,
반복 실행하기 어렵고 여러 테스트를 한번 에 실행하기 어렵다는 단점이 있다.
자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
백엔드 개발에 있어서 테스트 코드 작성 단계는 매우 중요하다고 생각한다.
이번 코드에서는 given-when-then (준비 - 실행 - 검증) 테스트 패턴을 적용했다.
먼저 테스트할 만들었던 메모리 저장소를 객체로 생성한다.
JUnit에서는 테스트 클래스에는 public을 붙이지 않아도 된다고 한다.
@AfterEach가 이 코드의 핵심이다.
이를 통해 각 테스트가 끝날 때마다 무조건 실행(clear())하여 데이터를 정리한다.
만약 이게 없다면,
findAll 테스트에서 회원을 여러 명 저장했는데
데이터가 남아있어 다른 테스트에서 실패할 수 있다.
1. 저장(save) 기능 테스트
@Test를 통해 테스트 코드라고 명시하는 역할을 해준다.
새로운 회원 객체를 생성하고, 회원의 이름을 spring으로 설정한다.
저장소에 회원을 저장한다.
저장된 회원의 ID를 꺼내서 findById로 찾아보라고 한다.
findById는 Optional을 반환하므로 .get()으로 박스 안의 회원 객체를 꺼내게 되고,
이를 result에 저장한다.
AssertJ 라이브러리를 사용하여
내가 찾은 결과(result) = 처음 생성한 회원 객체(member)이면,
초록불(성공), 아니면 빨간불(실패)을 띄워주게 된다.
2. 이름으로 찾기(findByName) 기능 테스트
두 명의 회원 객체를 생성하고,
각각 spring1, spring2로 이름을 설정한 후,
저장소에 넣는다.
spring1이라는 이름을 가진 회원을 findByName으로 찾고 얻은 회원 객체를
이의 결과를 result에 담는다.
위와 마찬가지로 asserthat isEqualTo를 통해
동일한지 검증한다.
3. 전체 찾기(findAll) 기능 테스트
이름으로 찾기와 마찬가지로
두 명의 회원 객체를 설정하여 저장소에 저장한다.
result 리스트에 findAll로 얻은 모든 회원 객체를 담는다.
result의 size가 2라면 초록불 아니라면 빨간불이 뜨게 되며 검증된다.
테스트 코드를 작성하며 알게 된 점은
테스트코드에서는 get()을 사용해도 되지만,
실제 서비스 코드에서는 get()을 사용하면,
null일 경우 에러가 나므로 사용하지 않는다고 한다.

이는 테스트 코드 3개를 모두 실행했을 때의 결과이다.
검증이 모두 성공하여 3개 모두 초록불이 뜬 것을 볼 수 있다.
'백엔드' 카테고리의 다른 글
| Spring 입문 #5 - 회원 관리 예제: 웹 MVC 개발 (0) | 2026.05.09 |
|---|---|
| Spring 입문 #4 - 스프링 빈과 의존관계 (0) | 2026.05.04 |
| Spring 입문 #3 - 회원 관리 예제: 회원 서비스 (2) | 2026.05.02 |
| Spring 입문 #1 - 스프링 웹 개발 기초 (0) | 2026.03.17 |
| Spring 입문 #0 - 프로젝트 환경설정 (0) | 2026.03.07 |
