Files
llm-wiki/vault/30-knowledge/projects/ca-tmpl/api-evolution-and-schema.md
T

34 KiB

title, source_type, status, confidence, tags, related_projects, last_reviewed
title source_type status confidence tags related_projects last_reviewed
ca-tmpl - API Evolution & Schema 결정 (versioning + contract baseline + serialization) project verified high
ca-tmpl
api-design
versioning
pagination
conditional-request
http-cache
openapi
schema
ca-tmpl
2026-07-02

ca-tmpl - API Evolution & Schema 결정 (versioning + contract baseline + serialization)

Layer: wiki/projects/ — 내 프로젝트 사실. 일반 개념은 wiki/concepts/api-evolution-and-schema 참고.

프로젝트 컨텍스트

ca-tmpl은 Clean Architecture 기반 백엔드 skeleton 프로젝트다. 이 문서는 API surface 의 세 영역을 다룬다.

  • API contract baseline (구현됨) — versioning (/v1 path prefix), pagination/sort, conditional request (ETag/If-Match/304/412), HTTP cache policy, OpenAPI producer, long-running operation, batch endpoint. feature-api-contract-baseline branch 가 producer-소유 결정을 실제 코드(adapter-web + sample-portfolio)에 구현하고 단위/슬라이스/임베디드 테스트로 검증했다. locally-verified.
  • Compatibility / deprecation 축 (설계만)90d public + 30d internal migration window, 7행 breaking change catalog, RFC 8594 Sunset + Deprecation 헤더 병기, OpenAPI deprecated: true marker. feature-api-compatibility-deprecation-contract branch 의 결정이며 코드 미구현 (documented-only).
  • Schema / serialization 축 (출력측 부분 구현) — ISO-8601 offset datetime, BigDecimal scale 2 + HALF_UP, unknown field strict inbound, null/empty/missing 분리. feature-schema-serialization-contract branch 의 결정이다. 직렬화 출력측 핀 (WRITE_DATES_AS_TIMESTAMPS=false / WRITE_BIGDECIMAL_AS_PLAIN=true) + new BigDecimal(double) 정적 차단 ArchUnit 룰 + 직렬화 동작 테스트는 실제 코드로 구현·로컬 검증됨 (locally-verified). 단 입력측 deser switch·null/empty/missing 3-상태(Patch<T>)는 sibling feature-boundary-validation-mapping-contract 가 소유하며, OpenAPI drift release gate (D5) · 제거-field 재사용 도구 (D6) · Avro Schema Registry (D7) · money string-vs-number per-API 코드 시연은 미구현 (documented-only / planned / needs-confirmation).

Ground-truth 대조 (2026-06-04, ca-tmpl @b15dcf5 "API 계약 baseline 구현"): contract baseline 축의 아래 actually-implemented / locally-verified 항목은 ca-tmpl 저장소 commit b15dcf5 의 실제 코드(dev.caskeleton.* package root)와 1:1 대조해 확인했다.

Ground-truth 대조 (2026-06-04, ca-tmpl @5d89766 "schema/serialization 계약 직렬화 출력측 구현"): schema/serialization 축 출력측 항목 — no_bigdecimal_double_constructor ArchUnit 룰(CleanArchitectureTest), JacksonSerializationPolicyTest, application.yml/application-test.yml/.env 의 직렬화 핀 두 키 — 은 commit 5d89766 의 실제 코드와 1:1 대조해 확인했다 (locally-verified). compatibility/deprecation 축 + schema 의 D5/D6/D7 + per-API money 직렬화 코드 시연은 여전히 documented-only / planned / needs-confirmation.

실제 구현 내용 (actually-implemented)

API contract baseline 축 (feature-api-contract-baseline) + schema/serialization 축의 출력측 (feature-schema-serialization-contract) 이 구현됨. compatibility/deprecation 축 + schema 의 D5/D6/D7 은 코드 부재 (§문서/계획만 존재).

코드에 존재하는 클래스/필터 (테스트 유무와 무관하게 production main 소스에 존재):

  • D2 versioning/v1 path prefix 는 설정 주도(app-bootstrap/.../application.ymlca-skeleton.presentation.api-base-path: ${PRESENTATION_API_BASE_PATH:/v1}) + adapter-web PresentationSettings (env 누락// 누락 시 warn + 보정). 코드 자체의 default 는 "", 운영 default 는 /v1.
  • D18/D20 pagination/sortadapter-web PageParams (page≥0, size 1..100, deep-offset>10000 플래그), SortParam (Spring native field,direction 파싱 + 비-네이티브 reject), shared-contract PageMeta/ResponseMeta.page.
  • D15 conditional requestadapter-web/conditional/ETags (weakFromVersion = W/"<version>", lenient matches), PreconditionFailedException.
  • D16 cache policyadapter-web/filter/CacheControlFilter (@Order(HIGHEST_PRECEDENCE+20), 모든 응답에 Cache-Control: no-store + Vary: Accept, Accept-Encoding, Authorization).
  • D22 cursor (SEAM)adapter-web/cursor/CursorCodec (base64url(iat:payload) + HMAC-SHA256 + 24h TTL) + CursorException.
  • D17 LROsample-portfolio OperationsController (POST /worklogs:export → 202 + Location + Operation, GET /operations/{id} polling), shared-contract Operation/OperationStatus, SampleOperationStore.
  • D8/D9/D12 transport errorsadapter-web/error/GlobalExceptionHandler 가 413(PAYLOAD_TOO_LARGE)/406(NOT_ACCEPTABLE)/415(UNSUPPORTED_MEDIA_TYPE)/405(METHOD_NOT_ALLOWED + Allow header)/412(PRECONDITION_FAILED) 를 envelope 로 매핑.
  • D23 batchsample-portfolio WorkLogControllerPOST /worklogs:batchCreate (단일 tx atomic, @Size(max=1000) cap) + BatchCreateWorkLogsUseCase.
  • D10 OpenAPI produceradapter-web/build.gradlespringdoc-openapi-starter-webmvc-api:2.8.6 의존 추가, /v3/api-docs 노출.

Schema / serialization 출력측 (feature-schema-serialization-contract, ca-tmpl @5d89766)

직렬화 출력측 계약을 코드에 핀하고 정적으로 차단했다. 입력측 deser switch(FAIL_ON_UNKNOWN_PROPERTIES/FAIL_ON_NULL_FOR_PRIMITIVES/READ_UNKNOWN_ENUM_VALUES_AS_NULL=false)와 null/empty/missing 3-상태(Patch<T>)는 sibling feature-boundary-validation-mapping-contract 소유이므로 본 축 출력측 만 여기서 다룬다.

  • D2 datetime 직렬화 핀app-bootstrap/.../application.ymlspring.jackson.serialization.write-dates-as-timestamps=false (env SPRING_JACKSON_SER_WRITE_DATES_AS_TIMESTAMPS 바인딩). java.time 값이 epoch/배열이 아니라 ISO-8601 문자열로 직렬화됨. JavaTimeModule 은 Spring Boot starter-json auto-config 가 classpath 의 jackson-datatype-jsr310 을 자동 등록 — 명시 등록 코드는 없음.
  • D3 BigDecimal plain 직렬화 핀application.ymlspring.jackson.generator.write-bigdecimal-as-plain=true (env SPRING_JACKSON_GEN_WRITE_BIGDECIMAL_AS_PLAIN 바인딩). 지수 표기(1.23E+10) 대신 plain notation 으로 직렬화.
  • D3 정적 차단 ArchUnit 룰app-bootstrap/.../architecture/CleanArchitectureTestno_bigdecimal_double_constructor (@ArchTest). dev.caskeleton.. production 패키지에서 callConstructor(BigDecimal.class, double.class) / float.class 호출을 build fail. (new BigDecimal(0.1) 의 부동소수 잔차 함정 = SBMS-C3 차단)
  • 위반 fixturearchitecture/violations/serialization/BigDecimalDoubleConstructorFixture (new BigDecimal(double/float) 사용) — 룰의 vacuous-pass 방지용 negative fixture.
  • 테스트 리소스 핀application-test.yml 에 위 두 키를 리터럴(false/true)로 박아 테스트 프로파일에서도 동일 계약 유지.

이 핀들은 현재 Spring Boot 기본값과 일치하나, future default flip 회귀를 차단하기 위해 명시했다 (rationale 은 .env 주석에 spring.mvc.problemdetails.enabled=false 와 동일 논리로 기록).

compatibility/deprecation 축: 없음 (version interceptor, Sunset/Deprecation header bean, OpenAPI deprecation marker 모두 부재). schema 축의 D5 OpenAPI drift release gate · D6 제거-field 재사용 도구 · D7 Avro Schema Registry · money string-vs-number per-API 코드 시연: 부재 (§문서/계획만 존재 / SEAM).

로컬/dev 검증 (locally-verified)

위 contract baseline 구현은 단위/슬라이스/임베디드-컨테이너 테스트로 동작이 확인됐다 (./gradlew check + ArchUnit gate PASS):

  • TransportErrorHandlingTest — 413/406/415 distinct + 405 + Allow header.
  • WorkLogControllerWireTest — D15 ETag 발행 / If-None-Match→304 / If-Match mismatch→412, D7/D18 meta.page + size·page 경계 400 + 빈 list [] + deep-offset Deprecation 헤더, D20 sort 네이티브/비-네이티브, D21 flat filter 무시(filter_dsl_is_ignored_not_parsed), D13 HEAD-mirror-GET(head_on_get_endpoint_is_supported_not_405), D23 batch size cap(batch_over_size_cap_is_400, 1001→400), D3 Idempotency-Key POST surface(post_accepts_idempotency_key_header, server-tolerant).
  • CacheControlFilterTest — D16 default no-store + Vary.
  • CursorCodecTest — D22 opacity / integrity(서명 변조 탐지) / TTL 3-invariant.
  • ETagsTest, PageParamsTest, SortParamTest — adapter 단위 검증.
  • OperationsControllerWireTest — D17 202 + Location + data.{operationId,statusUrl} + polling.
  • OpenApiSnapshotTest — D10 임베디드 RANDOM_PORT 컨테이너에서 /v3/api-docs 200 응답 + WorkLogController 반영.
  • VersioningPrefixTest — D2 /v1/probe 200, /probe 404 (unversioned public endpoint 불가).
  • DateHeaderContractTest — D24 임베디드 Tomcat 200·404 응답에 Date 헤더.
  • ErrorCodeRegistryMappingTest — D11 405/406/412/413/414/415 row 와 controller 응답 drift FAIL (producer contract test).

Schema / serialization 출력측 (feature-schema-serialization-contract, @5d89766) 테스트:

  • JacksonSerializationPolicyTest — ① JacksonProperties 바인딩 assert (WRITE_DATES_AS_TIMESTAMPS=false, WRITE_BIGDECIMAL_AS_PLAIN=true), ② wired ObjectMapper 직렬화 동작 assert: OffsetDateTime(UTC)→"1985-04-12T23:20:50.52Z", LocalDate"2026-06-02", new BigDecimal("1.10")1.10 (trailing zero 보존), 대형 값(12300000000000000000.00)이 비-scientific notation. ApplicationContextRunner 로 effective bean 동작까지 검증해 JavaTimeModule 누락 회귀(배열 직렬화)도 잡는다.
  • ArchitectureViolationFixtureTest.no_bigdecimal_double_constructor_catches_double_and_float_constructors — D3 ArchUnit 룰이 fixture 의 new BigDecimal(double/float) 를 실제로 잡는지 검증 (vacuous-pass 방지).
  • 검증 명령: ./gradlew verifyCleanArchitectureDependencies + :app-bootstrap:test + 전체 test 모두 BUILD SUCCESSFUL.

compatibility/deprecation 축 + schema 의 D5/D6/D7: 없음. Sunset+Deprecation 헤더 응답·api-version 헤더 라우팅·OpenAPI drift release gate·제거-field 재사용 도구·Avro compat 자동검사 어느 것도 로컬에서 실행/통합 테스트로 확인된 바 없다. per-API money string-vs-number 직렬화도 sample 도메인에 money 필드가 없어 코드 시연 없음(문서 의무만).

운영 검증 (prod-verified)

없음. ca-tmpl 은 운영 배포가 없다. contract baseline 항목은 전부 로컬/CI 검증까지이며, compatibility/schema 축은 90d/30d migration window·deprecation cutover·Sunset 시점 410 응답 같은 운영 검증 0건이다.

SEAM / 계획만 존재 (planned) — contract baseline 축

형제 branch 또는 인프라에 막혀 의도적으로 seam 또는 planned 로 남긴 항목 — 면접에서 "구현했다"고 말하면 안 되는 경계:

  • D22 HMAC 키 회전 / 운영 key 주입CursorCodec 은 주입식 key 와 withDevKey() (dev/test 전용) factory 만 제공. production key wiring + rotation 은 feature-security-operational-baseline 소유, 미구현. encode/decode·opacity·integrity·TTL 메커니즘 자체는 구현됨.
  • D8 414 URI Too Long end-to-end — Tomcat/gateway 가 Spring dispatch 전에 거부하므로 code + registry row 만 존재, end-to-end 검증 없음.
  • D3 key shape / replay semantics — header 이름(Idempotency-Key)과 POST surface 수용만 구현. key shape/scope/replay 는 feature-rate-limit-idempotency-contract 소유.
  • D5 / D10 drift 릴리스 게이트 — OpenAPI drift release-blocking 집행은 feature-contract-verification-test-suite 소유. 이 branch 는 producer(snapshot 발행 + registry mapping 정합 test)까지.
  • D16 cache layer — Redis/CDN 구현은 feature-cache-consistency-contract 소유. 이 branch 는 HTTP 응답 header 정책(no-store/Vary)만.
  • D22 sample cursor endpointCursorCodec 만 있고 cursor 페이징을 노출하는 sample endpoint 는 §Test Contract 미요구 (optional).
  • D14 PATCH merge-patch+json 차단 — content type 정책은 이 branch 가 producer 지만 ArchUnit rule no_merge_patch_json_media_type_string 와 mapper 구현은 feature-boundary-validation-mapping-contract B2 소유 (cross-branch SSOT).

근거 미명시 구현 결정 (UNSUPPORTED_IMPL_DECISION 잔존)

표준이 원칙 만 권고하고 숫자/메커니즘 은 project-internal trade-off 인 지점 — 면접에서 "표준이라서"가 아니라 "내가 이렇게 trade-off 했다"로 말해야 함:

  • pagination size cap 100 / min 1 / deep-offset 10000 — Spring 기본 DEFAULT_MAX_PAGE_SIZE 는 2000(PageParams 주석에도 명시). 100 cap 은 DoS 방지용 추가 제한, 숫자는 표준 근거 없음.
  • ETag lenient(weak) 비교 — RFC 9110 은 If-Matchstrong comparison 을 MUST 로 규정하나(ETags javadoc 에 명시), skeleton 은 W/ 마커·따옴표를 무시하는 lenient 비교로 weak-ETag 형태가 그대로 optimistic lock 을 구동하게 했다. production fork 는 strong ETag 로 교체 가능.
  • cursor 24h TTL + HMAC-SHA256 선택 — AIP-158 은 opacity/URL-safe 만 MUST, TTL 숫자와 서명 알고리즘은 project-internal.
  • LRO status enum 5종(PENDING/RUNNING/SUCCEEDED/FAILED/CANCELLED) — AIP-151 은 done/response/error 이진 모델만 정의, 5종 어휘 매핑은 project-internal.

문서/계획만 존재 (documented-only / planned)

Compatibility / deprecation 축 (feature-api-compatibility-deprecation-contract) 은 결정/설계만 있고 코드 미구현(documented-only) 이다. Schema / serialization 축출력측 (datetime/BigDecimal 핀 + ArchUnit) 만 locally-verified (위 §실제 구현 내용 참조) 이고, 아래 D5/D6/D7 + per-API money 직렬화는 여전히 미구현이다. contract baseline 의 locally-verified 와 혼동하면 안 된다.

다음 항목은 모두 canonical 계약 문서와 branch-note 단계에 머물러 있다. 면접에서 "구현했다 / 운영했다"고 말하면 안 된다.

Compatibility / deprecation 결정

  • 90d public + 30d internal migration window: 외부 client는 90일, internal client는 30일의 이중 window로 deprecated API를 계속 응답하면서 marker로 신호한다. Stripe의 freeze-forever, GitHub의 24mo EOL과 비교 검토 후 internal-first 환경 trade-off로 90d/30d를 선택.
  • 7행 breaking change catalog: 응답 필드 제거 / 응답 필드 의미 변화 / required request field 추가 / enum value 제거 / enum value 의미 변화 / narrow enum(허용값 축소) / 기본값 변경 — 7항목을 breaking으로 분류. Google AIP-180 정의를 ca-tmpl 도메인에 맞게 행 단위로 catalog화.
  • Sunset 헤더 (RFC 8594) + Deprecation 헤더 병기: Sunset 단독은 언제 사라지는지만 알리므로 지금 deprecated인지 신호인 Deprecation 헤더를 함께 보낸다. concept §흔한 오해 항목과 정합.
  • OpenAPI deprecated: true marker: operation / schema 양쪽에 둘 수 있는 표준 marker로 deprecation을 schema SSOT에 박는다.
  • Sunset + Deprecation 헤더 paired 전송 결정 (2026-05-22): API deprecation 응답은 Sunset: <HTTP-date> + Deprecation: @<unix-epoch> 헤더를 함께 송신한다. 단독 Sunset 금지. paired invariant는 "Sunset 시점 ≥ Deprecation 시점". 추가로 Link: <url>; rel="deprecation" (정책 문서), Link: <url>; rel="sunset" (마이그레이션 가이드)를 권장. 근거: raw/official-docs/sunset-deprecation-headers-paired-usage. 상태: documented-only — bean / interceptor 코드 미작성.

출처: raw/project-notes/ca-skeleton-operational-contract §13 API Contract Surface + §29 G-F (외부 근거 인덱스), raw/branch-notes/feature-api-compatibility-deprecation-contract.

Blog-topic ingest: api-deprecation-sunset-header-migration-window (2026-07-02)

raw/blog-topics/api-deprecation-sunset-header-migration-window-2026-07-02 는 위 compatibility/deprecation 축을 블로그로 풀기 위한 raw seed다. canonical 승격 기준은 다음처럼 정리했다.

  • 프로젝트 사실로 보존: D5-D8은 feature-api-compatibility-deprecation-contract 에 기록된 ca-tmpl 결정이다. 단 구현/운영 검증이 없으므로 등급은 documented-only / needs-confirmation 이다.
  • source-backed 로 말할 수 있는 부분: RFC 8594 Sunset, Deprecation header paired usage, Google AIP-180 기반 breaking-change 분류, OpenAPI deprecated: true marker 의 존재.
  • project-local policy 로만 말할 부분: 90d public + 30d internal 숫자, release-blocking diff gate, compatibility fixture 결합 방식. 표준 요구사항처럼 쓰지 않는다.
  • 블로그 전 과장 방지: 실제 API deprecation 운영 경험, 외부 client migration coordination, 410 cutover 실측은 없다.

Schema / serialization 결정 (출력측은 위 §에서 구현, 아래는 미구현분만)

아래 항목 중 datetime/BigDecimal 출력측 핀new BigDecimal(double) 정적 차단은 @5d89766 에서 locally-verified (§실제 구현 내용 참조). unknown field strict inbound 와 null/empty/missing 분리는 sibling feature-boundary-validation-mapping-contractlocally-verified (입력측 deser + Patch<T>). 여기 남는 미구현분은 D5/D6/D7 + per-API money 직렬화 코드 시연이다.

  • per-API money string-vs-number 직렬화 시연 (documented-only): scale 2 + HALF_UP 기본 + plain notation 핀은 구현됐으나, 외부/금융 API = string vs 내부 API = number+plain 의 endpoint별 명시 선택은 문서 의무(adapter-web 계약 문서)로만 박혔다. sample 도메인(WorkLog)에 money 필드가 없어 @JsonSerialize(ToStringSerializer) 같은 코드 시연은 없다.
  • Field 재사용 금지 catalog 정책 (자체 markdown 또는 OpenAPI x-removed-fields) (needs-confirmation, D6): Protobuf reserved 시맨틱(field number/name 재사용 영구 차단)을 JSON 환경에서 흉내내기 위해 제거된 field 이름/번호를 catalog로 관리하고 CI에서 재사용을 검출. 두 후보 — (a) OpenAPI Specification Extension x-removed-fields + 자체 lint, (b) 별도 markdown catalog + CI cross-check — 중 도구 선택이 미정. 2026-05-22 needs-confirmation. 출처: raw/official-docs/protobuf-reserved-vs-json-openapi-extension.
  • OpenAPI drift release gate (planned, D5): response 측 "schema 없는 field 미노출" 의 실제 강제는 verification suite 소유. springdoc producer 는 존재하나 release-blocking drift gate 는 feature-contract-verification-test-suite 미구현.
  • Avro Schema Registry compat 자동검사 (needs-confirmation, D7): outbox/event 한정 검토 가치. 외부 REST/JSON 은 JSON 유지. Confluent compatibility level enforcement 메커니즘 미확보.

출처: raw/project-notes/ca-skeleton-operational-contract §16 Schema / Serialization Contract + §29 G-F, raw/branch-notes/feature-schema-serialization-contract.

Blog-topic ingest: spring-boot-serialization-contract-pins (2026-07-02)

raw/blog-topics/spring-boot-serialization-contract-pins-2026-07-02 는 schema/serialization 출력측 구현을 블로그로 풀기 위한 raw seed다. canonical 승격 기준은 다음처럼 정리했다.

  • locally-verified 로 말할 수 있는 부분: WRITE_DATES_AS_TIMESTAMPS=false, WRITE_BIGDECIMAL_AS_PLAIN=true 설정 pin, wired ObjectMapper 직렬화 테스트, new BigDecimal(double/float) ArchUnit 차단과 negative fixture.
  • source-backed 로 말할 수 있는 부분: RFC 3339 datetime 표현, Java BigDecimal 생성자/scale/rounding 의미, Jackson serialization feature의 역할.
  • project-local policy 로만 말할 부분: 현재 Spring Boot 기본값과 같아도 future default drift를 막기 위해 명시 pin + effective-bean test를 둔 결정.
  • 블로그 전 과장 방지: 입력측 deser switch, null/empty/missing 3-상태, per-API money string-vs-number 직렬화 예제는 이 branch의 구현 범위가 아니다. 특히 sample 도메인에는 money field 코드 시연이 없다.

5종 대안 검토 결과

concept 문서(wiki/concepts/api-evolution-and-schema) Standard 섹션의 5개 진영 — Stripe date-based / GitHub X-GitHub-Api-Version + 24mo EOL / Google AIP-180 / Twitter tier-based / Spring HATEOAS — 을 비교한 결과 internal-first + 단일 팀 trade-off로 api-version 헤더 + 90d/30d migration window + Sunset+Deprecation 병기를 채택. 사유는 concept 문서 한계 / 주의점 섹션과 동일.

versioning/compatibility 대안 비교 자체는 문서/설계 단계 — version interceptor, Sunset header bean 미작성. (Jackson 직렬화 출력측 핀은 별개로 구현됨, §실제 구현 내용 참조.)

면접에서 말할 수 있는 범위

자신 있게 답할 수 있는 질문

  • 90d public + 30d internal migration window 근거 — Stripe(freeze forever)는 외부 결제 컨슈머 규모에 특화된 trade-off라 internal에 그대로 차용 시 server에 N개 버전 분기를 영구 운반, GitHub 24mo EOL은 catalog에 410 응답 명시가 없으면 사실상 어느 날 갑자기 410과 같음. internal-first 단일 팀 환경에서는 deploy lag을 흡수할 수 있는 가장 짧은 두 layer로 90d/30d.
  • Sunset vs Deprecation 헤더 차이 + 함께 보내는 이유Sunset(RFC 8594)은 언제 사라지는지의 HTTP-date 신호(ABNF: Sunset = HTTP-date), Deprecation 헤더(draft-ietf-httpapi-deprecation-header)는 지금 deprecated인지의 Structured Date 상태 신호. 하나만 보내면 "사라질 날짜는 아는데 권장 여부는 모름" 또는 그 반대 상태가 되므로 paired 송신이 IETF httpapi WG 권고. paired invariant는 "Sunset 시점 ≥ Deprecation 시점". 추가로 Link rel="deprecation" / rel="sunset"으로 사람-가독 가이드 연결. ca-tmpl도 결정 사항에 paired 전송을 명시 박음(2026-05-22).
  • Narrow enum이 breaking인 이유 — server-side에서는 허용값 축소가 invariant 강화처럼 보이지만, 이전 enum value를 합법적으로 보내던 client 입장에서는 어제까지 통과하던 요청이 오늘 거부됨. enum value 추가도 client side에 unknown enum fallback이 contract로 없으면 breaking.
  • Strict inbound + tolerant outbound 의미 — 요청은 unknown field를 거부해 typo/payload smuggling 방어, 응답은 schema 정의 외 field 누출을 막음. 단 concept 문서가 지적하듯 정확한 표현은 "strict inbound / schema-controlled outbound".
  • BigDecimal new BigDecimal(double) 함정 + ArchUnit 정적 차단new BigDecimal(0.1)0.1000...555 잔차를 담고 new BigDecimal("0.1")/BigDecimal.valueOf는 정확하다. ca-tmpl 은 이 함정을 no_bigdecimal_double_constructor ArchUnit 룰(callConstructor(BigDecimal.class, double.class)/float.class)로 production 패키지에서 build fail 시키고, vacuous-pass 방지 fixture 테스트까지 둔다 (locally-verified, @5d89766). HALF_UP 은 금융 round-half-up 관례와 정합. JSON number 직렬화 시 JS Number 정밀도 손실이 있어 외부/금융 API 는 string 직렬화 권장 — 단 per-API string-vs-number 는 문서 의무로만 박혔고 sample 도메인에 money 필드가 없어 코드 시연은 없다.
  • serialization 계약을 '기본값'이 아니라 '명시 핀 + effective-bean 테스트'로 고정한 이유WRITE_DATES_AS_TIMESTAMPS=false/WRITE_BIGDECIMAL_AS_PLAIN=true는 현재 Spring Boot 기본값과 일치하나, future default flip 회귀를 차단하려고 application.yml/application-test.yml/.env에 명시 핀했다 (spring.mvc.problemdetails.enabled=false와 동일 논리). JacksonSerializationPolicyTestApplicationContextRunner로 wired ObjectMapperOffsetDateTime"...Z"/LocalDate"YYYY-MM-DD"/BigDecimal→plain 직렬화 동작까지 검증해 JavaTimeModule 누락 회귀(배열 직렬화)도 잡는다 (locally-verified, @5d89766).
  • conditional request 가 DB optimistic lock 과 같은 충돌의 두 표현이라는 점 — read 응답이 entity @Version 으로부터 W/"<version>" ETag 를 발행하고(ETags.weakFromVersion), write 가 If-Match 로 그 버전을 제시한다. 버전이 안 맞으면 HTTP layer 에서 412 Precondition Failed (PreconditionFailedExceptionGlobalExceptionHandler), 같은 충돌이 persistence layer 면 serialization failure 로 표현된다. ca-tmpl 은 WorkLogControllerWireTest 로 ETag 발행/304/412 를 검증했다 (locally-verified).
  • 인증된 API 의 안전한 cache default = no-storeCacheControlFilter 가 모든 응답에 Cache-Control: no-store + Vary: Accept, Accept-Encoding, Authorization 를 박아 proxy/CDN cache poisoning 을 막는다. cacheable endpoint 만 ResponseEntityCache-Control 로 opt-in. Spring Security 자체 cache-control 은 비활성화해서 이 필터를 단일 owner 로 둠.
  • pagination 의 size cap 이 왜 DoS 방어인가 + Spring 기본값과의 관계size 를 1..100 으로 제한하고 page<0/size 범위 밖은 400 VALIDATION_FAILED (PageParams). Spring 의 기본 DEFAULT_MAX_PAGE_SIZE 는 Integer.MAX_VALUE 가 아니라 2000 이며, 100 cap 은 그 위에 얹은 project-internal 추가 제한이라는 점까지 말할 수 있다.
  • batch endpoint 의 sync = atomic 결정POST /worklogs:batchCreate 는 AIP-136 colon-verb + 단일 트랜잭션 all-or-nothing (partial 금지), @Size(max=1000) cap. partial failure 는 async LRO polling 응답에서만 허용. batch_over_size_cap_is_400 으로 검증.

적당히 답할 수 있는 질문

  • Stripe date-based versioning vs ca-tmpl — Stripe는 account 단위 version pin + freeze forever로 외부 결제 컨슈머 deploy lag을 server 측 영구 분기로 흡수, ca-tmpl은 헤더 기반 + 시한 migration window로 server 분기 부담을 한정. 다만 외부 컨슈머 규모 차이가 trade-off의 본질이라 "ca-tmpl이 더 낫다" 식의 단정은 금지.

답하면 안 되는 질문 (모른다고 해야 함)

  • "API deprecation을 운영해 본 경험" — 답: 없음. ca-tmpl은 운영 배포 자체가 없다.
  • "외부 컨슈머와 migration coordination을 해본 경험" — 답: 없음. 외부 컨슈머가 존재하지 않는다.
  • "compatibility/deprecation 결정을 코드로 구현했는가" — 답: 아니다. 계약·설계 단계. (schema/serialization 출력측은 별개로 C2 에서 locally-verified — 위 §실제 구현 참조. compatibility 축만 미구현.)
  • "운영 측정값 / cutover 인시던트 / 410 응답 실측" — 답: 모두 없다.

과장 금지 지점

  • "Stripe 방식이 API versioning의 표준이다". IETF/W3C 표준이 아니고 외부 결제 컨슈머 규모에 특화된 trade-off다. ca-tmpl은 다른 trade-off를 택한 것이지 우열을 판정한 게 아니다.
  • "Sunset 헤더만 보내면 deprecation 정책으로 충분하다". Sunset언제 신호이고 Deprecation지금 상태 신호다. 병기해야 정합.
  • "OpenAPI deprecated: true로 marker만 박으면 client가 알아서 migrate한다". schema marker는 신호일 뿐, 실제 cutover는 migration window + contract test + compatibility fixture가 함께 강제해야 한다.
  • "Protobuf reserved 시맨틱을 JSON 환경에서 동등하게 흉내낼 수 있다". OpenAPI에는 동등 시맨틱이 없고 x- extension으로 흉내내야 하는데 검증 도구 표준이 부재해 효과가 제한적이다. needs-confirmation.
  • "Jackson default가 안전하다". FAIL_ON_UNKNOWN_PROPERTIES=true는 strict이지만 FAIL_ON_NULL_FOR_PRIMITIVES=false는 lenient라 null/missing primitive가 묵시적으로 0이 된다. ca-tmpl 은 후자(입력측 deser switch)를 sibling feature-boundary-validation-mapping-contract 가 명시 override 했고 (locally-verified), 직렬화 출력측은 본 branch 가 핀했다. "default 라서 안전"이 아니라 "명시 핀 + 테스트"로 강제했다고 말해야 한다.
  • "90d/30d window를 운영에서 검증했다". 운영 배포 0건. 설계 결정의 근거는 말할 수 있지만 경험은 없다.
  • "envelope처럼 compatibility 결정도 구현했다". compatibility/deprecation 축은 계약/설계 단계, 코드 미구현. schema/serialization 축은 출력측 (datetime/BigDecimal 핀 + ArchUnit + 직렬화 테스트) 만 locally-verified 이고, D5 OpenAPI drift gate · D6 제거-field 도구 · D7 Avro · per-API money string-vs-number 코드 시연은 미구현이다. (contract baseline 축은 별개로 locally-verified)
  • "BigDecimal 을 금액 string 직렬화로 구현했다". WRITE_BIGDECIMAL_AS_PLAIN=true + new BigDecimal(double) 정적 차단은 구현했으나, 외부 API string 직렬화(@JsonSerialize(ToStringSerializer))는 sample 도메인에 money 필드가 없어 코드 시연이 없다 — per-API string-vs-number 는 문서 의무까지다.
  • "OpenAPI drift 로 schema 없는 response field 노출을 차단한다". 직렬화 출력측 핀은 했으나 response 측 "schema 없는 field 미노출"의 release-blocking 강제(D5)는 verification suite(feature-contract-verification-test-suite) 소유 planned 이다.
  • "conditional request 를 RFC 9110 대로 strong ETag 로 구현했다". If-Match 비교는 weak/lenient 다 (ETags.matchesW/·따옴표 무시). RFC 9110 의 strong comparison MUST 와는 다른 skeleton 단순화이며, production fork 에서 교체해야 한다.
  • "cursor pagination 을 운영 key 로 서명해 구현했다". CursorCodec 은 dev key factory(withDevKey())만 있고 운영 key 주입/회전은 security branch 소유 planned. 메커니즘(opaque base64url + HMAC + TTL)은 구현·검증됨.
  • "414 URI Too Long 을 end-to-end 로 처리한다". Tomcat/gateway 가 Spring dispatch 전에 거부하므로 registry row + code 만 있고 end-to-end 검증은 없다.
  • "Idempotency 를 구현했다". Idempotency-Key header 이름 수용(server-tolerant)만. key shape/replay 는 rate-limit branch 소유.
  • "OpenAPI drift 를 릴리스에서 차단한다". 이 branch 는 snapshot producer + registry mapping 정합 test 까지. release-blocking 집행은 verification-test-suite branch 소유.

관련 개념

Sources

Cluster / 묶음