97 KiB
title, source_type, status, branch, parent_branch, related_projects, tags, created, last_reviewed, target_merge, status_label, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
| title | source_type | status | branch | parent_branch | related_projects | tags | created | last_reviewed | target_merge | status_label | id | kind | project | work_item | inherits | refines | overrides | depends_on | contract_packet | contract_packet_sha256 | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-api-contract-baseline | branch-note | verified | feature-api-contract-baseline |
|
|
2026-05-21 | 2026-06-04 | review | BR-CA-SKELETON-OPERATIONAL-CONTRACT-011 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-011 |
|
1 | a7692d0779a614a3f52af2276360b0ce9e4c9b05f0d1af1b246fbbaa322102a3 |
branch: feature-api-contract-baseline
Layer:
raw/branch-notes/— HTTP API surface 전체의 계약을 정의합니다. 완료 후/ingest로wiki/projects/에만 추출합니다.status_label:in-progress|review|merged|abandoned
부모 (필수)
이 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) 의 결정/근거/금지 사항을 정제한다.
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: /v1 API와 envelope/OpenAPI contract test가 통과한다
상속한 프로젝트 결정
| 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 |
브랜치 지역 결정
기존 branch-local 결정은 아래
## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|
선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|
목표
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:
범위
포함 범위
- 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 +
Allowheader). - 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 +
Varyheader 의무. - 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
Pageablenative). - 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/_methodform parameter — forbid 가 기본값이지만 결정 자체는 security 계약 영역. cross-cite 로만.Server/X-Powered-By/ 기술 스택 노출 header — owner: security branch. 본 branch 는 forbid 만 cross-cite.- error message i18n (
Accept-Language) — 현재 envelopeerror.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-Keyheader 표준 - raw/official-docs/idempotency-ietf-draft — IETF httpapi draft가 동일 header 이름 정의
- raw/company-tech-blogs/idempotency-brandur-stripe-postgres — Postgres 구현 reference
- (가장 가까운 reference) raw/official-docs/idempotency-stripe-api-ref — Stripe
- 검토한 대안:
- 대안 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 자체 없음)
- 대안 1: Stripe v1 pair
- 보조 결정 (저장소): 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—GlobalExceptionHandleroverride +TransportErrorHandlingTest. - D15 ETag/
If-Match→412/If-None-Match→304 —adapter-webETags/PreconditionFailedException+ sampleWorkLog.version(@Version) +WorkLogControllerWireTest. - D7/D18 pagination
meta.page+ size 1..100/page≥0 → 400 + 빈 list[]+ deep-offsetDeprecation—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.ymlPRESENTATION_API_BASE_PATH:/v1. - D22 cursor seam —
adapter-webCursorCodec(opaque base64 + HMAC + 24h TTL) +CursorCodecTest(opacity/integrity/TTL 3-invariant = §3 D22 요구 충족).
- D8 413 (
소유 범위 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-KeyPOST surface —create/batchCreate의@RequestHeader(server-tolerant) +post_accepts_idempotency_key_header(shape는 여전히 rate-limit branch). -
D2 versioning 강제 —
VersioningPrefixTest(/v1/probe200,/probe404 → 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:export202+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.ymlca-skeleton.presentation.api-base-path: ${PRESENTATION_API_BASE_PATH:/v1}+adapter-web/settings/PresentationSettings(코드 default"", 운영 default/v1) (D2). - OpenAPI:
adapter-web/build.gradlespringdoc-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_statuscolumn). 본 branch 는 registry 의 매핑 정합성 contract test 의 producer. registry row 와 실제 controller 응답의 drift 는 contract test 가 release-blocking (D11). - 2026-05-31: HTTP method 미지원 응답은 405 Method Not Allowed +
Allowresponse header 의무.Allowheader 는 해당 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 7396application/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+jsoncontent type 사용은 raw/branch-notes/feature-boundary-validation-mapping-contract B2 의 ArchUnit ruleno_merge_patch_json_media_type_string으로 build 실패 차단. RFC 6902 (application/json-patch+json) 도 동일 이유로 미채택. - 2026-05-31: Conditional request 지원: read 응답에
ETagheader 발행 (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=Nopt-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}+ envelopedata.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 강제:
page0-indexed (SpringPageabledefault 와 정합),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
Pageablenative?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 endpointGET /v1/operations/{id}의data.result.results[]에서 항목별 success/error 반환 (partial failure 허용).BATCH_PARTIAL_FAILUREenvelope category 는 async batch 의 polling 응답에서만 사용. flat array body (POST /v1/worklogswith[...]) 금지. 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-C1official-reference (AIP-158 — AIP158-C1size 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 (필수 준수):
- R1. Reference 필수 — 각 sub-section / row / cell 은 본 branch 의
Decision ID(예: D1, D2) + 그 결정의Supporting Claim ID(예:RFC9110-C5) 를 reference.- R2. UNSUPPORTED_IMPL_DECISION 명시 — 근거 raw 가 원칙 만 권고하고 detail 은 권고하지 않는 cell 은
UNSUPPORTED_IMPL_DECISION라벨 + 사용자 trade-off 근거 한 줄.- 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-C1C3 + SPRING-PAGE-C1C6 + AIP158-C1C5) · 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-C13C17) · D16 (cache policy + Vary, RFC9111-C1C5 + 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 의
sizedefault 20 / max 100 / min 1 /page > 10000threshold 의 정확한 숫자 는 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는nameMUST 만 요구, RESTLocation매핑은 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 testcolumn 을 그대로 펼쳐 쓴 catalog. 각 라인은 §2 의 특정 row + Decision ID 와 1:1 매핑.
- UNSUPPORTED_IMPL_DECISION: 본 sub-section 은 §2 의 직접 도출 — 별도 임의 결정 없음.
- OpenAPI schema와 실제 response envelope가 다르면 실패 (D10).
/v1prefix 없는 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 응답에
Allowheader 가 없거나 envelope 우회로 직접 응답하면 실패 (D12). - GET 지원 endpoint 가 HEAD 요청에 405/404 응답하면 실패 (D13 HEAD-mirror-GET test).
- PATCH endpoint 가
application/merge-patch+json또는application/json-patch+jsoncontent type 을 허용하면 실패 — boundary branch B2 의 ArchUnitno_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 응답에
ETagheader 가 없거나If-Matchmismatch 시 412 가 아닌 409/500 으로 매핑되면 실패 (D15 conditional request matrix test). If-None-Matchmatch 시 304 응답에 body 가 동봉되면 실패 (D15 cache validation test).- 인증된 응답 default 가
Cache-Control: no-store가 아니거나 content-negotiated 응답에Varyheader 가 없으면 실패 (D16 cache policy test). - 비동기 endpoint 가 202 +
Location+ envelopedata.{operationId,statusUrl}형식이 아니거나 polling endpoint 의status가 enum 어휘 밖이면 실패 (D17 LRO test). error-codes.yaml의 임의의 row 에 대해 실제 controller 응답의 HTTP status 가 row 의http_statuscolumn 과 다르면 실패 (D11 mapping consistency test, registry 와 controller 의 drift 감지).- controller
@RequestMappingpath 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헤더 누락 또는 SpringHttpRequestMethodNotSupportedException가 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-Matchmismatch 를 409/500 으로 매핑하면 실패(→ 412),If-None-Matchmatch 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누락 또는 인증 응답이publiccacheable 이면 실패(defaultno-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 에 의존 — registryhttp_statuscolumn 이 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-Keykey 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-Bysuppression·X-HTTP-Method-Overrideforbid. - 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 에 의존 —
/v1deprecation Sunset/Deprecation 헤더(D2) + 깊은 offsetDeprecation헤더 형식(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).
- raw/branch-notes/feature-operational-error-observability-foundation 의
검증해야 할 주장
공식 문서/사례는 근거지만 내 프로젝트에서의 동작을 자동 보장하지 않는다. 구현 전/중/후 실제로 검증해야 하는 주장.
| 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에서 파생된 자료)
- 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
- raw/errors/mockmvc-406-produces-accept-double-fault-2026-06-02
- raw/errors/responseentityexceptionhandler-ambiguous-exception-handler-2026-06-02
이 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 —
:batchCreateURI 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_bysyntax ("foo desc, bar"형식),page_size/page_token/next_page_tokenproto field 명명,filterfield + 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-agedirective normative 정의 (D16) - raw/official-docs/spring-data-pageable-defaults — Spring Data
Pageable0-indexed default +sizedefault 20 +DEFAULT_MAX_PAGE_SIZE = 2000vendor-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우산과@ExceptionHandlerambiguous, 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/v1prefix +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 비추출.