Files
llm-wiki/raw/branch-notes/feature-api-contract-baseline.md

97 KiB
Raw Permalink Blame History

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
ca-skeleton
branch
ca-skeleton
api-contract
openapi
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
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-API-VERSIONING-001@1
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-OPENAPI-001@1
1 a7692d0779a614a3f52af2276360b0ce9e4c9b05f0d1af1b246fbbaa322102a3

branch: feature-api-contract-baseline

Layer: raw/branch-notes/ — HTTP API surface 전체의 계약을 정의합니다. 완료 후 /ingestwiki/projects/에만 추출합니다. status_label: in-progress | review | merged | abandoned

부모 (필수)

이 branch 가 어느 작업 묶음에 속하는지. 본 branch 는 project-note 의 직접 자식 (parent_branch: 비어 있음).

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 + 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 참조.

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 + AllowGlobalExceptionHandler 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 DeprecationPageParams/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 seamadapter-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-foundationerror-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 Claimsraw/<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=falsename 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-C1C6) + official-reference (AIP-158 — AIP158-C1C5) + 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-C1C3) · 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 의 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-C4name 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.yamlhttp_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 allowedAllow 헤더 누락 또는 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 requestIf-Match mismatch 를 409/500 으로 매핑하면 실패(→ 412), If-None-Match match 304 에 body 동봉하면 실패. (D15)
    • pagination footgunsize > 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 방향 압축):

검증해야 할 주장

공식 문서/사례는 근거지만 내 프로젝트에서의 동작을 자동 보장하지 않는다. 구현 전/중/후 실제로 검증해야 하는 주장.

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 에 연결.

묶음 (이 branch에서 파생된 자료)

이 branch는 단일 노트가 아니라 작업 묶음의 entry point. 본 feature branch 는 현재 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.

근거 자료

Sub-branches (세부 작업)

  • (없음 — 본 branch 가 leaf)

오류 기록 (이 branch 작업 중 발생)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

  • 후보 존재(이번 라운드 별도 노트 미작성, 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 (이 작업에서 파생된 글감)

관련 일일 노트

이 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 비추출.