| 도메인 | Method | Endpoint | Path Variable | Query Parameter | Request Body | Response | Description |
|---|---|---|---|---|---|---|---|
| Reservation | GET |
/reservations |
- | - | - | List<ReservationResponse> |
모든 예약 목록 조회 |
| Reservation | POST |
/reservations |
- | - | ReservationRequest(name, date, timeId, themeId) |
ReservationResponse |
새로운 예약 생성 |
| Reservation | DELETE |
/reservations/{id} |
id (Long) |
- | - | 200 OK (Void) |
식별자를 통한 예약 삭제 |
| Theme | GET |
/themes |
- | - | - | List<ThemeResponse> |
모든 테마 목록 조회 |
| Theme | GET |
/themes |
- | topCount (Long)during (Long) |
- | List<ThemeResponse> |
기간(during) 내 상위(topCount) 인기 테마 조회 |
| Theme | POST |
/themes |
- | - | ThemeRequest(name, description, thumbnailUrl) |
ThemeResponse |
새로운 테마 생성 |
| Theme | DELETE |
/themes/{id} |
id (Long) |
- | - | 200 OK (Void) |
식별자를 통한 테마 삭제 |
| Time | GET |
/time |
- | - | - | List<TimeResponse> |
모든 예약 시간 목록 조회 |
| Time | GET |
/time |
- | themeId (long)date (LocalDate) |
- | List<TimeResponse> |
특정 날짜, 테마의 예약 가능 시간 조회 |
| Time | POST |
/time |
- | - | TimeRequest(startAt) |
TimeResponse |
새로운 예약 시간 생성 |
| Time | DELETE |
/time/{id} |
id (Long) |
- | - | 200 OK (Void) |
식별자를 통한 예약 시간 삭제 |
| Waiting | POST |
/waitings |
- | - | WaitingRequest(name, date, timeId, themeId) |
200 OK (Void) |
새로운 예약 대기 신청 |
| Waiting | DELETE |
/waitings/{id} |
id (Long) |
userName (String) |
- | 204 No Content (Void) |
식별자 및 사용자 이름을 통한 예약 대기 취소 |
리소스 식별
URL을 정할 때
무엇을 리소스로 보고,어떻게표현할 것인가
Rule 1.
- 도메인 분석을 통해, 실행 흐름에 나타나는
명사를 리소스로 식별한다.
추상적인 규칙으로 식별과 명세를 각자의 자율에 맡기되
피드백 활동에서 각자의 경험을 공유해 규칙을 구체화한다.- 서버/도메인 모델 관점?
- 클라이언트/멘탈 모델 관점?
- 다른 리소스를
Path로?parameter로?
데이터 검증
어떤 판단을 서버가 책임지고, 어떤 판단을 클라이언트가 책임지는가
-
(If-Then) 만약 판단에 필요한 원천 데이터가 서버에만 존재한다면
→ 해당 데이터에 대한 검증은 서버의 책임이다.- 예: "예약 가능 여부(available)"는 서버가 기존 예약 현황을 계산하여 응답에 포함한다.
-
(If-Then) 만약 클라이언트가 받은 데이터를 기반으로 서버에 요청한다면
→ (받은 데이터 기준) 유효한 데이터를 요청해야 한다.- 예: "예약 불가능"한 시간대로 예약을 요청하지 못하도록 방어한다.
-
(우선순위) 책임 판단 순서:
- 데이터의 무결성을 보장해야 하는 곳이 어디인가? (보통 서버)
- 클라이언트가 중복된 계산 로직을 가질 필요가 있는가?
-
(금지) 클라이언트에게 비즈니스 로직 판단을 떠넘기지 않는다.
- 이유: 데이터 불일치 문제를 방지하고 클라이언트의 복잡도를 낮추기 위함이다.
권한과 역할
같은 자원을 다루는 관리자와 사용자 API를 분리하는가, 합치는가
-
(If-Then) 만약 리소스의 도메인 모델과 생성 행위가 동일하다면
→ 관리자와 사용자 구분 없이 같은 엔드포인트를 사용한다.- 예: POST /reservations (누가 생성하든 '예약 생성'이라는 행위는 동일함)
-
(If-Then) 만약 사용자의 권한에 따라 응답 필드의 구성이나 노출 데이터가 현저히 다르다면 →
도메인 목적에 따라 API를 분리하거나 권한 로직으로 분기한다. -
(우선순위) 결정 순서:
- 리소스의 식별자가 동일한가?
- 조회/수정의 목적과 Context가 동일한가?
바람직한 설계
동작하는 것을 넘어, 어떤 조건을 만족해야 좋은 API라고 부를 것인가
- 정의
- 동작을 완벽히 보장하는 것은 물론,
- 뛰어난 가독성을 바탕으로 명세만 보아도
- 요청과 행동을 직관적으로 식별할 수 있는 API.
- 특징
- 직관성: URL(리소스 명사)과 HTTP 메서드(행위 동사)의 조합만으로 해당 API가 어떤 자원에 대해 무슨 작업을 수행하는지 누구나 명확히 추론할 수 있습니다.
- 명확성: 리소스의 정의, 메서드 시그니처 등에 대해 팀이 합의한 명확한 기준과 규칙을 엄격하게 따릅니다.
- 문서화의 역할: 잘 짜인 API 명세 자체가 훌륭한 설명서 역할을 하여, 개발자 간의 소통 비용과 유지보수 비용을 크게 낮춥니다.
미션 진행 중 추가
- 동작하는 API 중 바람직한 API 가 좋은 API 다.
- 올바른 요청에 올바른 응답을 보내는 것은 동작하는 API 다.
- 잘못된 요청에 합당한 응답을 보내는 것은 바람직한 API 다.
- 명세만으로 목적/행위/결과를 추론할 수 있는 것은 바람직한 API 다.
- 드러나선 안 될 정보를 은닉하는 것은 바람직한 API 다.
- 엔드포인트는
무엇을, 메서드는어떻게를 표현한다.
대상과 내용
에러 발생 시 누구에게 무엇을 알릴 것인가
- 응답 수신자
- 사용자
- 에러를 발생시킨 사용자
- 클라이언트
- 에러를 전달한 클라이언트
- 에러를 발생시킨 클라이언트
- 사용자
- 응답 내용
- 사용자/클라이언트 공통
- 에러 메시지(에러 발생 원인과 대안)
- 클라이언트 전용
- 타임스탬프
- 에러 발생 경로
- 에러의 발생 원인
- 사용자/클라이언트 공통
사이클2 에러 응답 규칙 2/3번 통합해서 6번 규칙 설정
대안의 기준
에러의 대안을 어떤 기준으로 어느 범위까지 담을 것인가
-
(If-Then) 만약 비즈니스 규칙을 위반한 요청이라면
→ 4xx 상태 코드 + “위반 내용과 다음 행동을 유발할 수 있는 메시지를 담는다.”- 예: 중복된 날짜/테마/시간에 대한 예약 요청 시 409 + "선택하신 시간과 테마는 이미 예약되었습니다."
- (이유: 사용자가 응답만으로 다음 행동을 정할 수 있어야 한다)
-
(금지: 사용자의 다음 행동을 위한 추가적인 조회/로직 호출을 진행하지 않는다.)
- (이유: 응답에 사용되는 정보는 요청 - 응답 흐름 내부에서의 정보로 한정해야 한다)
프레임워크나 라이브러리 자체가 아니라,
우리가 작성한 비즈니스 규칙과 사용자에게 보이는 핵심 동작이
변경 이후에도 깨지지 않도록 보호하기 위해 테스트한다.
(If-Then)
만약 [핵심 도메인 규칙(예: 예약 중복 방지)이나 외부 동작 계약(API 응답)이 변경되는 상황]
→ [해당 스펙을 검증하는 테스트를 최우선으로 작성한다.]
(If-Then)
만약 외부 의존성 없이 순수 도메인 규칙만 검증 가능하다면 → 단위 테스트로 검증한다. (이유: 가장 빠르고, 실패 원인을 명확하게 드러낼 수 있다)
(If-Then)
만약 스프링 컨테이너, DB, HTTP, Repository 등 외부 의존성과의 결합이 검증 대상이라면
→ 통합 및 슬라이스 테스트로 검증한다.
(이유: 단위 테스트만으로는 결합 지점의 문제를 발견할 수 없다)
(If-Then)
만약 사용자 관점의 주요 흐름(예약 생성, 예약 대기, 인증 흐름 등)을 보호해야 한다면
→ E2E 테스트로 최소한의 핵심 시나리오를 검증한다.
(이유: 실제 사용 흐름이 깨지는 문제를 가장 늦게 발견하지 않기 위해서다)
(우선순위)
같은 검증을 어디서 할지 결정할 때 순서:
- 외부 의존 없이 검증 가능한가? → 단위 테스트
- 결합 자체가 검증 대상인가? → 통합 테스트
- 사용자 시나리오 전체 보호가 목적인가? → E2E 테스트
(금지)
단순 위임만 수행하는 계층은 별도 테스트하지 않는다.
(이유: 동일한 내용을 여러 계층에서 중복 검증하게 된다)
(If-Then)
만약 DB 저장/조회 정합성이 중요하다면
→ 우리는 테스트 DB 또는 인메모리 DB를 사용한다.
만약 비즈니스 분기만 검증하면 된다면
→ 우리는 Fake 또는 Mock을 사용할 수 있다.
(이유: 외부 프로덕션 DB 상태에 의존하지 않고, 로컬 환경에서 독립적이고 빠른 쿼리 정합성 검증이 필요하기 때문이다.)
만약 HTTP 요청/응답 전체가 중요하다면
→ 우리는 실제 요청 기반 테스트를 작성한다.
만약 외부 API 호출이 필요하다면
→ 우리는 실제 호출하지 않고 Fake 또는 Mock으로 대체한다.
(우선순위)
A. 실제 I/O 없이 협력 객체의 호출 행위(메서드 호출 여부)만 확인하면 되는가? (Mocking)
B. 실제 쿼리 문법과 ORM 매핑 확인이 필요한가? (인메모리 DB)
C. 벤더 특화 기능(MySQL Lock, 특정 함수 등) 검증이 필요한가? (실제 DB 연동)
(If-Then)
만약 단순 설정, 단순 등록, 공통 인프라, 단순 위임이라면
→ 우리는 테스트하지 않는다.
(우선순위)
A. 코드 내부에 비즈니스 분기나 데이터 가공 로직이 존재하는가? (테스트 필수 대상)
B. 우리가 직접 커스텀하게 구현한 인프라 설정인가? (통합/슬라이스 테스트로 간접 검증)
C. 프레임워크가 자체적으로 보장하는 기능인가? (테스트 생략)
(금지)
이번 사이클에서 @ControllerAdvice 등 전역 설정 파일 자체에 대한 테스트는 하지 않는다.
(이유: 전역 예외 처리나 필터는 프레임워크의 생명주기 내에서 동작하는 영역이므로, 이를 따로 떼어내어 검증하는 것은 무의미하기 때문이다)
(If) 만약 여러 변경이 하나의 비즈니스 결과를 완성한다면
(Then) 하나의 요청에 대응하는 하나의 서비스 메서드(작업 단위)가 여러 변경을 처리하도록 구현
(Code) [예약 취소/대기 승인] 이 하나의 비즈니스 결과이므로 cancelReservation 메서드가 여러 변경을 처리
(이유) 원자성이 보장되는 최소한의 작업 단위는 비즈니스 결과이다
(If) 만약 한 변경이 실패했을 때 다른 변경이 사용자에게 잘못된 상태를 보이게 한다면
(Then) 두 변경을 하나의 작업 단위로 묶는다.
(Code) 예약 취소+대기 유지 시 테이블은 무결하지만 비즈니스 로직에 부정합하므로 cancelReservation 메서드가 일괄 처리
(이유) 트랜잭션 경계는 테이블 단위가 아니라 비즈니스 정합성 단위로 정해야 한다
(If) 만약 사용자 요청에 대한 결과와 직접적으로 연관이 없는 작업이라면
(Then) 트랜잭션에 묶지 않고 별도로 처리한다.
(Code) 사용자 요청 실행 흐름에 속해도 응답과 관계 없는 부가 기능이라면 별도 메서드로 분리
(이유) 실패해도 사용자가 보는 핵심 결과가 달라지지 않는 작업까지 묶으면 트랜잭션 범위가 불필요하게 커진다
(If) 만약 한 작업이 실패해도 나중에 재시도하거나 비동기로 처리할 수 있다면
(Then) 트랜잭션에 묶지 않고 별도로 처리한다.
(Code) 실패 시 재시도 가능한 로직 / 비동기 호출하는 부가 기능이라면 별도 메서드로 분리
(이유) 즉시 정합성이 필요 없는 작업은 핵심 작업의 성공 여부를 막지 않아야 한다
(If) 만약 트랜잭션 중간에 실패가 발생한다면
(Then) 사용자에게 중간 결과를 보여주지 않고 전체 작업 실패로 안내한다.
(Code) 예약 취소는 성공 + 대기 승인은 실패 시 예약 취소에 실패했습니다 응답
(이유) 사용자가 확정되지 않은 결과를 성공한 결과로 오해하면 안 된다
경계를 결정할 때 순서:
- 사용자가 잘못된 중간 상태를 보게 되는지 생각해 본다.
- 일부만 성공했을 때 비즈니스 정합성이 깨지는지 생각해 본다.
- 해당 변경이 사용자 요청에 의한 결과를 완성하는 필수 작업인지 생각해 본다.
- 실패해도 사용자 경험에 직접 영향이 없는 부가 작업인지 생각해본다.
- 비동기 처리나 재시도로 복구 가능한지 생각해본다.
- “테이블이 여러 개 바뀐다”는 이유만으로 트랜잭션을 묶지 않는다.
- 이유: 트랜잭션 경계는 데이터베이스 구조가 아니라 비즈니스 정합성 기준으로 결정해야 한다.
- 사용자가 볼 수 있는 중간 실패 상태를 성공처럼 노출하지 않는다.
- 이유: 사용자는 완료된 결과만 신뢰할 수 있어야 한다.
CREATE TABLE slot
(
id BIGINT NOT NULL AUTO_INCREMENT,
date DATE NOT NULL,
time_id BIGINT NOT NULL,
theme_id BIGINT NOT NULL,
PRIMARY KEY (id),
CONSTRAINT uk_slot_date_time_theme UNIQUE (date, time_id, theme_id)
);
CREATE TABLE reservation
(
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
-- date DATE NOT NULL,
-- time_id BIGINT NOT NULL,
-- theme_id BIGINT NOT NULL,
slot_id BIGINT NOT NULL,
PRIMARY KEY (id),
-- FOREIGN KEY (time_id) REFERENCES time_slot (id),
-- FOREIGN KEY (theme_id) REFERENCES theme (id),
-- CONSTRAINT uk_reservation_date_time_theme UNIQUE (date, time_id, theme_id)
FOREIGN KEY (slot_id) REFERENCES slot (id),
CONSTRAINT uk_reservation_name_slot UNIQUE (name, slot_id)
);
CREATE TABLE waiting
(
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(250) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- date DATE NOT NULL,
-- time_id BIGINT NOT NULL,
-- theme_id BIGINT NOT NULL,
slot_id BIGINT NOT NULL,
PRIMARY KEY (id),
-- FOREIGN KEY (time_id) REFERENCES time_slot (id),
-- FOREIGN KEY (theme_id) REFERENCES theme (id),
-- CONSTRAINT uk_waiting_name_date_time_theme UNIQUE (name, date, time_id, theme_id)
FOREIGN KEY (slot_id) REFERENCES slot (id),
CONSTRAINT uk_waiting_name_slot UNIQUE (name, slot_id)
);- 중복된 데이터를 남겨놓을 것인가?
- 장점?
- 현재 구조 유지 가능
- 필요한 데이터를 직접 필드로 참조하는 도메인
- 단점?
- 중복 데이터 누적
- 도메인 상태 과중
- 상태값(name, time_id, theme_id) 기반 조회 빈발
- 예약 삭제 시 상태값으로 대기를 조회해서 승인 로직 진행 필요
- 도메인 객체보다 엔티티에 가까운 구조
- 장점?
장점보다 단점이 많다
- 데이터 중복을 어떻게 해결할 것인가?
- 추출과 분리, 정규?화
- 공통 컬럼인 (date, time_id, theme_id) 추출을 통한 분리
- 예약 1 : 슬롯 1 / 슬롯 1 : 대기 N 참조 구조
공통 데이터를 추출해 관리하고, 추출한 슬롯을 기반으로 조회/삭제/승인하도록 구조 변경
- 슬롯에 예약은 하나, 대기는 여럿인 상황에서 외래 키 참조 방식은?
- 예약 1(외래키 참조) - 슬롯 1 - 대기 N(외래키 참조)
- 스키마가 변경되어도 변경의 전파가
Repository에 국한되는가?- 아마?
- 동시성 제어 측면에서 효과적인가?
- 현재 외래키 없는 구조, 예약 삭제 - 대기 승인 흐름에서 대기 추가되면?
- 예약 추가/삭제 시 슬롯이 함께 추가/삭제되도록 하면 방지 가능?
- 현재 외래키 없는 구조, 예약 삭제 - 대기 승인 흐름에서 대기 추가되면?
분리하고 수정해보면서 보완하기
조회에선 지난 날짜/시간으로도 도메인이 생성 가능해야 하지만
생성/수정에선 그렇지 않다.
생성/수정이라는 상황을 도메인이 스스로 알 방법이 없어 서비스가 판별했지만
도메인의 자율성을 보장하고 책임을 집중해야.
- 날짜/시간을 판별할
Slot도메인 객체 추가- 기존의 파편화된 날짜/시간을 통합 관리하며 과거인지 판별
boolean isPast- 테스트 용이성과 기타등등을 위해 현재시간 파라미터로 받기
- 기존의 파편화된 날짜/시간을 통합 관리하며 과거인지 판별
- 날짜/시간에 따라 조작 가능 여부 / 생성 가능 여부를 도메인이 직접 확인
-
Reservation/Waiting-void validateModifiable/void validateNotPast
-
- 실행 시마다 새 인스턴스를 생성하고 있는
rowMapper()효율화-
정적 상수화? - 인스턴스 상수화?
-
공통 매핑 로직 유틸 클래스화? - 컴포지션 도입?
-
-
- 스키마 추가
table slot - 도메인 객체 추가
Slot- 과거인지 판별
boolean isPast
- 과거인지 판별
- 서비스 검증 로직 이관
void validateModifiable/void validateNotPast - 매핑 로직 효율화