시작 절차와 PR 규칙은 CONTRIBUTING.md를 따릅니다. 이 문서는 HTTP API를 추가하거나 변경할 때의 상세 기준입니다.
공용 명세가 정한 기본 경로는 /api/core/v1 이며, 이는 context path와 API 버전을 합친 값입니다(08_API_명세 §1). 두 조각의 출처가 다르므로 붙이는 위치도 다릅니다.
| 조각 | 누가 붙이나 |
|---|---|
/api/core |
인프라 고정값. server.servlet.context-path가 붙입니다. 컨트롤러에 다시 쓰지 않습니다 |
/v1 |
컨트롤러가 직접 @RequestMapping에 씁니다 |
- 컨트롤러는
@RequestMapping("/v1/records")처럼/v1을 포함해 매핑합니다. 최종 경로는/api/core/v1/records가 됩니다. /v1을 전역 설정(addPathPrefix등)으로 자동 부여하지 않습니다. 버저닝은 컨트롤러의 책임입니다 — 새 계약이 생기면v2컨트롤러를 추가해v1과 공존시키고, 준비된 리소스만 옮깁니다. 전역 prefix는 이 공존을 막습니다.- 컨트롤러 매핑에
/api/core를 중복하지 않습니다. 중복하면 최종 경로가/api/core/api/core/...가 됩니다. - URI는 복수 명사를 사용합니다. 동작이 필요한 경우에도 리소스와 하위 리소스로 표현하는 방식을 먼저 선택합니다.
-
JPA Entity를 request 또는 response DTO로 직접 노출하지 않습니다. Entity, request DTO, response DTO는 각각의 변경 이유에 맞게 분리합니다.
-
시간 값은 ISO-8601 UTC 형식으로 주고받습니다. 예:
2026-07-23T14:06:24Z. -
Bean Validation 실패는 HTTP 400으로 응답합니다.
-
목록 pagination은 커서 기반이며, query parameter는
cursor·size를 사용합니다.page/sort파라미터는 사용하지 않습니다. -
요청 본문에서 키를 생략한 것과 명시적
null을 구분하지 않습니다. 둘 다 "그 필드를null로 지정했다"로 해석합니다. PATCH도 예외가 아니므로, 값을 유지하려면 현재 값을 그대로 실어 보내야 합니다.Jackson이 record 컴포넌트의 키 부재와 명시적
null을 똑같이null로 역직렬화하기 때문이며, 구분하려면JsonNullable같은 장치를 DTO에 도입해야 합니다. 지금은 도입하지 않았습니다 — 부분 수정 요청이 실제로 필요한 리소스가 아직 없고, 장치를 먼저 깔면 모든 요청 DTO가 그 비용을 집니다.구분이 필요한 리소스가 생기면 그 PR에서 도입하고 이 항목을 갱신합니다. 필드가 여러 개인 PATCH가 등장하는 시점이 그 신호입니다.
적용 예:
PATCH /v1/follows/{followId}의alias—{}와{"alias": null}이 모두 별칭 제거입니다(08 §8.3, Jira 작업).
모든 API 응답은 최상위가 공통 envelope입니다. 성공은 { "success": true, "data": … }, 오류는 { "success": false, "error": {…} }이며 두 키는 동시에 나타나지 않습니다(오류 계약은 에러 처리 규약 참고). 공용 명세 원본은 Team-PinLog/docs의 static/08_API_명세.md §1.6입니다.
- 컨트롤러는 DTO(또는
void)를 그대로 반환합니다.com.pinlog.pinlogback.domain이하 컨트롤러의 응답은global/web/ApiResponseBodyAdvice가 자동으로ApiResponse로 감쌉니다. 컨트롤러에서 직접ApiResponse.ok(...)를 만들어 반환하지 않습니다.- 감쌀 대상인지 판정하는 곳은
global/web/EnvelopeTargets한 곳입니다. 런타임(advice)과 문서 생성(springdoc customizer)이 같은 결론을 내야 하므로 판정을 각자 두지 않습니다 — 두 곳에 두었을 때ResponseEntity<ApiResponse<T>>에서 결론이 갈려 문서만 이중 래핑된 적이 있습니다. 이미 envelope인 반환 타입은 양쪽 모두 감싸지 않습니다.
- 감쌀 대상인지 판정하는 곳은
- 성공 응답에는
message필드를 두지 않습니다. 사람이 읽을 문구가 필요하면data안의 도메인 필드로 표현합니다. - 목록 응답은
data안에items(배열)·nextCursor·hasNext를 담는 커서 기반 형태를 사용합니다. 요청은cursor·size쿼리 파라미터로만 받으며, 응답도 offset이 아니라 커서(nextCursor)로 이어집니다. 204 No Content는 envelope를 포함해 본문이 전혀 없습니다.
명세 §1.4가 정한 cursor·size 계약을 아래 타입으로 구현했습니다(결정 배경은 BD-04 참고).
- 목록 응답 타입은
global/response/CursorPage<T>이며ApiResponse의data에 담깁니다. - 커서는
global/response/Cursor가 만듭니다 —Base64(정렬키,id), URL-safe·패딩 없음. 클라이언트는 해석하지 않습니다. size기본값은CursorPage.DEFAULT_SIZE(20), 서버 방어 상한은CursorPage.MAX_SIZE(100)입니다. 범위 밖 값은CursorPage.normalizeSize(Integer)가 보정합니다(null·0·음수 → 기본값, 상한 초과 → 상한).- 잘못되거나(Base64가 아님·구분자 없음·id가 숫자가 아님) 비어 있는(
null·빈 문자열) 커서는400(INVALID_INPUT)으로 거절됩니다. - 마지막 페이지에서도
nextCursor는 키가 사라지지 않고 명시적으로null로 노출됩니다.
공통 오류 응답에는 항상 다음 필드를 제공합니다.
| 필드 | 의미 |
|---|---|
code |
클라이언트가 분기할 수 있는 안정적인 오류 코드 |
message |
사용자 또는 호출자가 이해할 수 있는 오류 설명 |
traceId |
로그와 요청을 연결하는 추적 식별자 |
새 오류를 추가할 때는 상태 코드, code, 발생 조건과 API 테스트를 함께 추가합니다. validation 오류도 이 공통 오류 계약을 지켜 HTTP 400으로 반환합니다.
결정 배경: BD-03 공통 응답 envelope · BD-04 커서 페이지네이션 · BD-13 권한 실패에 403이 아닌 404를 쓰는 이유 · BD-11 409
DELETE_CONFIRMATION_REQUIRED와error.impact· BD-12 중복 추가를 오류로 보지 않는 이유
요청·응답 계약을 바꾸면 정상 요청과 validation 실패를 테스트하고, 관련 API 문서를 갱신합니다. 인증이 포함된 API는 CONTRIBUTING.md의 인증 변경 검증도 함께 만족해야 합니다.