584 lines
97 KiB
Markdown
584 lines
97 KiB
Markdown
---
|
|
title: branch / feature-api-contract-baseline
|
|
source_type: branch-note
|
|
status: verified
|
|
branch: feature-api-contract-baseline
|
|
parent_branch:
|
|
related_projects: [ca-skeleton]
|
|
tags: [branch, ca-skeleton, api-contract, openapi]
|
|
created: 2026-05-21
|
|
last_reviewed: 2026-06-04
|
|
target_merge:
|
|
status_label: review
|
|
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-011
|
|
kind: project-work-item
|
|
project: ca-skeleton-operational-contract
|
|
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-011
|
|
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-API-VERSIONING-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-OPENAPI-001@1]
|
|
refines: []
|
|
overrides: []
|
|
depends_on: []
|
|
contract_packet: 1
|
|
contract_packet_sha256: a7692d0779a614a3f52af2276360b0ce9e4c9b05f0d1af1b246fbbaa322102a3
|
|
---
|
|
|
|
# branch: feature-api-contract-baseline
|
|
|
|
> Layer: `raw/branch-notes/` — HTTP API surface 전체의 계약을 정의합니다. 완료 후 `/ingest`로 `wiki/projects/`에만 추출합니다.
|
|
> `status_label`: `in-progress` | `review` | `merged` | `abandoned`
|
|
|
|
<!-- section-id: branch-parent -->
|
|
## 부모 (필수)
|
|
|
|
> 이 branch 가 어느 작업 묶음에 속하는지. 본 branch 는 project-note 의 직접 자식 (`parent_branch:` 비어 있음).
|
|
|
|
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
|
|
|
|
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 §13 API Contract Surface · §16 Schema/Serialization (envelope shape 부분) · §25 Default Decisions (API versioning row) 의 결정/근거/금지 사항을 정제한다.
|
|
|
|
<!-- GENERATED: branch-contract:start -->
|
|
<!-- section-id: branch-contract-packet -->
|
|
## 브랜치 계약 패킷
|
|
|
|
- **생성 시 프로젝트 개정**: `1`
|
|
- **패킷 스키마**: `contract_packet: 1`
|
|
- **완료 조건**: /v1 API와 envelope/OpenAPI contract test가 통과한다
|
|
|
|
<!-- section-id: inherited-project-decisions -->
|
|
### 상속한 프로젝트 결정
|
|
|
|
| Decision Ref | Project Summary | Branch Application | Source |
|
|
|---|---|---|---|
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-API-VERSIONING-001@1` | URI prefix /v1이 default이며 X-Api-Version은 compatibility 실험용 보조 header다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-OPENAPI-001@1` | verification suite가 OpenAPI drift의 release-blocking 판정권을 소유한다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
|
|
|
|
<!-- section-id: branch-local-decisions -->
|
|
### 브랜치 지역 결정
|
|
|
|
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
|
|
|
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
|
|---|---|---|---|---|
|
|
|
|
<!-- section-id: declared-overrides -->
|
|
### 선언한 예외
|
|
|
|
| Override ID | Overrides | Reason | Approval | Status |
|
|
|---|---|---|---|---|
|
|
<!-- GENERATED: branch-contract:end -->
|
|
|
|
<!-- section-id: branch-goal -->
|
|
## 목표
|
|
|
|
structured response envelope만으로 API contract는 완성되지 않습니다. versioning · pagination · sorting · filtering · content negotiation · request size · idempotency header · HTTP method semantics · conditional request · cache policy · long-running operation · OpenAPI drift 까지 기본 skeleton 기준으로 고정합니다.
|
|
|
|
- 이슈:
|
|
- PR:
|
|
|
|
<!-- section-id: branch-scope -->
|
|
## 범위
|
|
|
|
### 포함 범위
|
|
|
|
- API versioning 기준.
|
|
- pagination/sorting/filtering 표준 (page index base, size cap, 빈 list shape 포함).
|
|
- idempotency header 표준 (header 이름만 — key shape/scope SSOT 는 sibling).
|
|
- request size limit 실패 분류 (413).
|
|
- URI 길이 실패 분류 (414).
|
|
- multipart/file upload 실패 분류 (위임).
|
|
- content negotiation 실패 분류 (406/415).
|
|
- HTTP method 미지원 실패 분류 (405 + `Allow` header).
|
|
- HTTP method 의 safe / idempotent 분류 + PATCH 의 media type 결정.
|
|
- conditional request / concurrency at HTTP layer (`ETag`, `If-Match`, `If-None-Match`, 304 Not Modified, 412 Precondition Failed).
|
|
- response cache 정책 default + `Vary` header 의무.
|
|
- HEAD / OPTIONS support 의무 (GET 지원 endpoint 는 HEAD MUST).
|
|
- HTTP status code ↔ envelope `error.category`/`error.code` 의 전체 매핑 SSOT 위치 결정.
|
|
- long-running operation 응답 패턴 (202 + `Location` + polling endpoint).
|
|
- resource URL naming convention (plural + lowercase + AIP-122 regex).
|
|
- sort parameter syntax (Spring `Pageable` native).
|
|
- filter parameter syntax (flat key=value equality only).
|
|
- cursor pagination shape (opaque base64 JSON + HMAC + 24h TTL).
|
|
- bulk operation URL pattern (AIP-136 colon-verb `:batchCreate`).
|
|
- response Date header 자동 발행 (Spring/Tomcat default).
|
|
- OpenAPI schema와 실제 응답 계약 일치 검증.
|
|
|
|
### 제외 범위
|
|
|
|
> 의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.
|
|
|
|
- business-specific endpoint 설계.
|
|
- API gateway / WAF / reverse proxy 설정 (gateway-pre-reject 의 envelope-bypass 정책만 본 branch 가 *명시*).
|
|
- public API product policy.
|
|
- CORS allowlist / credentials / preflight policy — **owner**: [[raw/branch-notes/feature-security-operational-baseline]] D9. 본 branch 는 OPTIONS 응답이 envelope 우회한다는 점만 cross-cite.
|
|
- response cache layer 구현 (Redis / CDN) — **owner**: [[raw/branch-notes/feature-cache-consistency-contract]]. 본 branch 는 HTTP 응답 header 정책만.
|
|
- webhook outbound contract (signature header, replay protection, retry semantics) — 별도 branch 신설 필요. 현재 ca-skeleton 범위 밖.
|
|
- Server-Sent Events / WebSocket / long polling / streaming response — ca-skeleton 은 request-response 만 지원. SSE/WS 도입은 별도 branch.
|
|
- `X-HTTP-Method-Override` / `_method` form parameter — forbid 가 기본값이지만 *결정 자체*는 security 계약 영역. cross-cite 로만.
|
|
- `Server` / `X-Powered-By` / 기술 스택 노출 header — **owner**: security branch. 본 branch 는 forbid 만 cross-cite.
|
|
- error message i18n (`Accept-Language`) — 현재 envelope `error.message` 는 한국어/영어 어느 default 인지 *미정*. 본 branch 는 결정 안 함, schema/serialization 또는 별도 branch 위임.
|
|
- response body compression negotiation (`Accept-Encoding` / `Content-Encoding` / gzip / br) — reverse proxy/gateway 책임으로 위임. Spring 자체 `server.compression.enabled` 는 dev/staging 에서 옵션.
|
|
- response field naming case (camelCase vs snake_case) — **owner**: [[raw/branch-notes/feature-schema-serialization-contract]]. 본 branch 는 envelope `meta.*` 가 camelCase 라는 cross-cite 만.
|
|
- resource ID format 자체는 본 branch 범위 밖이며 [[raw/branch-notes/feature-resource-identifier-contract]] D1/D19가 ULID를 소유한다. 본 branch는 URL 구조와 `{id}` placeholder 연결만 소유한다.
|
|
- multipart / file upload body 처리 — **owner**: [[raw/branch-notes/feature-file-resource-handling-contract]]. 본 branch 는 415 분류만.
|
|
|
|
## 근거 (필수, 최소 1개+)
|
|
|
|
> 본 branch의 결정 근거. 상세 대안 비교는 §외부 근거 / 대안 조사 sub-section 참조. 같은 자료가 여러 결정의 근거면 여러 번 등장 가능.
|
|
|
|
| Source | 정당화하는 결정 |
|
|
|---|---|
|
|
| [[raw/official-docs/idempotency-stripe-api-ref]] | Stripe `Idempotency-Key` header 표준 (D3) — `official-vendor-doc` |
|
|
| [[raw/official-docs/idempotency-ietf-draft]] | IETF httpapi draft가 동일 header 이름 정의 (D3) — `official-reference` (draft 상태) |
|
|
| [[raw/company-tech-blogs/idempotency-brandur-stripe-postgres]] | Postgres 구현 reference (D3 보조) — `company-case-study` |
|
|
| [[raw/official-docs/idempotency-paypal-docs]] | header 이름 `PayPal-Request-Id`로 다름 (D3 대안) — `official-vendor-doc` |
|
|
| [[raw/official-docs/idempotency-aws-lambda-powertools]] | header 불요, server-derived (D3 대안) — `official-vendor-doc` |
|
|
| [[raw/official-docs/idempotency-square-api]] | body 필드로 받음, header 표준 미준수 (D3 대안) — `official-vendor-doc` |
|
|
| [[raw/company-tech-blogs/idempotency-toss-payments-techblog]] | header 이름 `Idempotency-Key` 동일 (D3 보조) — `company-case-study` |
|
|
| [[raw/official-docs/idempotency-no-api-level-github-rest]] | header 자체 없음 (D3 대안) — `official-vendor-doc` |
|
|
| [[raw/company-tech-blogs/idempotency-redis-vs-db-storage]] | ca-tmpl이 DB 선택 근거 (D3 보조, header layer 만) — `company-case-study` |
|
|
| [[raw/official-docs/google-aip-185-resource-versioning]] | URI `/v1` major-only path versioning 근거 (D2, D6) — `official-reference` |
|
|
| [[raw/official-docs/api-versioning-google-aip-180]] | backward compatibility 의무 cross-cite (D6) — `official-reference` |
|
|
| [[raw/official-docs/jsonapi-pagination-format]] | pagination link key 명명 + `links` object 위치 표준 (D7) — `official-standard` |
|
|
| [[raw/official-docs/rfc9110-http-semantics]] | HTTP 의미론 normative — D8 (413), D9 (406/415), D12 (405 + Allow), D13 (HEAD/OPTIONS), D15 (ETag/If-Match/If-None-Match/304/412), D16 (Vary), D17 (202 + Retry-After), D24 (Date), D8 형제 (414) — `official-standard` |
|
|
| [[raw/official-docs/openapi-spec-3-1-0]] | OAS = machine-readable HTTP API contract — manual stale schema 금지 근거 (D10) — `official-standard` |
|
|
| [[raw/official-docs/patch-json-merge-rfc7396]] | IETF RFC 7396 Standards Track — **미채택 근거**. RFC7396-C3 ("explicit null 사용 모델에 부적합") 가 본 branch 의 envelope 정책 + boundary branch B2 의 absent/null 3-상태 mapper 결정과 충돌 — *미채택의 직접 normative 근거*. RFC7396-C2 (null=deletion) 는 대안으로 인용 — `official-standard` |
|
|
| [[raw/official-docs/google-aip-151-long-running-operations]] | AIP-151: LRO 패턴 — Operation `done`/`result`/`error` 분기 + `name` 필드 polling 의무 (D17) — `official-reference` |
|
|
| [[raw/official-docs/rfc9111-http-caching]] | IETF RFC 9111 (HTTP Caching) — `no-store` / `private` / `public` / `max-age` directive normative 정의 (D16 cache policy default) — `official-standard` |
|
|
| [[raw/official-docs/google-aip-122-resource-names]] | (future B13 — 미결) Resource URL naming convention — collection segment plural + lowercase 근거 (AIP122-C2, AIP122-C3). sample-portfolio `/v1/worklogs` collection name 명명 기준 — `official-reference` |
|
|
| [[raw/official-docs/google-aip-136-custom-methods]] | (future B18 — 미결) Bulk operation URL pattern — colon-verb suffix syntax + collection-based custom method 원칙. D17 LRO cross-ref: custom method 가 LRO entry point 가 될 수 있음 (AIP136-C1~C5) — `official-reference` |
|
|
| [[raw/official-docs/fetch-spec-cors]] | WHATWG Fetch §3.3 CORS protocol — D13 (OPTIONS preflight envelope 우회) 의 normative 근거. preflight = OPTIONS + Access-Control-Request-Method (FETCH-CORS-C2). CORS safelisted method: GET/HEAD/POST — `official-standard` |
|
|
| [[raw/official-docs/google-aip-132-list-method]] | AIP-132 List method: `order_by` syntax (`"foo desc, bar"` 형식, AIP132-C4) + `page_size`/`page_token`/`next_page_token` proto field 명명 (future B14 sort syntax 결정 근거 후보) — `official-reference` |
|
|
| [[raw/official-docs/google-aip-158-pagination]] | AIP-158 Pagination: `page_size` server-side cap SHOULD coerce (AIP158-C2), `next_page_token` empty = EoC (AIP158-C4), page token opaque + URL-safe (AIP158-C5). D18 size cap + (future B16) cursor pagination shape 근거 — `official-reference` |
|
|
| [[raw/official-docs/google-aip-160-filtering]] | AIP-160 Filtering: filter DSL syntax (Common Expression Language) 옵션 정의 (future B15 filter syntax 결정의 1개 옵션 근거) — `official-reference` |
|
|
| [[raw/official-docs/spring-data-pageable-defaults]] | Spring Data `Pageable` zero-indexed (SPRING-PAGE-C1/C3) + `size` default 20 (SPRING-PAGE-C2) + `DEFAULT_MAX_PAGE_SIZE = 2000` (SPRING-PAGE-C4 — Integer.MAX_VALUE 가 아님). D18 정합성 근거 — `official-vendor-doc` |
|
|
|
|
근거 자료가 raw에 아직 없다면 먼저 `raw-source-template` 으로 raw에 등록한 뒤 여기서 링크.
|
|
|
|
### 외부 근거 / 대안 조사 (2026-05-22 — Topic 5: Idempotency-Key)
|
|
|
|
본 branch의 `Idempotency-Key` HTTP header 및 idempotent command 정책 결정 (D3) 에 대한 외부 source. key shape SSOT는 `feature-rate-limit-idempotency-contract` (consume only). 비교 분석은 (예정) `wiki/concepts/idempotency-key-design.md` 참조.
|
|
|
|
- **채택 결정 (header 이름 `Idempotency-Key`, idempotent command에만 적용)**:
|
|
- (가장 가까운 reference) [[raw/official-docs/idempotency-stripe-api-ref]] — Stripe `Idempotency-Key` header 표준
|
|
- [[raw/official-docs/idempotency-ietf-draft]] — IETF httpapi draft가 동일 header 이름 정의
|
|
- [[raw/company-tech-blogs/idempotency-brandur-stripe-postgres]] — Postgres 구현 reference
|
|
- **검토한 대안**:
|
|
- **대안 1: Stripe v1 pair `(account, key)`** — [[raw/official-docs/idempotency-stripe-api-ref]] (endpoint dimension 없음, ca-tmpl보다 덜 보수적)
|
|
- **대안 2: PayPal `(req-id, API call type)` + 45일 TTL** — [[raw/official-docs/idempotency-paypal-docs]] (header 이름 `PayPal-Request-Id`로 다름)
|
|
- **대안 3: Content-hash `(fn, payload_hash)`** — [[raw/official-docs/idempotency-aws-lambda-powertools]] (header 불요, server-derived)
|
|
- **대안 4: Square endpoint-scoped** — [[raw/official-docs/idempotency-square-api]] (body 필드로 받음, header 표준 미준수)
|
|
- **대안 5: 토스 4-tuple `(account, key, URL, method)` + 15일 TTL** — [[raw/company-tech-blogs/idempotency-toss-payments-techblog]] (header 이름 `Idempotency-Key` 동일)
|
|
- **대안 6: No API-level idempotency** — [[raw/official-docs/idempotency-no-api-level-github-rest]] (header 자체 없음)
|
|
- **보조 결정 (저장소)**: [[raw/company-tech-blogs/idempotency-redis-vs-db-storage]] — ca-tmpl이 DB 선택 근거 (이 branch는 header layer만)
|
|
- **비교 핵심**: API baseline은 header 이름만 결정. shape/scope는 rate-limit-idempotency branch가 owns. Stripe/Toss/Square 모두 `Idempotency-Key` 또는 동등 header를 사용 — header 이름은 사실상 industry de facto.
|
|
|
|
## TODO
|
|
|
|
각 항목 옆에 증거 등급 표기: `actually-implemented` | `locally-verified` | `prod-verified` | `documented-only` | `planned` | `needs-confirmation`
|
|
|
|
> TODO drained — 결정은 §결정 사항 / Decisions 표 + §구현 가이드 §2 Decisionized Work Items 표 참조. multipart/file upload 는 `feature-file-resource-handling-contract` 로 위임.
|
|
|
|
## 진행 중 메모
|
|
|
|
- API contract 는 controller 구현보다 먼저 고정되어야 한다.
|
|
|
|
### Phase C2 구현 결과 (2026-06-02)
|
|
|
|
ca-tmpl 실 코드에 producer-소유 결정을 구현하고 sample-portfolio 를 계약에 정합시켰다. 사용자 결정: **전체 구현 + 샘플 정합**, 차단 항목은 **producer seam + planned**.
|
|
|
|
- `locally-verified` (단위/슬라이스/임베디드 테스트로 검증):
|
|
- D8 413 (`PAYLOAD_TOO_LARGE`) · D9 406/415 distinct · D12 405 + `Allow` — `GlobalExceptionHandler` override + `TransportErrorHandlingTest`.
|
|
- D15 ETag/`If-Match`→412/`If-None-Match`→304 — `adapter-web` `ETags`/`PreconditionFailedException` + sample `WorkLog.version`(@Version) + `WorkLogControllerWireTest`.
|
|
- D7/D18 pagination `meta.page` + size 1..100/page≥0 → 400 + 빈 list `[]` + deep-offset `Deprecation` — `PageParams`/`PageMeta`/`ResponseMeta.page` + wire test.
|
|
- D20 sort 네이티브 syntax(비-네이티브 400) — `SortParam` + wire test. D21 flat key=value filter — wire test.
|
|
- D16 default `Cache-Control: no-store` + `Vary` (+ Security 기본 cache-control 비활성으로 단일 owner) — `CacheControlFilter` + test.
|
|
- D19 AIP-122 URL 네이밍 — ArchUnit `controller_request_mappings_follow_aip122` + `KebabPathControllerFixture` + violations-as-data. sample 경로 `/work-logs`→`/worklogs`, `/repo-stats`→`/worklogs/repoStats`.
|
|
- D23 sync atomic `:batchCreate` (AIP-136 colon-verb, partial 금지) — `BatchCreateWorkLogsUseCase`(단일 tx) + wire test.
|
|
- D11 status↔registry 정합성 — `ErrorCodeRegistryMappingTest` (error-codes.yaml 의 405/406/412/413/414/415 row 추가, drift FAIL).
|
|
- D10 OpenAPI producer — springdoc `/v3/api-docs` 임베디드 컨테이너 테스트(`OpenApiSnapshotTest`).
|
|
- D2 `/v1` 기본 prefix — application.yml `PRESENTATION_API_BASE_PATH:/v1`.
|
|
- D22 cursor **seam** — `adapter-web` `CursorCodec`(opaque base64 + HMAC + 24h TTL) + `CursorCodecTest` (opacity/integrity/TTL 3-invariant = §3 D22 요구 충족).
|
|
|
|
#### 소유 범위 gap 보완 (2026-06-02, 2차 패스)
|
|
|
|
1차 패스에서 `planned` 로 둔 것 중 **차단되지 않은 소유 결정**을 추가 구현(§3 Test Contract 항목 기준):
|
|
|
|
- D13 HEAD-mirror-GET — `WorkLogControllerWireTest.head_on_get_endpoint_is_supported_not_405` (405/404 아님).
|
|
- D23 batch size cap — `BatchCreateRequest @Size(max=1000)` + `batch_over_size_cap_is_400` (1001→400).
|
|
- D3 `Idempotency-Key` POST surface — `create`/`batchCreate` 의 `@RequestHeader`(server-tolerant) + `post_accepts_idempotency_key_header` (shape는 여전히 rate-limit branch).
|
|
- D2 versioning 강제 — `VersioningPrefixTest` (`/v1/probe` 200, `/probe` 404 → unversioned public endpoint 불가).
|
|
- D21 filter DSL 미파싱 — `filter_dsl_is_ignored_not_parsed` (`?filter=status==OPEN` 무시).
|
|
- D17 LRO endpoint — `SampleOperationStore`(id를 controller 밖에서 mint) + `OperationsController`(`POST /worklogs:export` 202+`Location`+`data.{operationId,statusUrl}`, `GET /operations/{id}` polling) + `OperationsControllerWireTest`.
|
|
- D24 Date matrix — `DateHeaderContractTest` (임베디드 Tomcat, 200·404 응답에 `Date` 헤더).
|
|
|
|
- `planned` (실제 차단 — 형제 branch/인프라): D3 key shape/replay (rate-limit), D5/D10 drift 릴리스 게이트 (verification-test-suite), D16 cache layer (cache), D22 HMAC 키 회전 (security), D8 **414 end-to-end** (Tomcat/gateway가 Spring 디스패치 전 거부 — code+registry row만), D23 async partial (boundary B14), D22 sample cursor endpoint (§3 미요구, optional).
|
|
- 검증: `./gradlew check` + `verifyCleanArchitectureDependencies` + `*CleanArchitectureTest`/`*ArchitectureViolationFixtureTest` 모두 PASS.
|
|
- 구현 계획서: ca-tmpl `docs/superpowers/plans/2026-06-02-api-contract-baseline.md`.
|
|
|
|
### Ground-truth 대조 (2026-06-04, ca-tmpl @b15dcf5)
|
|
|
|
`/ingest` reconcile 시 ca-tmpl commit `b15dcf5` ("API 계약 baseline 구현") 의 실제 코드(package root `dev.caskeleton.*`)와 1:1 대조해 위 `locally-verified` 항목을 확정했다. 실재 확인 클래스/파일:
|
|
|
|
- `adapter-web/conditional/{ETags,PreconditionFailedException}` (D15), `adapter-web/filter/CacheControlFilter` (D16), `adapter-web/pagination/{PageParams,SortParam}` (D18/D20), `adapter-web/cursor/{CursorCodec,CursorException}` (D22 seam), `adapter-web/error/GlobalExceptionHandler` (D8/D9/D12 + 412 매핑).
|
|
- `shared-contract/response/{PageMeta,ResponseMeta}` (D7/D18), `shared-contract/operation/{Operation,OperationStatus}` (D17).
|
|
- `sample-portfolio/.../controller/{WorkLogController,OperationsController}` (D15/D23/D17), `.../operation/{SampleOperationStore,WorkLogExportResult}`.
|
|
- versioning: `app-bootstrap/.../application.yml` `ca-skeleton.presentation.api-base-path: ${PRESENTATION_API_BASE_PATH:/v1}` + `adapter-web/settings/PresentationSettings` (코드 default `""`, 운영 default `/v1`) (D2).
|
|
- OpenAPI: `adapter-web/build.gradle` `springdoc-openapi-starter-webmvc-api:2.8.6` + `OpenApiSnapshotTest` `/v3/api-docs` (D10).
|
|
- 테스트: `TransportErrorHandlingTest`, `WorkLogControllerWireTest`, `CacheControlFilterTest`, `CursorCodecTest`, `ETagsTest`, `PageParamsTest`, `SortParamTest`, `OperationsControllerWireTest`, `VersioningPrefixTest`, `DateHeaderContractTest`, `ErrorCodeRegistryMappingTest`.
|
|
|
|
UNSUPPORTED_IMPL_DECISION 확인된 잔존: pagination size cap 100/min 1/deep-offset 10000, ETag lenient(weak) 비교(RFC 9110 strong MUST 와 차이), cursor 24h TTL + HMAC-SHA256, LRO status enum 5종. planned 잔존: D22 HMAC 운영 key/회전(security), D8 414 end-to-end(Tomcat pre-dispatch), D3 key shape/replay(rate-limit), D5/D10 drift 릴리스 게이트(verification-suite), D16 cache layer(cache).
|
|
|
|
추출 결과: [[wiki/projects/ca-tmpl/api-evolution-and-schema]] 의 "API contract baseline 구현" 절 + [[wiki/concepts/api-evolution-and-schema]] 의 HTTP contract surface 표준/Claim-backed Knowledge.
|
|
|
|
## 결정 사항
|
|
|
|
> 추후 면접/회고에서 "왜 이렇게 했나" 답할 근거. 대안과 함께 기록. 각 결정의 근거는 위 §Sources 또는 §Decision Evidence Map 의 Supporting Claims 참조.
|
|
|
|
- 2026-05-21: envelope 응답 외 API surface도 skeleton 계약에 포함 (D1).
|
|
- 2026-05-22: API versioning 기본값은 URI prefix `/v1`. `X-Api-Version`은 실험/compatibility 보조 header이며 path version과 충돌하면 path가 우선 (D2).
|
|
- 2026-05-22: idempotency header 이름은 `Idempotency-Key`, key scope와 replay semantics의 SSOT는 [[raw/branch-notes/feature-rate-limit-idempotency-contract]] (D3, D4).
|
|
- 2026-05-22: OpenAPI drift의 release-blocking 집행권은 [[raw/branch-notes/feature-contract-verification-test-suite]]가 단일 owner이며 이 branch는 producer (D5).
|
|
- 2026-05-31: **HTTP status code ↔ envelope `error.category`/`error.code` 매핑 SSOT** = `feature-operational-error-observability-foundation` 의 `error-codes.yaml` (registry §21 row 49 + `http_status` column). 본 branch 는 *registry 의 매핑 정합성 contract test* 의 producer. registry row 와 실제 controller 응답의 drift 는 contract test 가 release-blocking (D11).
|
|
- 2026-05-31: **HTTP method 미지원** 응답은 405 Method Not Allowed + `Allow` response header 의무. `Allow` header 는 해당 URL 이 지원하는 method 의 comma-separated 목록. Spring 의 `HttpRequestMethodNotSupportedException` 가 envelope 우회로 직접 응답하면 contract 위반 (D12).
|
|
- 2026-05-31: **GET 을 지원하는 endpoint 는 HEAD 도 자동 지원** (Spring MVC 가 자동 처리하나 contract test 로 검증 의무). OPTIONS 는 CORS preflight 또는 resource 자체 metadata 응답으로 분기 — CORS preflight 는 envelope 우회 (security branch SSOT), resource metadata 용도는 envelope 따름 (D13).
|
|
- 2026-05-31 (정정): **PATCH 의 default media type 은 `application/json`** (RFC 7396 `application/merge-patch+json` *미채택*). request shape 는 `JsonNullable<T>` (openapi-generator) 또는 `Optional<T>` wrapper 로 **absent / null / value 3-상태 구분** — absent = 변경 없음, null = 명시적 null/clear, value = 새 값. RFC 7396 null=deletion semantics 는 envelope success/error 대칭 정책과 충돌하여 *미채택* (RFC7396-C3 가 "explicit null 사용 모델에 부적합" normative). `application/merge-patch+json` content type 사용은 [[raw/branch-notes/feature-boundary-validation-mapping-contract]] B2 의 ArchUnit rule `no_merge_patch_json_media_type_string` 으로 build 실패 차단. RFC 6902 (`application/json-patch+json`) 도 동일 이유로 미채택.
|
|
- 2026-05-31: **Conditional request 지원**: read 응답에 `ETag` header 발행 (sample-portfolio 의 `WorkLogVersion` 같은 version field 가 있으면 derived ETag, 없으면 content hash). write request 는 `If-Match` 헤더로 optimistic concurrency 검증 — mismatch 시 412 Precondition Failed (envelope 따름). `If-None-Match` 로 cache validation — match 시 304 Not Modified (body 없음, envelope 우회). `If-Match` 누락된 write 는 *허용* 하되, contract test 로 sample-portfolio 에서 *권장 패턴* 검증 (D15).
|
|
- 2026-05-31: **응답 cache 정책 default**: 모든 응답에 `Cache-Control: no-store` (인증된 API 의 안전한 default). 명시적으로 cacheable 한 endpoint 만 controller annotation 으로 `Cache-Control: private, max-age=N` opt-in. content negotiation 또는 인증된 응답에는 `Vary: Accept, Accept-Encoding, Authorization` 헤더 의무 — proxy/CDN cache poisoning 방지 (D16).
|
|
- 2026-05-31: **Long-running operation (LRO) 응답 패턴**: 비동기 처리 endpoint 는 202 Accepted + `Location: /v1/operations/{id}` + envelope `data.operationId` + `data.statusUrl`. polling endpoint (`GET /v1/operations/{id}`) 는 `status` ∈ {`PENDING`, `RUNNING`, `SUCCEEDED`, `FAILED`, `CANCELLED`}. `Retry-After` 헤더로 polling interval 권고. Webhook callback 은 별도 branch (D17).
|
|
- 2026-05-31: **Pagination size cap + index base 강제**: `page` 0-indexed (Spring `Pageable` default 와 정합), `size` 기본 20 + 최대 100 + 최소 1. `size > 100` 또는 `size < 1` 또는 `page < 0` 은 400 VALIDATION_FAILED. 빈 list 는 `data: []` (절대 `null` 아님), `meta.page.total = 0`. 깊은 offset pagination (예: `page > 10000`) 은 `Deprecation` 헤더 + 권고: cursor pagination 사용 — cursor endpoint 의 shape 결정은 별도 후속 작업 (D18, D7 row 보강).
|
|
- 2026-05-31: **본 branch 의 cross-branch consumer/producer 관계**: §구현 가이드 §4 Cross-branch Contract Map 참조.
|
|
- 2026-05-31: **Resource URL naming convention** = `plural` + `lowercase` + AIP-122 regex `[a-z][a-zA-Z0-9]*`. single-word resource: `/v1/worklogs` · multi-word: `lowerCamelCase` (예: `/v1/worklogComments`). **kebab-case 금지** (AIP122-C3 regex 위반 — `/v1/worklog-comments` ❌). singular path 금지 (`/v1/worklog/{id}` ❌). CamelCase 금지 (case-sensitivity footgun) (D19).
|
|
- 2026-05-31: **Sort parameter syntax** = Spring `Pageable` native `?sort=field,direction` (`?sort=createdAt,desc`). multi-sort 는 param repeat (`?sort=createdAt,desc&sort=title,asc`). 다른 syntax (`?sort=-foo`, `?sort=foo:desc`, `?order_by=foo desc`) 금지 — Spring 자동 binding 깨짐 (D20).
|
|
- 2026-05-31: **Filter parameter syntax** = flat key=value (equality only). `?status=OPEN&owner=user123` 만 허용. 복잡 filter (range / `in` / `like` / `AND/OR` 조합) 는 *out of scope* — 필요 시 별도 branch 또는 GraphQL 도입 시점 재검토. AIP-160 DSL / RSQL / FIQL / JSON:API bracket syntax 모두 *미채택* (parsing/security 부담 + ergonomics 낮음) (D21).
|
|
- 2026-05-31: **Cursor pagination shape** = opaque base64-encoded JSON token + server-side HMAC signature (tamper detection) + 24h TTL. cursor endpoint 는 별도 query param (`?cursor=<token>&size=20`) 또는 별도 path (`/v1/worklogs:listByCursor`). client 는 token parse 금지 (opacity 강제 — AIP158-C5 normative). cursor + 전통적 `?page=N` 동시 사용 금지 — 별도 endpoint (D22).
|
|
- 2026-05-31: **Bulk operation URL pattern** = AIP-136 colon-verb `POST /v1/{resource}:batchCreate` (verb suffix). request body = `{ requests: [...] }`. **sync vs async 명확 분기 (AIP233-C7 MUST atomic 정합)**: (a) **sync batch endpoint** = MUST **atomic** (all-or-nothing). 한 항목 실패 시 전체 rollback + HTTP 4xx (예: 400 VALIDATION_FAILED + envelope.success=false). partial failure 허용 안 함. (b) **async batch endpoint** = D17 LRO pattern 결합 — `POST /v1/{resource}:batchCreate` 가 202 Accepted + `Location: /v1/operations/{id}` 반환 → polling endpoint `GET /v1/operations/{id}` 의 `data.result.results[]` 에서 항목별 success/error 반환 (partial failure 허용). `BATCH_PARTIAL_FAILURE` envelope category 는 **async batch 의 polling 응답에서만** 사용. flat array body (`POST /v1/worklogs` with `[...]`) 금지. kebab subpath (`POST /v1/worklogs/batch-create`) 금지 (D23).
|
|
- 2026-05-31: **Response Date header** = 모든 응답에 `Date` 헤더 자동 발행 (Spring/Tomcat default). controller 별도 설정 불요. Date header 명시적 비활성화 금지. log correlation + RFC 9110 §6.6.1 SHOULD 정합 (D24).
|
|
|
|
## 결정-근거 매핑
|
|
|
|
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. `Decision ID` 는 본 branch-note 안에서 안정적으로 유지. `Supporting Claims` 는 `raw/<category>/<slug>.md#<ClaimID>` 형식. company-tech-blog 출처는 `company-case-study` 로 라벨링하며 공식 best practice 로 격상하지 않는다.
|
|
|
|
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|
|
|---|---|---|---|---|
|
|
| D1 | envelope 외 API surface 도 skeleton 계약에 포함 (2026-05-21) | UNSUPPORTED_DECISION (project-internal scoping decision; no external standard cited) | N/A | scope drift — wiki/projects 추출 시 본 결정의 근거를 별도 design 문서로 보강 필요 |
|
|
| D2 | API versioning 기본값 `/v1` URI prefix + `X-Api-Version` 은 supplemental | `raw/official-docs/google-aip-185-resource-versioning.md#AIP185-C1` (major version 노출 의무), `#AIP185-C2` (minor/patch 노출 금지 — `/v1.0` 금지 → `/v1`), `#AIP185-C3` (새 major 는 이전 major 에 의존 금지) | `official-reference` (Google AIP — Google 사내 community guideline; IETF/W3C 표준 아님) | AIP-185 본문은 protobuf 컨텍스트 — REST URI path vs header 선택 자체에 대한 normative 진술은 본 인용 범위 밖. `X-Api-Version` 을 supplemental 로 두는 결정의 직접 근거는 별도 (예: ca-tmpl internal design) — AIP-185 는 path 형식 (`/v1`) 만 corroborate. **UNSUPPORTED_IMPL_DECISION**: path version 과 `X-Api-Version` 충돌 시 *path 우선* 규칙은 project-internal convention — AIP-185 에 path/header 우선순위 normative 진술 없음. trade-off: URI 가 1급 계약 표면(캐시·라우팅·로그에서 가시)이므로 path 를 권위 source 로, header 는 실험/전환 보조로 둠 |
|
|
| D3 | idempotency header 이름 `Idempotency-Key` 채택 (POST endpoint 표준 surface) | `raw/official-docs/idempotency-stripe-api-ref.md#STRIPE-IDEMP-C1`, `raw/official-docs/idempotency-ietf-draft.md#IETF-IDEMP-C1`, `raw/company-tech-blogs/idempotency-toss-payments-techblog.md#TOSS-IDEMP-C1` | `official-vendor-doc + official-reference + company-case-study` | IETF-IDEMP 는 draft 상태 (정식 RFC 아님); TOSS 는 company-case-study — 표준 lock-in 아님. PayPal 은 다른 header (`PayPal-Request-Id`) 사용 — 호환성은 별도 |
|
|
| D4 | key scope / replay semantics SSOT 는 `feature-rate-limit-idempotency-contract` 로 위임 (이 branch 는 header 이름만 결정) | UNSUPPORTED_DECISION (project-internal SSOT 분할 결정; 외부 표준 근거 없음) | N/A | sibling branch 와의 결정 정합성은 cross-branch review 로 확보 필요 |
|
|
| D5 | OpenAPI drift release-blocking 집행은 `feature-contract-verification-test-suite` 가 owner; 이 branch 는 producer | UNSUPPORTED_DECISION (project-internal owner 분할 결정) | N/A | producer-owner contract 가 깨지면 drift 가 untracked — verification suite 의 입력 spec 정합성 추적 필요 |
|
|
| D6 | versioning Decisionized Work Item — media-type/header/path version 혼용 금지 | `raw/official-docs/google-aip-185-resource-versioning.md#AIP185-C4` (alpha/beta 만 stability level append, stable 은 append 금지 — version 표기 일관성), `#AIP185-C5` (beta 는 stable 의 superset — channel 간 일관성), `#AIP185-C6` (deprecated 기능은 채널 승격 금지); cross-cite `raw/official-docs/api-versioning-google-aip-180.md#AIP180-C1`~`C5` (backward compatibility 의무) | `official-reference` (AIP-185 + AIP-180 Google 사내 guideline 양쪽 cross-cite) | AIP-185/180 은 version 표기 일관성과 호환성을 normatively 요구하나 "path vs header vs media-type 셋 중 하나만 써야 한다" 는 직접 진술은 본 인용에 포함 안됨 — 혼용 금지는 일관성 원칙의 본 branch 적용 (project-internal 해석) |
|
|
| D7 | pagination — `page`/`size`/`sort` request + `meta.page` response | `raw/official-docs/jsonapi-pagination-format.md#JSONAPI-PAGE-C1` (pagination 은 `MAY` — 옵션), `#JSONAPI-PAGE-C2` (pagination link 는 `links` object 안에 `MUST`), `#JSONAPI-PAGE-C3` (`first`/`last`/`prev`/`next` 4개 key `MUST`) | `official-standard` (JSON:API v1.1 community spec) | JSON:API 는 link key 명명 (`first/last/prev/next`) 과 위치 (`links` object) 를 normatively 정의 — 본 branch 의 `meta.page` envelope shape 와는 **다름**. JSON:API 표준 그대로가 아닌 `meta.page` shape 채택은 project-internal 해석 (envelope contract 와의 통합 우선). pagination 전략 자체 (offset vs cursor) 는 `JSONAPI-PAGE-C6` 가 agnostic 명시 — 본 branch 의 `page`/`size` (offset-style) 선택은 별도 결정 |
|
|
| D8 | request size limit — oversized request 가 raw 500 으로 가면 실패 | `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C5` (413 Content Too Large = server 가 request content 가 너무 커서 처리 거부), `#RFC9110-C6` (413 이 일시적이면 `Retry-After` 헤더 생성 SHOULD) | `official-standard` (IETF RFC 9110) | RFC 9110 은 413 이 의미적으로 "oversized request 의 정상 응답" 임을 normatively 정의하므로 envelope wrapping 자체는 별도 application 책임. raw 500 으로 변환되면 본 의미론 위반 — 본 결정의 직접 근거. envelope shape (VALIDATION vs RATE_LIMIT category 매핑) 은 owner branch 책임으로 위임됨 |
|
|
| D9 | content negotiation — 415 / 406 distinct codes 사용 | `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C4` (406 Not Acceptable = 응답 표현 협상 실패 — `Accept` 계열 헤더 부적합), `#RFC9110-C7` (415 Unsupported Media Type = 요청 본문 format 미지원), `#RFC9110-C8` (415 trigger 는 `Content-Type`/`Content-Encoding` 또는 데이터 직접 검사) | `official-standard` (IETF RFC 9110) | RFC 9110 은 406 (응답 표현) 과 415 (요청 본문) 를 의미적으로 구별 — 동일 error code 로 뭉개면 표준 의미 손실. 본 결정의 직접 근거. Spring 의 `HttpMediaTypeNotSupportedException` / `HttpMediaTypeNotAcceptableException` 매핑은 Spring vendor 책임 — 검증은 `Claims To Verify` 표 참조 |
|
|
| D10 | OpenAPI producer — generated snapshot, manual stale schema 금지 | `raw/official-docs/openapi-spec-3-1-0.md#OPENAPI31-C2` (OAS = HTTP API 에 대한 standard, language-agnostic interface — machine-readable discover/understand), `#OPENAPI31-C4` (Data Type 은 JSON Schema 2020-12 base — schema validation 정합성) | `official-standard` (OpenAPI Initiative — Linux Foundation OAS 3.1.0) | OAS 3.1 은 "machine-readable contract" 를 정의하므로 manual stale schema 는 본 표준의 목적 (discover/understand) 자체를 위반 — 본 결정의 의미론적 근거. 단 OAS 본문은 "snapshot 을 어떻게 생성해야 하는지" (e.g., springdoc-openapi 같은 도구) 는 normative 하지 않음 — 도구 선택은 vendor/project 책임 |
|
|
| D11 | HTTP status code ↔ envelope `error.category`/`error.code` 매핑 SSOT 는 `error-codes.yaml` (foundation branch 소유 registry §21 row 49 + `http_status` column). 본 branch 는 mapping consistency contract test 의 producer | UNSUPPORTED_DECISION (project-internal SSOT 위치 결정; 외부 표준 직접 근거 없음 — RFC 9110 §15.x 가 *개별 status code* 의미를 정의할 뿐 *registry 형식의 SSOT* 자체는 표준 영역 밖); 보강: 개별 row 의 매핑 (예: 404 ↔ `RESOURCE_NOT_FOUND`) 의 normative 근거는 RFC 9110 §15.x 각 status section — 본 branch 의 contract test 가 *registry 와 응답의 drift* 만 검증, *registry 자체의 row 정합성* 은 foundation branch 책임 | N/A | mapping SSOT 가 registry 인 것 자체는 project-internal 결정. RFC 9110 §15.5.6 (405), §15.5.13 (412), §15.5.15 (414) 같은 다른 status code section 의 raw 발췌가 *다음 세션* 작업 — 이후 각 row 의 Supporting Claim ID 보강 가능. cross-branch: registry row 의 *추가/수정* 은 §21 변경 절차 (registry-governance branch) 적용 |
|
|
| D12 | HTTP method 미지원 응답 = 405 Method Not Allowed + `Allow` header 의무 + envelope 따름 | `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C9` (405 = method 알지만 target resource 가 지원 안 함, `Allow` header 생성 MUST), `#RFC9110-C10` (`Allow` header 가 405 응답에서 MUST 생성; empty value = 어떤 method 도 허용 안 함의 정상 표현) | `official-standard` (IETF RFC 9110) | Spring 의 `HttpRequestMethodNotSupportedException` 가 자동 `Allow` 헤더 생성 — contract test 로 envelope wrap + `Allow` 양쪽 모두 검증 의무. 405 응답 body 의 envelope shape 은 표준 외 application 책임 — 본 결정의 envelope 따름 부분은 RFC 9110 가 강제하지 않음 (project-internal) |
|
|
| D13 | GET 을 지원하는 endpoint 는 HEAD 도 MUST 지원 (Spring MVC 자동 처리, contract test 로 verify). OPTIONS 분기: CORS preflight 는 envelope 우회 (security branch SSOT), resource metadata 용도는 envelope 따름 | `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C11` (HEAD = GET 과 동일 의미론, MUST NOT send content), `#RFC9110-C12` (OPTIONS = communication options 요청, resource action 함의 없음 — pure introspection); CORS preflight 식별의 normative 근거는 `raw/official-docs/fetch-spec-cors.md#FETCH-CORS-C2` (preflight = OPTIONS + `Access-Control-Request-Method`) | `official-standard` (IETF RFC 9110 HEAD/OPTIONS 의미론 + WHATWG Fetch CORS preflight 식별 기준) | "GET 지원 endpoint 가 HEAD 도 MUST 지원" 의 *명시적 MUST* 는 RFC9110-C11 인용 자체에는 *함의* 만 포함 — HEAD 의 정의가 "GET 과 동일하나 content 없음" 이므로 GET 지원 시 HEAD 도 자동 의미. Spring MVC 가 이를 자동 mirror — contract test 로 검증 의무. OPTIONS resource metadata 용도는 ca-skeleton 범위에서 *지원 안 함* 옵션도 가능 (opt-in 결정) |
|
|
| D14 | PATCH default media type = `application/json` (RFC 7396 `application/merge-patch+json` *미채택*). request shape = `JsonNullable<T>` / `Optional<T>` wrapper 로 **absent / null / value 3-상태** 구분 · ArchUnit rule SSOT 는 [[raw/branch-notes/feature-boundary-validation-mapping-contract]] B2 | `raw/official-docs/patch-json-merge-rfc7396.md#RFC7396-C3` (merge patch 는 explicit null 사용 모델에 부적합 — *미채택의 직접 근거*), `#RFC7396-C2` (대안: null=deletion normative — 본 branch *대안으로* 인용); [[raw/branch-notes/feature-boundary-validation-mapping-contract]] B2 (PATCH mapper SSOT, ArchUnit `no_merge_patch_json_media_type_string` enforced) | `official-standard` (RFC 7396 — 미채택 근거) + `cross-branch-SSOT` (boundary branch B2) | content type 정책은 본 branch 가 producer, mapper 구현은 boundary branch 책임. 클라이언트가 merge-patch semantics 가정하지 않도록 OpenAPI spec 에 명시. |
|
|
| D15 | Conditional request 지원: read 응답에 `ETag` 발행, write 의 `If-Match` mismatch → 412 Precondition Failed (envelope 따름), read 의 `If-None-Match` match → 304 Not Modified (body 없음, envelope 우회) | `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C13` (ETag = opaque validator, weak/strong 표시 가능), `#RFC9110-C14` (If-Match conditional + strong comparison MUST — representation 변경 시 method 적용 방지가 client 의도), `#RFC9110-C15` (If-None-Match conditional + weak comparison MUST), `#RFC9110-C16` (304 Not Modified = conditional GET/HEAD condition false 시 representation 미전송 + client stored representation 사용), `#RFC9110-C17` (412 Precondition Failed = 하나 이상 condition false 시) | `official-standard` (IETF RFC 9110) | sample-portfolio 의 `WorkLogVersion` 이 ETag derivation 의 1차 source — DB layer 의 optimistic lock 과 HTTP layer 의 412 가 *동일 conflict 의 두 표현* 이라는 점이 본 결정의 의미. RFC 9110 은 ETag 값의 derivation 방법 (version vs hash) 자유 — opaque 성만 강제. `If-Match` 누락 허용 결정은 ca-skeleton 의 "skeleton 은 강제하지 않고 *권장 패턴* 만 fixture 로 보여줌" 정신 — project-internal trade-off (RFC 9110 은 *If-Match 가 있으면* 의 의미론만 정의; 428 Precondition Required 강제 옵션은 RFC 6585 별도) |
|
|
| D16 | 응답 cache 정책 default = `Cache-Control: no-store` (인증된 API 안전 default) · cacheable endpoint 만 controller annotation 으로 `private, max-age=N` opt-in · content negotiation 또는 인증 응답에 `Vary: Accept, Accept-Encoding, Authorization` 의무 | `raw/official-docs/rfc9111-http-caching.md#RFC9111-C1` (`no-store` MUST NOT store — directive normative 정의), `#RFC9111-C2` (`private` = shared cache MUST NOT store, single user), `#RFC9111-C3` (`public` = Authorization 있어도 shared cache 허용), `#RFC9111-C4` (`max-age` = stale 판정 초 수), `#RFC9111-C5` (Cache-Control 헤더 unidirectional 특성); `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C18` (Vary header = response 의 어떤 부분이 content 선택에 영향을 줬는지 description — method/URI 외의 request 부분 명시) | `official-standard` (IETF RFC 9111 §5.2 + RFC 9110 §12.5.5) | proxy/CDN cache poisoning 방지가 본 결정의 운영상 motivation — RFC 9110 + 9111 은 *normative requirement* 를 제공하나 *기본값으로 `no-store` 를 권고* 한다는 진술은 표준 자체에 없음 (안전한 default 는 project-internal trade-off). Vary 가 *없으면* cache poisoning 가능성을 RFC9110-C18 가 의미론적으로 함의 — "MUST generate Vary" 의 명시적 진술은 별도 발췌 필요. cache layer 구현 자체는 [[raw/branch-notes/feature-cache-consistency-contract]] 책임 — 본 branch 는 HTTP header 정책만 |
|
|
| D17 | Long-running operation (LRO) 응답 = 202 Accepted + `Location: /v1/operations/{id}` + envelope `data.{operationId,statusUrl}` · polling endpoint `GET /v1/operations/{id}` 의 `status` ∈ {PENDING, RUNNING, SUCCEEDED, FAILED, CANCELLED} | `raw/official-docs/google-aip-151-long-running-operations.md#AIP151-C1` (장시간 처리 메서드는 Operation 반환), `#AIP151-C4` (`done=false` 시 `name` MUST — polling 조건), `#AIP151-C3` (성공 완료 시 `response` 필드 필수), `#AIP151-C5` (실패 완료 시 `error` 필드 필수); HTTP 202 normative 의미는 `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C22` (202 Accepted = processing 위해 accept, 완료 안 됨, intentionally noncommittal); polling interval 권고 `Retry-After` 는 `#RFC9110-C21` (server send Retry-After to indicate wait time). AIP-136 cross-ref: `raw/official-docs/google-aip-136-custom-methods.md#AIP136-C3` (`:cancel` 등 LRO 조작 custom method 는 side effect → `POST` MUST), `#AIP136-C5` (collection-scoped custom method 패턴 — `:batchCreate` 가 202 LRO 응답 반환 시 B18 과 연결) | `official-standard` (IETF RFC 9110 — 202 + Retry-After) + `official-reference` (Google AIP-151/136 — Operation shape + polling pattern, Google API community guideline; IETF/W3C 표준 아님) | 5종 enum 어휘 (PENDING/RUNNING/SUCCEEDED/FAILED/CANCELLED) 는 AIP-151 에 없음 — project-internal 매핑 (UNSUPPORTED_IMPL_DECISION 잔존). status enum 5종과 AIP-151 의 done/result/error 이진 모델 간 매핑은 project-internal 결정으로 남음. webhook callback 패턴은 별도 branch 신설 필요. `Location` header 의 정확한 형식 (`/v1/operations/{id}`) 은 RFC 9110 §10.2.2 별도 발췌 미진행 |
|
|
| D23 (2026-05-31) | Bulk operation URL pattern = AIP-136 colon-verb (`POST /v1/{resource}:batchCreate`). request body `{ requests: [...] }`. **sync batch** = MUST atomic (한 항목 실패 → 전체 rollback + HTTP 4xx + envelope.success=false, partial failure 금지). **async batch** = 202 Accepted + `Location: /v1/operations/{id}` → polling endpoint 의 `data.result.results[]` 에 항목별 success/error (BATCH_PARTIAL_FAILURE 는 async polling 응답에서만 사용) | `raw/official-docs/google-aip-136-custom-methods.md#AIP136-C2` (URI MUST use `:` + custom verb), `#AIP136-C5` (collection-scoped custom method 패턴); `raw/official-docs/google-aip-233-batch-create.md#AIP233-C2` (HTTP verb MUST `POST`), `#AIP233-C3` (URI MUST end with `:batchCreate`), `#AIP233-C4` (request message MUST repeated field, SHOULD named `requests`), `#AIP233-C7` (sync batch create MUST atomic); D17 LRO 결합 — `raw/official-docs/google-aip-151-long-running-operations.md#AIP151-C1`~`C5` (async endpoint 의 Operation shape) + `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C22` (202 Accepted) | `official-reference` (AIP-136 + AIP-233 + AIP-151) + `official-standard` (RFC 9110) + `cross-branch-SSOT` (foundation envelope) | UNSUPPORTED_IMPL_DECISION 잔존: (1) `data.results[]` REST envelope shape (항목별 success/error 구조) 은 boundary branch B14 (BulkEnvelope.partial) SSOT 의존. (2) sync batch atomic rollback 시 HTTP status (400 VALIDATION_FAILED vs 422 Unprocessable Entity vs 409 CONFLICT) 는 error-codes.yaml row 정합성으로 결정 (D11 mapping consistency contract test 가 강제) |
|
|
| D19 | Resource URL naming convention = plural + lowercase + AIP-122 regex `[a-z][a-zA-Z0-9]*` · single-word `/v1/worklogs` · multi-word `lowerCamelCase` (`/v1/worklogComments`) · kebab-case / singular / CamelCase 모두 금지. **`{id}` placeholder 의 concrete format** = ULID 26-char Crockford base32 (`01ARZ3NDEKTSV4RRFFQ69G5FAV`) per [[raw/branch-notes/feature-resource-identifier-contract]] D1/D19 SSOT | `raw/official-docs/google-aip-122-resource-names.md#AIP122-C2` (collection segment plural rule), `#AIP122-C3` (collection segment lowercase + ASCII-only character set regex `[a-z][a-zA-Z0-9]*`). cross-cite [[raw/branch-notes/feature-resource-identifier-contract]] D1 (ULID 채택) + D19 (sample-portfolio `WorkLogId` fixture concrete value). cross-cite [[raw/branch-notes/feature-architecture-enforcement-rules]] (있다면 — controller mapping ArchUnit 강제 영역) | `official-reference` (Google AIP-122 — community guideline, IETF/W3C 표준 아님) + `cross-branch-SSOT` (resource-identifier branch D1/D19) | AIP-122 가 protobuf 컨텍스트 — REST URL path 매핑은 AIP-127 별도 cross-cite 필요 (현재 raw 미보관, future). 본 branch 의 `/v1/worklogs` 채택은 AIP122-C2/C3 가 *direct corroborate*. multi-word resource 의 lowerCamelCase 가 implementation 단계에서 hyphen 욕구와 충돌 가능 (예: `customer-orders` vs `customerOrders`) — 이 결정으로 후자만 허용 명시. `{id}` format 분리 SSOT 는 resource-identifier branch — 본 branch 는 URL 구조 (placeholder + path 패턴) 만 결정 |
|
|
| D20 | Sort parameter syntax = Spring `Pageable` native `?sort=field,direction` · multi-sort = param repeat · 다른 syntax (`?sort=-foo` / `?sort=foo:desc` / `?order_by=foo desc`) 금지 | `raw/official-docs/spring-data-pageable-defaults.md#SPRING-PAGE-C1` (Pageable zero-indexed), `#SPRING-PAGE-C2` (size default 20), `#SPRING-PAGE-C3` (zero-indexed infrastructure); cross-cite `raw/official-docs/google-aip-132-list-method.md#AIP132-C4` (대안 syntax: `"foo desc, bar"` — 본 결정 미채택 근거, space encoding 부담 + Spring 자동 binding 깨짐) | `official-vendor-doc` (Spring Data Commons — D20 의 직접 근거) + `official-reference` (AIP-132 — 대안 비교용 cross-cite) | Spring `Pageable` 의 sort syntax 가 multi-sort 시 param repeat 인지 (별도 separator 인지) 검증 필요 — Spring `PageableHandlerMethodArgumentResolver` default 동작 vendor doc 추가 fetch 권고. JSON:API `?sort=-foo` prefix syntax 의 미채택 근거는 *Spring binding 부재* (project-internal trade-off — JSON:API 자체는 `official-standard`) |
|
|
| D21 | Filter parameter syntax = flat key=value (equality only) · `?status=OPEN&owner=user123` 만 허용 · 복잡 filter (range / `in` / `like` / AND/OR 조합) 는 *out of scope* · AIP-160 DSL / RSQL / FIQL / JSON:API bracket 모두 미채택 | UNSUPPORTED_DECISION (project-internal trade-off — *minimalist default* + parsing/security 부담 회피). cross-cite `raw/official-docs/google-aip-160-filtering.md#AIP160-C1`~`C6` (대안 DSL *옵션 존재* 만 corroborate, 본 결정 미채택 근거: SQL injection 위험 + ergonomics 학습곡선 + Spring 자동 binding 부재) | UNSUPPORTED + `official-reference` (AIP-160 대안 cross-cite) | flat key=value 가 복잡 query 요구사항 발생 시 어떻게 확장할지의 *migration path* 가 본 결정에 없음 — 후속 결정으로 미룸. controller 가 명시적으로 받지 않는 query param 의 silent 무시 정책은 boundary branch 의 ACL mapper 책임 (cross-link 필요) |
|
|
| D22 | Cursor pagination shape = opaque base64-encoded JSON token + HMAC signature + 24h TTL · cursor endpoint 는 별도 query param 또는 별도 path · client 의 token parse 금지 (opacity 강제) · cursor + `?page=N` 동시 사용 금지 | `raw/official-docs/google-aip-158-pagination.md#AIP158-C3` (page_token MUST NOT required + subsequent page_size 변경 MUST honor), `#AIP158-C5` (page token opaque + URL-safe MUST; base64 단독 불충분 → HMAC signature 추가 정당화) | `official-reference` (Google AIP-158 — opacity/URL-safe MUST normative) + UNSUPPORTED_IMPL_DECISION (24h TTL + HMAC algorithm 선택 + JSON shape 자체는 project-internal trade-off) | TTL 24h 의 정확한 숫자는 AIP-158 에 없음 (project-internal — 짧으면 long-running export 깨짐, 길면 DB layout 변경 시 stale cursor 문제). HMAC key rotation 정책은 별도 결정 (security branch 와 cross-link 필요). cursor endpoint URL 패턴 (`/v1/worklogs:listByCursor` colon-verb vs `?cursor=` query param) 미정 — 후속 D23 colon-verb 채택과 정합성 위해 colon-verb 권고 가능 (future revision) |
|
|
| D24 | Response Date header = 모든 응답에 `Date` 헤더 자동 발행 (Spring/Tomcat default 활용, controller 별도 설정 불요) · Date header 명시적 비활성화 금지 | `raw/official-docs/rfc9110-http-semantics.md#RFC9110-C20` (sender 가 Date header 생성 시 best available approximation SHOULD) | `official-standard` (IETF RFC 9110 §6.6.1) | RFC 9110 SHOULD 권고만 — MUST 아님. Spring/Tomcat default 가 자동 발행하지만 controller 또는 filter 에서 강제 제거하는 경우 (테스트 reproducibility 또는 cache 제어 이유) 차단 의무. error response (404/500) 에서도 Date 발행 여부 검증 contract test 필요. 단 Date header 의 정확한 format (HTTP-date — §5.6.7) 검증은 별도 (Spring vendor 책임) |
|
|
| D18 | Pagination — `page` 0-indexed (Spring `Pageable` 정합), `size` default 20 / max 100 / min 1 · `size > 100` 또는 `size < 1` 또는 `page < 0` 은 400 VALIDATION_FAILED · 빈 list 는 `data: []` (절대 `null` 아님) + `meta.page.total = 0` · 깊은 offset (`page > 10000`) 은 `Deprecation` 헤더 + cursor 권고 | `raw/official-docs/spring-data-pageable-defaults.md#SPRING-PAGE-C1` (`page` 파라미터 0-indexed, default 0), `#SPRING-PAGE-C2` (`size` 파라미터 default 20), `#SPRING-PAGE-C3` (Spring Data 레포지토리 infrastructure 의 `Pageable` 은 zero-indexed), `#SPRING-PAGE-C4` (`PageableHandlerMethodArgumentResolverSupport.DEFAULT_MAX_PAGE_SIZE = 2000` — Spring 기본 max 가 2000 이므로 project 의 100 cap 은 별도 opt-in override 임을 정당화), `#SPRING-PAGE-C5` (annotation 없는 fallback = `PageRequest.of(0, 20)`), `#SPRING-PAGE-C6` (`setOneIndexedParameters` default false → page 0 = first page). 추가 normative 근거: `raw/official-docs/google-aip-158-pagination.md#AIP158-C1` (collection pagination 처음부터 제공 필수, 나중 추가 = backwards-incompatible), `#AIP158-C2` (page_size server-side cap SHOULD coerce down; max 숫자는 server-defined — 100 은 project-internal), `#AIP158-C3` (page_token MUST NOT required; subsequent page_size 변경 MUST honor), `#AIP158-C4` (next_page_token empty = EoC MUST, 유일한 EoC 시그널), `#AIP158-C5` (page token opaque + URL-safe MUST; base64 단독 불충분). JSON:API JSONAPI-PAGE-C1~C6 모두 size cap 또는 index base 의 normative 진술 없음 — JSONAPI-PAGE-C6 가 pagination strategy 에 agnostic 임을 명시. `size` max 100 cap 및 min 1 / `page < 0` 400 처리는 project-internal DoS prevention trade-off (UNSUPPORTED_IMPL_DECISION 잔존 — Spring 기본값 이하 추가 제한) | `official-vendor-doc` (Spring Data Commons — SPRING-PAGE-C1~C6) + `official-reference` (AIP-158 — AIP158-C1~C5) + UNSUPPORTED_IMPL_DECISION (`size` 100 cap / min 1 / 깊은 offset threshold 10000 은 project-internal 숫자; size>100 을 AIP-158 권고 coerce 대신 400-reject 강화도 project-internal) | 가장 critical footgun 결정 — `size=10000000` DoS 방지 + 0/1-indexed Spring 정합. Spring default max 는 2000 이지 Integer.MAX_VALUE 가 아님 (C4 보정). 깊은 offset 의 cursor 권고는 본 branch 가 박지만 cursor endpoint 의 shape (opaque token encoding/TTL) 결정은 future B16 — sibling 또는 후속 작업 |
|
|
|
|
## 구현 가이드
|
|
|
|
> *결정 (Decisions)* 이 "*무엇* 을 할 것인가" 라면, 본 §는 "*어디에 어떻게* 구현될 것인가" 의 사전 명세 — *문서가 모호해서 구현자가 임의로 정해야 했던 결정* 카탈로그. 작성 목표는 다음 구현자가 *되묻지 않아도 코드를 작성할 수 있는 수준*.
|
|
>
|
|
> **3-rule meta principle (필수 준수)**:
|
|
>
|
|
> 1. **R1. Reference 필수** — 각 sub-section / row / cell 은 본 branch 의 `Decision ID` (예: D1, D2) + 그 결정의 `Supporting Claim ID` (예: `RFC9110-C5`) 를 reference.
|
|
> 2. **R2. UNSUPPORTED_IMPL_DECISION 명시** — 근거 raw 가 *원칙* 만 권고하고 *detail* 은 권고하지 않는 cell 은 `UNSUPPORTED_IMPL_DECISION` 라벨 + 사용자 trade-off 근거 한 줄.
|
|
> 3. **R3. OUT_OF_BRANCH_SCOPE 정제** — 본 branch 결정 범위 밖 cell 은 §구현 가이드에 *남기지 않음*. 별도 branch 또는 canonical SSOT 로 이관.
|
|
|
|
### 1. Work Item Contract (TODO → canonical 승급 판정 단위)
|
|
|
|
> **Trace**: 본 sub-section 은 branch 의 *모든* TODO 가 canonical 승급 가능한 형태로 정제되어야 한다는 project-wide 메타 규약. ca-skeleton operational contract §23 Branch Canonical Promotion Criteria 와 정합.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 본 표는 project 공통 메타 규약 — 본 branch 의 외부 표준 직접 근거 영역 밖.
|
|
|
|
각 TODO 는 아래 판정 단위로 재작성되어야 canonical 승급 가능. TODO 가 단순히 `기준 작성` 으로 남아 있으면 branch 완료로 보지 않는다.
|
|
|
|
| field | required | rule |
|
|
| --- | --- | --- |
|
|
| Decision | yes | 구현자가 선택해야 하는 기본값 |
|
|
| Allowed | yes | 허용되는 예외와 조건 |
|
|
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
|
|
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
|
|
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
|
|
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
|
|
| Canonical extraction target | yes | `wiki/projects` 승급 위치 |
|
|
|
|
### 2. Decisionized Work Items (결정의 implementation matrix)
|
|
|
|
> **Trace**: D2 (versioning, AIP185-C1~C3) · D7 + D18 (pagination, JSONAPI-PAGE-C1~C3 + SPRING-PAGE-C1~C6 + AIP158-C1~C5) · D3 + D4 (idempotency header, STRIPE-IDEMP-C1 + IETF-IDEMP-C1 + TOSS-IDEMP-C1) · D8 (request size, RFC9110-C5/C6) · D8 형제 (URI length, RFC9110-C19) · D9 (content negotiation, RFC9110-C4/C7/C8) · D12 (405 + Allow, RFC9110-C9/C10) · D14 (PATCH, RFC7396-C1/C2/C3/C5) · D13 (HEAD/OPTIONS, RFC9110-C11/C12 + FETCH-CORS-C2) · D15 (conditional request, RFC9110-C13~C17) · D16 (cache policy + Vary, RFC9111-C1~C5 + RFC9110-C18) · D17 (LRO, AIP151-C1~C7 + AIP136-C3/C5 + RFC9110-C21/C22) · D11 (HTTP status mapping SSOT, project-internal — UNSUPPORTED_DECISION 잔존) · D10 (OpenAPI producer, OPENAPI31-C2/C4).
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**:
|
|
> - pagination row 의 `size` default 20 / max 100 / min 1 / `page > 10000` threshold 의 정확한 *숫자* 는 project-internal trade-off (DoS prevention + UX). 대안: max 50 / max 200 — 외부 표준은 숫자 미정. 본 branch 가 *안전한 default* 로 100 채택.
|
|
> - URI length row 의 Tomcat `maxHttpHeaderSize` 기본 8KB threshold 는 server vendor (Tomcat) default — 다른 server (Undertow/Netty) 면 다름. 본 branch 는 *Tomcat 기준 default* 만 명시, 다른 server 채택 시 별도 결정.
|
|
> - **`UNSUPPORTED_IMPL_DECISION` (D17 LRO)**: polling status enum 5종 (PENDING/RUNNING/SUCCEEDED/FAILED/CANCELLED) 은 AIP-151 에 *없음* — AIP-151 의 `done`/`response`/`error` 이진 모델에서 project-internal 파생. 매핑: `PENDING`=accepted+미시작, `RUNNING`=`done=false`+진행중, `SUCCEEDED`=`done=true`+`response`(AIP151-C3), `FAILED`=`done=true`+`error`(AIP151-C5), `CANCELLED`=`done=true`+cancelled error. polling endpoint URL `/v1/operations/{id}` 형식도 project-internal (`AIP151-C4` 는 `name` MUST 만 요구, REST `Location` 매핑은 RFC 9110 §10.2.2 별도 발췌 미진행 — Should-fix). trade-off: 5-state 가 client 에 명시적 진행 단계를 제공하나 AIP-151 이진 모델보다 표면이 넓음(어휘 drift 위험은 §3 LRO contract test 로 차단).
|
|
> - PATCH row 의 RFC 6902 (JSON Patch) endpoint 옵트인 *경로 명명* (예: `PATCH /v1/worklogs/{id}` Content-Type 분기 vs 별도 path) 미정.
|
|
|
|
| item | Decision | Allowed | Forbidden | Required test | Failure condition |
|
|
| --- | --- | --- | --- | --- | --- |
|
|
| versioning | `/v1` URI prefix | `X-Api-Version` supplemental header | media-type/header/path version 혼용 | OpenAPI path version test | version 없는 public endpoint |
|
|
| pagination | `page` 0-indexed (Spring `Pageable` 정합), `size` default 20 / max 100 / min 1, `sort` request + `meta.page` response (`number`, `size`, `total`, `sort`) | cursor pagination은 별도 endpoint에서만 + 깊은 offset (`page > 10000`) 은 `Deprecation` 헤더 + cursor 권고 | pagination metadata in `data` · `size > 100` · `page < 0` 통과 · 빈 list 가 `data: null` | response meta contract + size cap boundary test + empty list shape test | list response에 page metadata 누락 또는 `size=10000` 통과 |
|
|
| idempotency header | POST 등 non-idempotent method 에 `Idempotency-Key` 만 적용 (GET/HEAD/PUT/DELETE 는 의미 없음) | optional 표시 가능하나 server 가 무시 | GET/HEAD/PUT/DELETE 에 idempotency key replay semantics 강제 | replay contract | duplicate write on retry · GET 에 replay 의미 부여 |
|
|
| request size | app limit maps to `VALIDATION` or `RATE_LIMIT` style envelope per owner branch + 일시적이면 `Retry-After` 헤더 (RFC9110-C6) | gateway pre-reject may bypass app envelope with documented log correlation | raw 500 for 413 | oversized request contract | payload too large가 raw server error |
|
|
| URI length | URL+query 길이 초과는 414 URI Too Long + envelope 따름 | gateway-level reject 시 envelope 우회 가능 (log correlation 필수) | raw 500 또는 400 으로 변환 | URI length boundary test (Tomcat `maxHttpHeaderSize` 기본 8KB 초과) | 414 가 raw server error 또는 잘못된 400 |
|
|
| content negotiation | unsupported media type and not acceptable use distinct codes | gateway-owned negotiation if documented | 415/406 same error code | MVC exception mapping | 415/406 분류가 같음 |
|
|
| method not allowed | 405 + `Allow` header (지원 method comma-separated) + envelope 따름 | gateway pre-reject 시 envelope 우회 가능 | 405 응답에 `Allow` 누락 · Spring `HttpRequestMethodNotSupportedException` envelope 우회 직접 응답 | 405 contract test (DELETE-only endpoint 에 GET 요청 → 405 + `Allow: DELETE` + envelope) | `Allow` 누락 |
|
|
| method-PATCH | PATCH default media type = `application/json` · request shape = `JsonNullable<T>` / `Optional<T>` wrapper · absent/null/value 3-상태 구분 (boundary branch B2 SSOT) | 없음 — `application/merge-patch+json` (RFC 7396) / `application/json-patch+json` (RFC 6902) 모두 **금지** | `application/merge-patch+json` / `application/json-patch+json` content type 사용 · absent vs null 미구분 (`null` 이 absent 와 동일 의미로 처리됨) | `no_merge_patch_json_media_type_string` ArchUnit rule (boundary branch B2) + PATCH absent/null 3-상태 mapper contract test | PATCH endpoint 가 `application/merge-patch+json` content type 허용 · Java record canonical constructor 가 absent 와 null 을 같은 기본값으로 수렴 |
|
|
| HEAD support | GET 지원 endpoint 는 HEAD MUST (Spring MVC 자동 처리) | OPTIONS 분기: CORS preflight (envelope 우회, security branch SSOT) / resource metadata (envelope 따름) | GET-only endpoint 에 HEAD 가 405 또는 404 | HEAD-mirror-GET contract test | HEAD 미지원 |
|
|
| conditional request | read 응답에 `ETag` 발행 (version field 기반 또는 content hash) · write 의 `If-Match` mismatch → 412 + envelope · read 의 `If-None-Match` match → 304 (body 없음, envelope 우회) | write 의 `If-Match` 누락 *허용* (sample-portfolio fixture 에서 *권장* 패턴 검증) | `ETag` 미발행 · 412 가 raw 500 또는 409 로 매핑 · 304 에 body 동봉 | conditional request matrix test (4 시나리오) | 412/304 잘못 매핑 |
|
|
| response cache policy | 모든 응답 default `Cache-Control: no-store` · content negotiation 또는 인증 응답에 `Vary: Accept, Accept-Encoding, Authorization` 의무 | cacheable endpoint 만 controller annotation 으로 `Cache-Control: private, max-age=N` opt-in | 인증 응답에 `public` Cache-Control · `Vary` 누락 | Cache-Control default test + Vary header presence test | 인증 응답이 public cacheable |
|
|
| long-running operation | 202 Accepted + `Location: /v1/operations/{id}` + envelope `data.{operationId,statusUrl}` · polling `GET /v1/operations/{id}` 의 `status` ∈ {PENDING,RUNNING,SUCCEEDED,FAILED,CANCELLED} | `Retry-After` 헤더로 polling interval 권고 | 비동기 endpoint 가 sync-pretend 로 long-wait + timeout | LRO contract test (202 + Location + polling status transition) | 비동기 endpoint 가 동기 timeout 으로 응답 |
|
|
| HTTP status mapping SSOT | `error-codes.yaml` 의 `http_status` column 이 모든 매핑의 SSOT (registry §21, owner: foundation branch) | 본 branch 는 mapping consistency contract test 의 producer | controller 가 registry 와 다른 HTTP status 반환 | status mapping consistency test (모든 `error.code` row 에 대해 실제 응답의 HTTP status 가 registry 와 일치) | registry-controller drift |
|
|
| OpenAPI producer | generated OpenAPI snapshot produced by this branch | external openapi generator allowed | manual stale schema only | verification drift check | schema/response mismatch passes |
|
|
| resource URL naming (D19) | plural + lowercase + AIP-122 regex `[a-z][a-zA-Z0-9]*` (single-word: `/v1/worklogs`, multi-word: `/v1/worklogComments` lowerCamelCase) | sub-resource path 허용 (`/v1/worklogs/{id}/comments`), custom method 의 colon-verb suffix 허용 (`/v1/worklogs:batchCreate`) | singular path (`/v1/worklog/{id}`) · kebab-case (`/v1/worklog-comments`) · CamelCase (`/v1/Tickets`) · UPPER_CASE | ArchUnit 또는 Spring controller mapping inspector — 모든 `@RequestMapping` path segment 가 AIP-122 regex 매치 검증 | path segment 가 regex 위반 |
|
|
| sort syntax (D20) | Spring `Pageable` native `?sort=field,direction` · multi-sort = param repeat | reverse direction 명시 (`,desc` 필수, 생략 시 default `asc`) | `?sort=-foo` (JSON:API), `?sort=foo:desc`, `?order_by=foo desc` (AIP-132 space) | sort syntax contract test (각 endpoint 의 `?sort=createdAt,desc` 정상 + `?sort=-createdAt` 거부) | non-Spring syntax 통과 |
|
|
| filter syntax (D21) | flat key=value (equality only) (`?status=OPEN&owner=user123`) | controller 가 명시적으로 받지 않는 query param 은 silently 무시 (boundary branch 의 ACL mapper 책임) | AIP-160 DSL · RSQL/FIQL · JSON:API bracket (`?filter[key]=value`) · 복잡 expression (`?filter=status==OPEN AND priority>3`) | filter syntax contract test (각 list endpoint 의 `?status=OPEN` 정상 + `?filter=...` DSL 무시 또는 거부) | DSL syntax 가 controller 에서 parsing 시도 |
|
|
| cursor pagination shape (D22) | opaque base64-encoded JSON token + HMAC signature + 24h TTL · cursor endpoint 는 별도 query param (`?cursor=<token>&size=20`) 또는 별도 path | size cap (D18) 동일 적용 · token 만료 시 400 VALIDATION_FAILED + 권장: 첫 페이지 재요청 | typed cursor (last value 노출) · unsigned token (tamper risk) · cursor + `?page=N` 동시 사용 · TTL 무한 | cursor token roundtrip test + tamper detection test + TTL expiration test + opacity 검증 (client parse 가능하면 실패) | typed/unsigned/no-TTL token |
|
|
| bulk operation URL (D23) | AIP-136 colon-verb `POST /v1/{resource}:batchCreate` · request body `{ requests: [...] }` · **sync batch** = atomic MUST (한 항목 실패 → 전체 rollback + HTTP 4xx + envelope.success=false) · **async batch** = 202 + Location → polling endpoint 의 `data.result.results[]` 에 항목별 success/error (BATCH_PARTIAL_FAILURE 는 async polling 에서만) | async batch 의 polling endpoint 가 D17 LRO pattern 따름 + BATCH_PARTIAL_FAILURE category 적용 | sync batch 에서 partial failure 허용 (AIP233-C7 위반) · flat array body · kebab subpath · sync batch 의 atomic rollback 누락 | bulk endpoint contract test (sync: atomic rollback 검증 · async: 202+polling+partial result 검증) | sync batch 가 partial success 응답 · BATCH_PARTIAL_FAILURE 가 sync 응답에 사용됨 |
|
|
| response Date header (D24) | 모든 응답에 `Date` 헤더 자동 발행 (Spring/Tomcat default) | local profile 에서 fixed clock 으로 테스트 reproducibility 확보 가능 | `server.servlet.dispatchOptionsRequest=false` 같은 Date 비활성화 옵션 · 404/500 등 error path 에서 Date 누락 | Date header presence contract test (전체 status code matrix — 200/204/400/404/500) | Date header 누락 |
|
|
|
|
### 3. Test Contract (테스트 계약 — 결정 위반 감지 trigger)
|
|
|
|
> **Trace**: 본 sub-section 은 §2 Decisionized Work Items 의 `Required test` column 을 *그대로 펼쳐 쓴 catalog*. 각 라인은 §2 의 특정 row + Decision ID 와 1:1 매핑.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 본 sub-section 은 §2 의 직접 도출 — 별도 임의 결정 없음.
|
|
|
|
- OpenAPI schema와 실제 response envelope가 다르면 실패 (D10).
|
|
- `/v1` prefix 없는 public API가 추가되면 실패 (D2).
|
|
- pagination 응답에 page/size/total/sort 기준이 없으면 실패 (D7).
|
|
- pagination 의 `size > 100` 또는 `size < 1` 또는 `page < 0` 이 통과하면 실패 (D18 boundary test).
|
|
- 빈 list 응답이 `data: null` 이거나 `meta.page.total` 누락이면 실패 (D18 empty list shape test).
|
|
- unsupported media type과 not acceptable이 같은 code로 뭉개지면 실패 (D9).
|
|
- oversized request가 raw server error로 변환되면 실패 (D8).
|
|
- URI 길이 초과 (Tomcat `maxHttpHeaderSize` 기본 8KB 초과) 가 raw 500 또는 잘못된 400 으로 매핑되면 실패 (D8 형제).
|
|
- 405 응답에 `Allow` header 가 없거나 envelope 우회로 직접 응답하면 실패 (D12).
|
|
- GET 지원 endpoint 가 HEAD 요청에 405/404 응답하면 실패 (D13 HEAD-mirror-GET test).
|
|
- PATCH endpoint 가 `application/merge-patch+json` 또는 `application/json-patch+json` content type 을 허용하면 실패 — boundary branch B2 의 ArchUnit `no_merge_patch_json_media_type_string` 으로 build 차단 (content-type test).
|
|
- PATCH 요청 mapper 가 absent (JSON 에 키 자체 부재) 와 null (명시적 `null` 값) 을 같은 기본값으로 수렴하면 실패 — `JsonNullable<T>` / `Optional<T>` wrapper 검증 (D14 absent/null/value 3-상태 mapper contract test).
|
|
- write 응답에 `ETag` header 가 없거나 `If-Match` mismatch 시 412 가 아닌 409/500 으로 매핑되면 실패 (D15 conditional request matrix test).
|
|
- `If-None-Match` match 시 304 응답에 body 가 동봉되면 실패 (D15 cache validation test).
|
|
- 인증된 응답 default 가 `Cache-Control: no-store` 가 아니거나 content-negotiated 응답에 `Vary` header 가 없으면 실패 (D16 cache policy test).
|
|
- 비동기 endpoint 가 202 + `Location` + envelope `data.{operationId,statusUrl}` 형식이 아니거나 polling endpoint 의 `status` 가 enum 어휘 밖이면 실패 (D17 LRO test).
|
|
- `error-codes.yaml` 의 임의의 row 에 대해 실제 controller 응답의 HTTP status 가 row 의 `http_status` column 과 다르면 실패 (D11 mapping consistency test, registry 와 controller 의 drift 감지).
|
|
- controller `@RequestMapping` path segment 가 AIP-122 regex `[a-z][a-zA-Z0-9]*` 를 위반 (kebab-case, singular, CamelCase) 하면 실패 (D19 URL naming convention ArchUnit test).
|
|
- `?sort=-foo` 또는 `?sort=foo:desc` 같은 non-Spring-Pageable sort syntax 가 controller 에서 정상 처리되면 실패 (D20 sort syntax contract test).
|
|
- list endpoint 에 AIP-160 DSL (`?filter=status==OPEN`) 또는 JSON:API bracket (`?filter[status]=OPEN`) 이 통과하면 실패 (D21 filter syntax contract test — flat key=value 만 허용).
|
|
- cursor token 이 typed (last field value 노출) · unsigned (tamper 가능) · TTL 없음 (영구 유효) 중 하나면 실패 (D22 cursor shape contract test — opacity/integrity/TTL 3개 invariant).
|
|
- sync bulk endpoint 가 atomic 이 아니거나 (한 항목 실패 시 전체 rollback 안 됨), partial failure 응답을 sync 에서 반환하거나, async bulk endpoint 가 202+Location+polling pattern 이 아니거나, BATCH_PARTIAL_FAILURE category 가 sync 응답에 사용되면 실패 (D23 contract test — AIP233-C7 정합).
|
|
- 모든 응답 (success/error 무관, status code 200/204/400/404/500 매트릭스) 에 `Date` 헤더가 없으면 실패 (D24 Date header presence test).
|
|
|
|
### 4. Cross-branch Contract Map (본 branch 의 owner/consumer/producer role)
|
|
|
|
> **Trace**: 본 sub-section 은 project-note §25 SSOT Owner Map 의 *본 branch 관련 row 의 역 인덱스*. cross-branch 결정 정합성 깨짐을 추적하기 위함.
|
|
>
|
|
> - **UNSUPPORTED_IMPL_DECISION**: 본 sub-section 은 cross-branch 관계의 *기록* 일 뿐 본 branch 의 외부 표준 직접 근거 영역 밖.
|
|
> - **OUT_OF_BRANCH_SCOPE 정리**: `consumer only` 로 표시된 영역은 *결정 자체* 는 다른 branch 가 소유. 본 branch 는 *cross-cite* 만 — 결정 변경 시 owner branch 를 통해야 함.
|
|
|
|
| 영역 | 본 branch 의 role | counterpart owner | 의존 방향 |
|
|
|---|---|---|---|
|
|
| API versioning (`/v1` URI prefix) | **owner** (D2, D6) | (consumer) `feature-api-compatibility-deprecation-contract` — `/v1` deprecation 시 Sunset/Deprecation header 발행 | 본 branch → compatibility branch |
|
|
| HTTP header naming + headers.yaml (registry §21) | **owner** (cross-owner: idempotency, tracing, tenant, compat, security 모두 cross-cite) | (consumers) tracing/tenant/security/compat 모든 branch | 본 branch ← multiple branches |
|
|
| HTTP status ↔ envelope `error.code` 매핑 (D11) | **producer** of mapping consistency contract test | **owner** of registry: `feature-operational-error-observability-foundation` (`error-codes.yaml`) | 본 branch ← foundation branch |
|
|
| envelope schema (`success`/`data`/`error`/`meta`) | **consumer only** | **owner**: `feature-operational-error-observability-foundation` | 본 branch ← foundation |
|
|
| `Idempotency-Key` header *이름* (D3) | **owner** (header name 만) | **owner** of key shape/scope/replay: `feature-rate-limit-idempotency-contract` | 본 branch → rate-limit branch (header name produces, key shape consumes) |
|
|
| `Idempotency-Key` key shape `(principal, key, useCase)` + replay semantics | **consumer only** | **owner**: `feature-rate-limit-idempotency-contract` | 본 branch ← rate-limit branch |
|
|
| OpenAPI snapshot 생성 (D10) | **producer** | **owner** of drift gate: `feature-contract-verification-test-suite` | 본 branch → verification suite |
|
|
| Pagination / sorting / filtering shape (D7, D18) | **owner** (`page`/`size`/`sort` + `meta.page`) | (no counterpart — leaf) | — |
|
|
| PATCH semantics (D14 정정 후) | **consumer** (content type 정책만 producer — `application/json` only) | **owner**: `feature-boundary-validation-mapping-contract` B2 (RFC 7396 미채택 + absent/null/value 3-상태 mapper + ArchUnit `no_merge_patch_json_media_type_string` enforced) | 본 branch ← boundary branch SSOT |
|
|
| Conditional request (`ETag`/`If-Match`/412/304) (D15) | **owner** (HTTP layer) | **consumer**: sample-portfolio fixture (`WorkLogVersion` 이 ETag derivation 의 source) | 본 branch → sample-portfolio fixture |
|
|
| 405 + `Allow` header (D12) | **owner** | (no counterpart — leaf) | — |
|
|
| HEAD/OPTIONS support (D13) | **owner** (HEAD 부분) · **consumer** (OPTIONS preflight 분기) | **owner** of CORS: [[raw/branch-notes/feature-security-operational-baseline]] (D9) | 본 branch ← security branch (preflight bypass 결정) |
|
|
| Response cache policy + `Vary` header (D16) | **owner** (HTTP header 정책) | **owner** of cache layer 구현: `feature-cache-consistency-contract` | 본 branch → cache branch (header policy produces, cache 구현 consumes) |
|
|
| Long-running operation (LRO) 응답 패턴 (D17) | **owner** (polling-only LRO) | (no current counterpart — webhook callback 은 별도 branch 신설 필요) | — |
|
|
| Field naming case (camelCase) | **consumer only** | **owner**: `feature-schema-serialization-contract` | 본 branch ← schema-serialization |
|
|
| date/time/decimal serialization | **consumer only** | **owner**: `feature-schema-serialization-contract` | 본 branch ← schema-serialization |
|
|
| `Server` / `X-Powered-By` header suppression | **consumer only** (forbid 명시) | **owner**: `feature-security-operational-baseline` | 본 branch ← security branch |
|
|
| `X-HTTP-Method-Override` forbid | **consumer only** | **owner**: security branch (예정) | 본 branch ← security branch |
|
|
| `Accept-Encoding` / response compression | **out of scope** | reverse proxy/gateway 책임 (운영 영역) | — |
|
|
| `Accept-Language` / error message i18n | **out of scope** | 결정 미정 (future) | — |
|
|
| Resource URL naming (D19) | **owner** (AIP-122 plural+lowercase regex) — URL 구조만 | (consumer) ArchUnit/architecture branch — controller mapping 검증. `{id}` placeholder format SSOT = [[raw/branch-notes/feature-resource-identifier-contract]] D1 (ULID) | 본 branch → architecture branch ← resource-identifier branch (`{id}` format) |
|
|
| Sort parameter syntax (D20) | **owner** (Spring `Pageable` native) | (consumer) `feature-schema-serialization-contract` (field name case 정합) | 본 branch ↔ schema-serialization |
|
|
| Filter parameter syntax (D21) | **owner** (flat key=value default) | (no counterpart — leaf, 복잡 filter는 future branch) | — |
|
|
| Cursor pagination shape (D22) | **owner** (opaque base64 + HMAC + 24h TTL) | (consumer) `feature-security-operational-baseline` (HMAC key rotation 정책 cross-link 필요) | 본 branch → security branch |
|
|
| Bulk operation URL (D23) | **owner** (AIP-136 colon-verb + AIP-233 sync MUST atomic + async LRO 결합) | (consumer) `feature-boundary-validation-mapping-contract` B14 (BulkEnvelope.partial — async polling 응답 영역만), [[raw/branch-notes/feature-operational-error-observability-foundation]] (BATCH_PARTIAL_FAILURE — async polling 에서만 사용); D17 LRO 결합 (async batch 의 polling endpoint) | 본 branch → boundary + foundation · 본 branch internal cross-cite (D23 ↔ D17) |
|
|
| Response Date header (D24) | **owner** (Spring/Tomcat default 활용) | (no counterpart — leaf) | — |
|
|
| Resource ID format (UUID / ULID / opaque) | **out of scope** | 기존 owner [[raw/branch-notes/feature-resource-identifier-contract]] D1/D19의 ULID 결정을 소비하고 본 branch는 URL placeholder만 연결 | 본 branch → resource-identifier branch |
|
|
| Webhook outbound contract | **out of scope** | 별도 branch 신설 필요 (예정) | — |
|
|
| SSE / WebSocket / streaming | **out of scope** | 별도 branch (예정, 현재 ca-skeleton 미지원) | — |
|
|
|
|
## 엣지·실패·의존
|
|
|
|
> R4(깊이 게이트) 캡처용. 정상 경로 외에 *구현 중 부딪힐* 실패/엣지/다른 계약 의존을 한 곳에 열거. (§3 Test Contract·§4 Cross-branch Contract Map·§Claims To Verify 에 분산된 것을 R4 형식으로 통합 — 새 결정 없음. 각 항목은 Decision ID reference.)
|
|
|
|
- **실패·엣지 경로** (기대 동작은 §3 Test Contract; 위반 = 계약 실패):
|
|
- **oversized request (413) / URI 길이 초과 (414)** — raw 500 금지, envelope 따름. gateway pre-reject 시에만 envelope 우회 + log correlation 필수. (D8 / D8 형제 — RFC9110-C5/C6/C19)
|
|
- **content negotiation 406 vs 415** — 동일 error code 로 뭉개면 실패(distinct). (D9)
|
|
- **405 method not allowed** — `Allow` 헤더 누락 또는 Spring `HttpRequestMethodNotSupportedException` 가 envelope 우회 직접 응답하면 실패. (D12)
|
|
- **PATCH absent/null/value footgun** — Java record canonical constructor 가 absent(키 부재)와 null(명시적 clear)을 같은 기본값으로 수렴하면 실패. `JsonNullable<T>`/`Optional<T>` wrapper 로 3-상태 구분(boundary B2 SSOT). content type 은 `application/json` 만 — merge-patch/json-patch 금지. (D14)
|
|
- **conditional request** — `If-Match` mismatch 를 409/500 으로 매핑하면 실패(→ 412), `If-None-Match` match 304 에 body 동봉하면 실패. (D15)
|
|
- **pagination footgun** — `size > 100` / `size < 1` / `page < 0` 통과하거나 빈 list 가 `data: null` 이면 실패(`data: []` + `meta.page.total=0`). Spring default max 가 2000(≠Integer.MAX_VALUE)이라 project cap 100 은 별도 opt-in override. (D18 — SPRING-PAGE-C4)
|
|
- **cursor token** — typed(값 노출)/unsigned(tamper)/no-TTL 중 하나면 실패(opacity+HMAC+24h TTL 3-invariant). (D22)
|
|
- **LRO 비동기 endpoint** — sync-pretend long-wait/timeout 으로 응답하면 실패(202 + `Location` + polling). (D17)
|
|
- **cache poisoning** — content-negotiated/인증 응답에 `Vary` 누락 또는 인증 응답이 `public` cacheable 이면 실패(default `no-store`). (D16)
|
|
- **bulk** — sync batch 가 atomic 아니거나 partial failure 를 sync 응답에 반환, 또는 `BATCH_PARTIAL_FAILURE` 가 sync 응답에 쓰이면 실패. (D23 — AIP233-C7)
|
|
|
|
- **다른 계약 의존** (해당 계약이 바뀌면 본 branch 영향 — §4 Cross-branch Contract Map 의 consumer 방향 압축):
|
|
- [[raw/branch-notes/feature-operational-error-observability-foundation]] 의 `error-codes.yaml`(D11) + envelope schema 에 의존 — registry `http_status` column 이 status 매핑 SSOT. **(엣지) `error-codes.yaml` 미존재 또는 row 누락 시 D11 mapping consistency contract test 동작**: registry 가 존재하는데 row 가 빠진 경우 = test FAIL(drift). registry 자체가 아직 생성 전인 현재 documented-only 단계에서는 D11 test 가 `planned`(비활성) — **registry 생성이 D11 test 활성화의 선행 조건**이며, registry 부재를 SKIP(통과)으로 처리하면 안 됨.
|
|
- [[raw/branch-notes/feature-boundary-validation-mapping-contract]] 의 `B2`(PATCH mapper, D14) + `B14`(BulkEnvelope.partial) 에 의존. **(엣지) B14 미완 시 D23 async batch 구현은 blocked**: async polling 응답의 `data.result.results[]` 항목별 success/error shape 이 B14 SSOT 의존 → B14 결정 전까지 async batch + `BATCH_PARTIAL_FAILURE` 는 미구현 보류. **단 sync batch(atomic all-or-nothing)는 B14 무관하게 독립 진행 가능**.
|
|
- [[raw/branch-notes/feature-rate-limit-idempotency-contract]] 에 의존(D3/D4) — `Idempotency-Key` key shape/scope/replay semantics owner. 본 branch 는 header *이름* 만.
|
|
- [[raw/branch-notes/feature-security-operational-baseline]] 에 의존 — CORS preflight envelope 우회(D13), cursor HMAC key 소유/rotation(D22 — **Should-fix #5: 해당 Decision ID 미인용, 미결**), `Server`/`X-Powered-By` suppression·`X-HTTP-Method-Override` forbid.
|
|
- [[raw/branch-notes/feature-schema-serialization-contract]] 에 의존 — envelope `meta.*` camelCase(D16) + sort field name case(D20).
|
|
- [[raw/branch-notes/feature-cache-consistency-contract]] 에 의존 — cache layer 구현(D16 header policy 만 producer).
|
|
- [[raw/branch-notes/feature-api-compatibility-deprecation-contract]] 에 의존 — `/v1` deprecation Sunset/Deprecation 헤더(D2) + 깊은 offset `Deprecation` 헤더 형식(D18 — **Advisory #8: 발행 메커니즘 owner 미확정**).
|
|
- [[raw/branch-notes/feature-contract-verification-test-suite]] 에 의존 — OpenAPI drift release-gate(D5/D10, 본 branch 는 producer).
|
|
- [[raw/branch-notes/feature-resource-identifier-contract]] 에 의존 — `{id}` ULID format(D19, 본 branch 는 URL 구조만).
|
|
- [[raw/branch-notes/feature-architecture-enforcement-rules]] 에 의존 — controller mapping ArchUnit 강제 영역(D19 URL naming).
|
|
|
|
## 검증해야 할 주장
|
|
|
|
> 공식 문서/사례는 근거지만 내 프로젝트에서의 동작을 자동 보장하지 않는다. 구현 전/중/후 실제로 검증해야 하는 주장.
|
|
|
|
| Claim | Why uncertain | How to verify | Status |
|
|
|---|---|---|---|
|
|
| `Idempotency-Key` 헤더가 모든 POST endpoint 에 실제로 노출되는지 (OpenAPI snapshot 기준) | header 채택 결정은 design 단계, OpenAPI snapshot 에 반영됐는지 별도 확인 필요 | `openapi.yaml` snapshot grep 또는 contract test 로 모든 POST operation 에 `Idempotency-Key` parameter 존재 검증 | `planned` |
|
|
| `/v1` URI prefix 가 모든 public endpoint 에 적용되는지 | versioning 결정과 실제 controller mapping 의 drift 가능성 | ArchUnit / Spring controller mapping inspector 로 `RequestMapping` prefix 검증 | `planned` |
|
|
| pagination response 가 `meta.page` 표준 shape 와 일치하는지 | request param 처리는 검증 가능하지만 response envelope 의 일관성은 별도 contract test 필요 | response envelope contract test (모든 list endpoint 응답에 `meta.page.{number,size,total,sort}` 존재) | `planned` |
|
|
| 415 (Unsupported Media Type) 과 406 (Not Acceptable) 가 distinct error code 로 매핑되는지 | Spring `HttpMediaTypeNotSupportedException` / `HttpMediaTypeNotAcceptableException` 가 동일 핸들러로 뭉개질 위험 | MVC exception 매핑 test (각 예외별 distinct error code 검증) | `planned` |
|
|
| OpenAPI snapshot 과 실제 response envelope 의 drift 가 release-blocking 으로 감지되는지 | producer 와 verification suite 의 결합 정합성 검증 필요 | CI gate 의 `openapi-diff` 단계가 mismatch 시 build fail 시키는지 dry-run | `needs-confirmation` |
|
|
| oversized request (413) 가 envelope 안의 VALIDATION / RATE_LIMIT category 로 매핑되는지 | Tomcat/Spring 의 기본 413 응답이 envelope 우회 가능성 | request size limit 초과 request 의 응답 body 가 envelope shape 인지 contract test | `planned` |
|
|
| 405 응답에 `Allow` header 가 항상 포함되고 envelope shape 인지 (D12) | Spring `HttpRequestMethodNotSupportedException` 의 기본 처리가 envelope 우회 가능성 | DELETE-only endpoint 에 GET 보내고 응답 검증: status 405, `Allow: DELETE`, envelope `error.code` 존재 | `planned` |
|
|
| GET 지원 endpoint 가 HEAD 요청에 body=0 으로 동일 status 반환하는지 (D13) | Spring MVC 자동 처리 여부 의존 | sample-portfolio `GET /v1/worklogs/{id}` 에 HEAD 요청 → 200 + Content-Length 일치 + body 빈 응답 | `planned` |
|
|
| OPTIONS preflight 가 envelope 우회하고 직접 응답하는지 (D13 CORS 분기) | CORS 정책 본 branch 가 아닌 security branch 가 owner — 정합성 확인 필요 | OPTIONS 요청에 envelope 응답이 떨어지면 실패 (CORS preflight 는 envelope 미적용) | `needs-confirmation` |
|
|
| PATCH endpoint 가 merge-patch+json / json-patch+json content type 을 거부하는지 (D14 정정 후) | boundary branch B2 의 ArchUnit rule 활성화 필요 — controller 작성자가 우회 시 build fail 보장 | `@RequestMapping(consumes="application/merge-patch+json")` 가 build fail 시키는 ArchUnit test 추가 검증 | `planned` |
|
|
| PATCH 요청의 `null` 값 필드가 *명시적 null* (clear) 의미로 처리되는지 (D14, RFC7396-C3 — 미채택 근거) | Java record canonical constructor 가 absent vs null 을 같은 기본값으로 수렴 → mapper 가 `JsonNullable<T>` / `Optional<T>` wrapper 로 구분 필요. boundary branch B2 SSOT | sample-portfolio `PATCH /v1/worklogs/{id}` 에 `{"description": null}` 전송 → DB 의 description 컬럼이 NULL 로 *변경* 됨 (clear). `{}` (absent) 전송 → description 변경 *없음*. wrapper 사용 controller test | `needs-confirmation` (boundary branch B2 SSOT 와 cross-link) |
|
|
| write 응답에 `ETag` header 가 자동 발행되는지 (D15) | 모든 write controller 가 일관되게 ETag 생성하는지 contract 강제 | sample-portfolio POST/PUT/PATCH 응답에 `ETag: W/"<version>"` 헤더 존재 + 값이 envelope `data.version` 또는 `data.id+version` 의 derived | `planned` |
|
|
| `If-Match` mismatch 가 412 Precondition Failed 로 응답하는지 (D15) | optimistic lock 충돌을 409 Conflict 또는 500 으로 매핑할 위험 | sample-portfolio UPDATE 에 `If-Match: W/"0"` (stale version) 전송 → 412 + envelope `error.code=CONFLICT` 또는 별도 `PRECONDITION_FAILED` | `planned` |
|
|
| `If-None-Match` match 가 304 + body 없음 응답인지 (D15) | Spring 의 ResponseEntity 처리 또는 controller 직접 304 응답 필요 | sample-portfolio GET 응답의 `ETag` 받은 후 동일 endpoint 에 `If-None-Match: <etag>` 전송 → 304 + Content-Length 0 + body 빈 응답 | `planned` |
|
|
| 인증된 응답 default 가 `Cache-Control: no-store` 인지 (D16) | Spring Security 또는 controller default 가 비어 있어 proxy 가 임의 캐시 위험 | sample-portfolio 의 모든 응답에 `Cache-Control: no-store` 존재 (단, 명시적 cacheable opt-in endpoint 제외) | `planned` |
|
|
| content-negotiated 응답에 `Vary` header 가 자동 발행되는지 (D16) | Spring MVC 가 Accept-driven negotiation 시 자동 Vary 추가하나 모든 경우 보장 안 됨 | Accept-driven content negotiation 사용하는 endpoint 응답에 `Vary: Accept` 포함, 인증 응답에 `Vary: Authorization` 포함 | `planned` |
|
|
| LRO endpoint 가 202 + `Location` + envelope `data.{operationId,statusUrl}` 형식인지 (D17) | 현재 ca-skeleton 에 LRO endpoint 자체가 없음 — sample-portfolio fixture 신설 필요 | sample-portfolio 에 `POST /v1/worklogs:export` 같은 LRO fixture 추가 후 응답 검증 | `needs-confirmation` (sample-portfolio fixture 확장 필요) |
|
|
| polling endpoint `GET /v1/operations/{id}` 의 status enum 이 SSOT 어휘인지 (D17) | enum 어휘 (PENDING/RUNNING/SUCCEEDED/FAILED/CANCELLED) 의 변형 위험 | polling endpoint 응답 schema 의 enum 정의 + 실제 응답값 매트릭스 test | `planned` |
|
|
| pagination `size` cap 이 강제되는지 (D18, 최대 footgun) | Spring `Pageable` default max = `DEFAULT_MAX_PAGE_SIZE = 2000` (SPRING-PAGE-C4 정정 — 이전 표현 `Integer.MAX_VALUE` 는 부정확). 2000 도 DoS 위험은 충분 — `?size=2000` × 무거운 응답 = 메모리 폭발 | `?size=10000000` → 400 VALIDATION_FAILED + envelope `error.details.field=size` + `error.details.code=SIZE_EXCEEDS_MAX` · `?size=500` (Spring default 2000 이하지만 project cap 100 초과) → 400 VALIDATION_FAILED | `planned` |
|
|
| pagination `page` 0-indexed 이 Spring Pageable 정합인지 (D18) | 0-indexed vs 1-indexed 혼동 — controller 와 OpenAPI snapshot 의 drift | `?page=0` 응답 = 첫 페이지 (first), `?page=-1` → 400 VALIDATION_FAILED | `planned` |
|
|
| 빈 list 응답이 `data: []` + `meta.page.total=0` 인지 (D18) | controller 가 `null` 반환 또는 meta 누락 위험 | empty list endpoint 응답 = `{"success":true,"data":[],"meta":{"page":{"number":0,"size":20,"total":0,"sort":...}}}` | `planned` |
|
|
| `error-codes.yaml` 의 모든 row 가 실제 controller 응답의 HTTP status 와 일치하는지 (D11, registry drift detection) | registry row 와 controller drift 가 untracked 위험 | 모든 `error.code` row 에 대해 contract test 가 trigger (오류 발생 fixture) 후 실제 응답 status code 가 `http_status` column 과 일치 검증 | `needs-confirmation` (registry-governance branch 와 정합성 확인) |
|
|
| 모든 `@RequestMapping` path segment 가 AIP-122 regex `[a-z][a-zA-Z0-9]*` 매치하는지 (D19) | controller 작성자가 kebab-case (`/v1/worklog-comments`) 또는 CamelCase (`/v1/Tickets`) 사용 가능성 | ArchUnit rule 또는 Spring controller mapping inspector 로 모든 endpoint path segment regex 검증. multi-word resource fixture (예: `customerOrders`) 로 lowerCamelCase 동작 확인 | `planned` |
|
|
| sort syntax 가 Spring `Pageable` native (`?sort=field,direction`) 인지 (D20) | controller 작성자가 `?sort=-foo` (JSON:API) / `?sort=foo:desc` 같은 다른 syntax 채택 가능성 | sort syntax contract test: `?sort=createdAt,desc` 200 + `?sort=-createdAt` 400 또는 ignore 검증. multi-sort `?sort=createdAt,desc&sort=title,asc` 동작 검증 | `planned` |
|
|
| filter syntax 가 flat key=value (equality) 만 통과하는지 (D21) | controller 작성자가 RSQL / FIQL / AIP-160 DSL library 도입 가능성 | filter syntax contract test: `?status=OPEN` 200 + `?filter=status==OPEN` (DSL) 가 controller 에서 parse 되지 않고 silent 무시 또는 거부됨 검증 | `planned` |
|
|
| cursor token 이 opaque (client parse 불가) + signed (tamper 감지) + TTL (24h 후 만료) 3개 invariant 모두 만족하는지 (D22) | typed cursor 노출 / unsigned token / no TTL 중 하나라도 깨지면 contract 위반 | cursor token roundtrip test (next page 정상) + base64 decode 후 client 가 의미 있는 정보 추출 못함 검증 + tamper test (token 1byte 변조 → 400) + TTL test (24h 1초 후 token → 400) | `planned` |
|
|
| sync bulk endpoint 가 atomic 인지 + async bulk endpoint 가 LRO polling pattern 인지 (D23 sync/async 분기) | sync 에서 partial failure 허용은 AIP233-C7 위반. controller 작성자가 sync/async 구분 없이 partial failure 응답하거나 flat array body 채택 가능성 | sync batch contract test: `POST /v1/worklogs:batchCreate` 의 한 항목 fail 시 전체 rollback + HTTP 4xx + envelope.success=false. async batch contract test: `POST /v1/operations:batchCreate` 의 202 + Location + polling endpoint 의 `data.result.results[]` 가 항목별 success/error 매핑. BATCH_PARTIAL_FAILURE 가 sync 응답에 나타나면 실패. 1001 항목 size cap 위반 → 400 | `needs-confirmation` (boundary branch B14 BulkEnvelope.partial + D17 LRO endpoint pattern cross-link) |
|
|
| 모든 응답 (200/204/400/404/500 status matrix) 에 `Date` 헤더가 자동 발행되는지 (D24) | Spring/Tomcat default 가 자동 발행하지만 controller / filter / @ResponseBody 의 명시적 제거 위험 | response header presence contract test (각 status code 별로 endpoint 응답 검증) — 모든 응답에 `Date` 헤더 존재 + RFC 9110 §5.6.7 HTTP-date format 일치 | `planned` |
|
|
|
|
## 마주친 문제
|
|
|
|
> 짧은 메모만. 깊이 있는 트러블슈팅은 `raw/errors/` 로 분리하고 아래 §Cluster 에 연결.
|
|
|
|
- [[raw/errors/responseentityexceptionhandler-ambiguous-exception-handler-2026-06-02]] — `ResponseEntityExceptionHandler` 우산이 이미 다루는 `MaxUploadSizeExceededException` 을 `@ExceptionHandler` 로 가로채자 advice 등록 ambiguous → protected override 로 해소 (D8).
|
|
- [[raw/errors/mockmvc-406-produces-accept-double-fault-2026-06-02]] — 406 produces/Accept 불일치 경로의 에러 응답 직렬화 2차 실패 → 예외 직접 throw probe 로 결정적 검증 (D9).
|
|
|
|
## 묶음 (이 branch에서 파생된 자료)
|
|
|
|
<!-- GENERATED: sources:start -->
|
|
- [[raw/company-tech-blogs/idempotency-brandur-stripe-postgres]]
|
|
- [[raw/company-tech-blogs/idempotency-redis-vs-db-storage]]
|
|
- [[raw/company-tech-blogs/idempotency-toss-payments-techblog]]
|
|
- [[raw/official-docs/fetch-spec-cors]]
|
|
- [[raw/official-docs/google-aip-122-resource-names]]
|
|
- [[raw/official-docs/google-aip-127-http-transcoding]]
|
|
- [[raw/official-docs/google-aip-132-list-method]]
|
|
- [[raw/official-docs/google-aip-136-custom-methods]]
|
|
- [[raw/official-docs/google-aip-151-long-running-operations]]
|
|
- [[raw/official-docs/google-aip-158-pagination]]
|
|
- [[raw/official-docs/google-aip-160-filtering]]
|
|
- [[raw/official-docs/google-aip-185-resource-versioning]]
|
|
- [[raw/official-docs/google-aip-233-batch-create]]
|
|
- [[raw/official-docs/idempotency-aws-lambda-powertools]]
|
|
- [[raw/official-docs/idempotency-ietf-draft]]
|
|
- [[raw/official-docs/idempotency-no-api-level-github-rest]]
|
|
- [[raw/official-docs/idempotency-paypal-docs]]
|
|
- [[raw/official-docs/idempotency-square-api]]
|
|
- [[raw/official-docs/idempotency-stripe-api-ref]]
|
|
- [[raw/official-docs/jsonapi-pagination-format]]
|
|
- [[raw/official-docs/openapi-spec-3-1-0]]
|
|
- [[raw/official-docs/rfc9110-http-semantics]]
|
|
- [[raw/official-docs/rfc9111-http-caching]]
|
|
- [[raw/official-docs/spring-data-pageable-defaults]]
|
|
<!-- GENERATED: sources:end -->
|
|
|
|
<!-- GENERATED: errors:start -->
|
|
- [[raw/errors/mockmvc-406-produces-accept-double-fault-2026-06-02]]
|
|
- [[raw/errors/responseentityexceptionhandler-ambiguous-exception-handler-2026-06-02]]
|
|
<!-- GENERATED: errors:end -->
|
|
|
|
<!-- GENERATED: blog-topics:start -->
|
|
- [[raw/blog-topics/spring-responseentityexceptionhandler-transport-failure-envelope-2026-07-02]]
|
|
<!-- GENERATED: blog-topics:end -->
|
|
|
|
> 이 branch는 단일 노트가 아니라 **작업 묶음의 entry point**. 본 feature branch 는 현재 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
|
|
|
|
### 근거 자료
|
|
|
|
- [[raw/official-docs/google-aip-233-batch-create]] — AIP-233 Batch Methods: Create — `:batchCreate` URI suffix MUST + HTTP POST MUST + requests SHOULD + atomicity MUST (D23 `:batchCreate` 명칭 vocabulary normative 근거 — AIP233-C2/C3/C4/C7)
|
|
- [[raw/official-docs/google-aip-132-list-method]] — AIP-132 List method standard: `order_by` syntax (`"foo desc, bar"` 형식), `page_size`/`page_token`/`next_page_token` proto field 명명, `filter` field + AIP-160 cross-ref (future B14 sort syntax 결정 근거 — AIP132-C1~C6)
|
|
- [[raw/official-docs/google-aip-122-resource-names]] — AIP-122 Resource Names: collection segment plural + lowercase 규칙 (future B13 — resource URL naming convention 근거 후보, AIP122-C2/C3)
|
|
- [[raw/official-docs/google-aip-158-pagination]] — AIP-158 pagination: D18 size cap + cursor-based page_token opaque normative reference (AIP158-C1~C5)
|
|
- [[raw/official-docs/google-aip-151-long-running-operations]] — AIP-151 LRO 패턴 normative reference (D17 UNSUPPORTED_DECISION 해소 — AIP151-C1~C7)
|
|
- [[raw/official-docs/rfc9111-http-caching]] — RFC 9111 HTTP Caching: `no-store`/`private`/`public`/`max-age` directive normative 정의 (D16)
|
|
- [[raw/official-docs/spring-data-pageable-defaults]] — Spring Data `Pageable` 0-indexed default + `size` default 20 + `DEFAULT_MAX_PAGE_SIZE = 2000` vendor-doc 근거 (D18 — SPRING-PAGE-C1~C6)
|
|
- [[raw/official-docs/google-aip-160-filtering]] — AIP-160 filter DSL 정의 (future B15 — filter syntax 결정의 옵션 근거, AIP160-C1~C6)
|
|
- [[raw/official-docs/google-aip-136-custom-methods]] — AIP-136 Custom Methods: colon-verb URI syntax + collection-scoped custom method 패턴 (future B18 bulk operation URL pattern 근거 + D17 LRO entry point cross-ref — AIP136-C1~C5)
|
|
- [[raw/official-docs/fetch-spec-cors]] — WHATWG Fetch §3.3 CORS protocol: OPTIONS preflight 식별 기준 normative 정의 (D13 — preflight = OPTIONS + Access-Control-Request-Method, FETCH-CORS-C2)
|
|
- (기타 Sources 는 §Sources / 근거 표 참조)
|
|
|
|
### Sub-branches (세부 작업)
|
|
|
|
- (없음 — 본 branch 가 leaf)
|
|
|
|
### 오류 기록 (이 branch 작업 중 발생)
|
|
|
|
- [[raw/errors/responseentityexceptionhandler-ambiguous-exception-handler-2026-06-02]] — D8 413 핸들러 추가 시 `ResponseEntityExceptionHandler` 우산과 `@ExceptionHandler` ambiguous, override 로 해소.
|
|
- [[raw/errors/mockmvc-406-produces-accept-double-fault-2026-06-02]] — D9 406 협상 경로 에러 직렬화 2차 실패, 예외 직접 throw 로 검증.
|
|
|
|
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
|
|
|
|
- 후보 존재(이번 라운드 별도 노트 미작성, errors + branch note 로 충분): (1) `ResponseEntityExceptionHandler` 상속 시 우산 예외는 왜 `@ExceptionHandler` 가 아니라 protected override 인가, (2) 406 vs 415 의 RFC 9110 의미 차이와 둘을 같은 코드로 뭉개면 잃는 것, (3) HTTP 412(`If-Match`)↔DB optimistic lock 의 동치성, (4) ArchUnit 으로 URL 네이밍(AIP-122) 같은 *값* 규칙을 강제하는 법(annotation 값 스캔 + violations-as-data).
|
|
|
|
### 강의 (이 작업을 위해 학습한 강의)
|
|
|
|
- (없음)
|
|
|
|
### job-posting tie-ins (이 작업에서 파생된 글감)
|
|
|
|
- 후보(이번 라운드 별도 노트 미작성): "Spring `ResponseEntityExceptionHandler` 를 깨지 않고 transport 실패(405/406/413/415)를 envelope 로 분류하기" — [[raw/errors/responseentityexceptionhandler-ambiguous-exception-handler-2026-06-02]] + [[raw/errors/mockmvc-406-produces-accept-double-fault-2026-06-02]] 가 원석.
|
|
- derived blog: 생성 전. 생성 시 `wiki/blog/<slug>-YYYY-MM-DD.md` 후보
|
|
|
|
## 관련 일일 노트
|
|
|
|
> 이 branch를 작업한 날짜들. 양방향 nav 유지.
|
|
|
|
- 2026-05-21 (initial scaffolding) — daily note 미생성
|
|
- 2026-05-22 (TODO drained, D1~D10 확정) — daily note 미생성
|
|
- 2026-05-31 (D11~D18 추가, template 정렬) — daily note 미생성
|
|
|
|
## 완료 후 정리
|
|
|
|
> 머지/종료 시점에 채움. `/ingest`가 이 섹션을 기준으로 `wiki/projects/` 에 추출.
|
|
|
|
- PR 링크:
|
|
- 리뷰 메모:
|
|
- 머지 결과 / 배포 환경: 로컬/CI 검증까지 (운영 배포 없음). ca-tmpl @b15dcf5.
|
|
- **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출): → [[wiki/projects/ca-tmpl/api-evolution-and-schema]] "API contract baseline 구현" 절
|
|
- `actually-implemented` 항목: `ETags`, `PreconditionFailedException`, `CacheControlFilter`, `PageParams`, `SortParam`, `PageMeta`/`ResponseMeta.page`, `CursorCodec`(seam), `GlobalExceptionHandler`(413/406/415/405+Allow/412), `Operation`/`OperationStatus`/`OperationsController`, `WorkLogController` `:batchCreate`, `application.yml` `/v1` prefix + `PresentationSettings`, springdoc 의존.
|
|
- `locally-verified` 항목: 위 클래스의 동작 — `TransportErrorHandlingTest`, `WorkLogControllerWireTest`(ETag/304/412/pagination/sort/filter-ignore/HEAD/batch-cap/idempotency-header), `CacheControlFilterTest`, `CursorCodecTest`, `OperationsControllerWireTest`, `OpenApiSnapshotTest`, `VersioningPrefixTest`, `DateHeaderContractTest`, `ErrorCodeRegistryMappingTest`.
|
|
- `prod-verified` 항목: 없음 (운영 배포 0).
|
|
- **추출하지 않을 항목** (planned / documented-only / abandoned): D22 HMAC 운영 key/회전, D8 414 end-to-end, D3 idempotency key shape/replay, D5/D10 drift 릴리스 게이트, D16 cache layer 구현, D22 sample cursor endpoint, D14 merge-patch 차단 ArchUnit(boundary B2 소유). idempotency-key shape 는 [[raw/branch-notes/feature-rate-limit-idempotency-contract]] 소유 — 본 branch 비추출.
|