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 |
|
|
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 (
/v1path prefix), pagination/sort, conditional request (ETag/If-Match/304/412), HTTP cache policy, OpenAPI producer, long-running operation, batch endpoint.feature-api-contract-baselinebranch 가 producer-소유 결정을 실제 코드(adapter-web+sample-portfolio)에 구현하고 단위/슬라이스/임베디드 테스트로 검증했다.locally-verified. - Compatibility / deprecation 축 (설계만) —
90d public + 30d internal migration window, 7행 breaking change catalog, RFC 8594Sunset+Deprecation헤더 병기, OpenAPIdeprecated: truemarker.feature-api-compatibility-deprecation-contractbranch 의 결정이며 코드 미구현 (documented-only). - Schema / serialization 축 (출력측 부분 구현) — ISO-8601 offset datetime,
BigDecimalscale 2 +HALF_UP, unknown field strict inbound, null/empty/missing 분리.feature-schema-serialization-contractbranch 의 결정이다. 직렬화 출력측 핀 (WRITE_DATES_AS_TIMESTAMPS=false/WRITE_BIGDECIMAL_AS_PLAIN=true) +new BigDecimal(double)정적 차단 ArchUnit 룰 + 직렬화 동작 테스트는 실제 코드로 구현·로컬 검증됨 (locally-verified). 단 입력측 deser switch·null/empty/missing 3-상태(Patch<T>)는 siblingfeature-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 저장소 commitb15dcf5의 실제 코드(dev.caskeleton.*package root)와 1:1 대조해 확인했다.Ground-truth 대조 (2026-06-04, ca-tmpl @5d89766 "schema/serialization 계약 직렬화 출력측 구현"): schema/serialization 축 출력측 항목 —
no_bigdecimal_double_constructorArchUnit 룰(CleanArchitectureTest),JacksonSerializationPolicyTest,application.yml/application-test.yml/.env의 직렬화 핀 두 키 — 은 commit5d89766의 실제 코드와 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 —
/v1path prefix 는 설정 주도(app-bootstrap/.../application.yml의ca-skeleton.presentation.api-base-path: ${PRESENTATION_API_BASE_PATH:/v1}) +adapter-webPresentationSettings(env 누락//누락 시 warn + 보정). 코드 자체의 default 는"", 운영 default 는/v1. - D18/D20 pagination/sort —
adapter-webPageParams(page≥0, size 1..100, deep-offset>10000 플래그),SortParam(Spring nativefield,direction파싱 + 비-네이티브 reject),shared-contractPageMeta/ResponseMeta.page. - D15 conditional request —
adapter-web/conditional/ETags(weakFromVersion=W/"<version>", lenientmatches),PreconditionFailedException. - D16 cache policy —
adapter-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 LRO —
sample-portfolioOperationsController(POST /worklogs:export→ 202 +Location+Operation,GET /operations/{id}polling),shared-contractOperation/OperationStatus,SampleOperationStore. - D8/D9/D12 transport errors —
adapter-web/error/GlobalExceptionHandler가 413(PAYLOAD_TOO_LARGE)/406(NOT_ACCEPTABLE)/415(UNSUPPORTED_MEDIA_TYPE)/405(METHOD_NOT_ALLOWED+Allowheader)/412(PRECONDITION_FAILED) 를 envelope 로 매핑. - D23 batch —
sample-portfolioWorkLogController의POST /worklogs:batchCreate(단일 tx atomic,@Size(max=1000)cap) +BatchCreateWorkLogsUseCase. - D10 OpenAPI producer —
adapter-web/build.gradle에springdoc-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.yml의spring.jackson.serialization.write-dates-as-timestamps=false(envSPRING_JACKSON_SER_WRITE_DATES_AS_TIMESTAMPS바인딩).java.time값이 epoch/배열이 아니라 ISO-8601 문자열로 직렬화됨.JavaTimeModule은 Spring Bootstarter-jsonauto-config 가 classpath 의jackson-datatype-jsr310을 자동 등록 — 명시 등록 코드는 없음. - D3 BigDecimal plain 직렬화 핀 —
application.yml의spring.jackson.generator.write-bigdecimal-as-plain=true(envSPRING_JACKSON_GEN_WRITE_BIGDECIMAL_AS_PLAIN바인딩). 지수 표기(1.23E+10) 대신 plain notation 으로 직렬화. - D3 정적 차단 ArchUnit 룰 —
app-bootstrap/.../architecture/CleanArchitectureTest의no_bigdecimal_double_constructor(@ArchTest).dev.caskeleton..production 패키지에서callConstructor(BigDecimal.class, double.class)/float.class호출을 build fail. (new BigDecimal(0.1)의 부동소수 잔차 함정 = SBMS-C3 차단) - 위반 fixture —
architecture/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 +Allowheader.WorkLogControllerWireTest— D15 ETag 발행 /If-None-Match→304 /If-Matchmismatch→412, D7/D18meta.page+ size·page 경계 400 + 빈 list[]+ deep-offsetDeprecation헤더, 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), D3Idempotency-KeyPOST surface(post_accepts_idempotency_key_header, server-tolerant).CacheControlFilterTest— D16 defaultno-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-docs200 응답 +WorkLogController반영.VersioningPrefixTest— D2/v1/probe200,/probe404 (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), ② wiredObjectMapper직렬화 동작 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 endpoint —
CursorCodec만 있고 cursor 페이징을 노출하는 sample endpoint 는 §Test Contract 미요구 (optional). - D14 PATCH
merge-patch+json차단 — content type 정책은 이 branch 가 producer 지만 ArchUnit ruleno_merge_patch_json_media_type_string와 mapper 구현은feature-boundary-validation-mapping-contractB2 소유 (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-Match에 strong comparison 을 MUST 로 규정하나(ETagsjavadoc 에 명시), 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: truemarker: 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,Deprecationheader paired usage, Google AIP-180 기반 breaking-change 분류, OpenAPIdeprecated: truemarker 의 존재. - 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 분리는 siblingfeature-boundary-validation-mapping-contract가locally-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): Protobufreserved시맨틱(field number/name 재사용 영구 차단)을 JSON 환경에서 흉내내기 위해 제거된 field 이름/번호를 catalog로 관리하고 CI에서 재사용을 검출. 두 후보 — (a) OpenAPI Specification Extensionx-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, wiredObjectMapper직렬화 테스트,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.
SunsetvsDeprecation헤더 차이 + 함께 보내는 이유 —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_constructorArchUnit 룰(callConstructor(BigDecimal.class, double.class)/float.class)로 production 패키지에서 build fail 시키고, vacuous-pass 방지 fixture 테스트까지 둔다 (locally-verified, @5d89766). HALF_UP 은 금융 round-half-up 관례와 정합. JSON number 직렬화 시 JSNumber정밀도 손실이 있어 외부/금융 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와 동일 논리).JacksonSerializationPolicyTest가ApplicationContextRunner로 wiredObjectMapper의OffsetDateTime→"...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 (PreconditionFailedException→GlobalExceptionHandler), 같은 충돌이 persistence layer 면 serialization failure 로 표현된다. ca-tmpl 은WorkLogControllerWireTest로 ETag 발행/304/412 를 검증했다 (locally-verified). - 인증된 API 의 안전한 cache default =
no-store—CacheControlFilter가 모든 응답에Cache-Control: no-store+Vary: Accept, Accept-Encoding, Authorization를 박아 proxy/CDN cache poisoning 을 막는다. cacheable endpoint 만ResponseEntity의Cache-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)를 siblingfeature-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.matches가W/·따옴표 무시). 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-Keyheader 이름 수용(server-tolerant)만. key shape/replay 는 rate-limit branch 소유. - "OpenAPI drift 를 릴리스에서 차단한다" — ❌. 이 branch 는 snapshot producer + registry mapping 정합 test 까지. release-blocking 집행은 verification-test-suite branch 소유.
관련 개념
Sources
- raw/project-notes/ca-skeleton-operational-contract §13 API Contract Surface / §16 Schema / Serialization Contract / §25 Default Decisions (API versioning) / §29 G-F (외부 근거 / 대안 조사 인덱스)
- raw/branch-notes/feature-api-contract-baseline — versioning(
/v1), pagination/sort, conditional request(ETag/If-Match/304/412 = D15), HTTP cache(no-store/Vary), OpenAPI producer, LRO, batch endpoint. Ground-truth @b15dcf5 로 대조해locally-verified확정. - raw/branch-notes/feature-api-compatibility-deprecation-contract — 90d/30d migration window, 7행 breaking change catalog, Sunset + Deprecation 헤더 병기, OpenAPI
deprecated: truemarker (documented-only) - raw/blog-topics/api-deprecation-sunset-header-migration-window-2026-07-02 — compatibility/deprecation 블로그 글감 raw seed. canonical 반영 범위: documented-only project decision + source-backed/header-role 경계 + 과장 금지 항목.
- raw/branch-notes/feature-schema-serialization-contract — ISO-8601 offset/UTC, BigDecimal scale 2 + HALF_UP, unknown field strict inbound, null/empty/missing 분리. 직렬화 출력측(datetime/BigDecimal 핀 +
no_bigdecimal_double_constructorArchUnit +JacksonSerializationPolicyTest)은 Ground-truth @5d89766 로 대조해locally-verified; D5/D6/D7 + per-API money 코드 시연은 미구현. - raw/blog-topics/spring-boot-serialization-contract-pins-2026-07-02 — Spring Boot serialization pin 블로그 글감 raw seed. canonical 반영 범위: output serialization pin + effective ObjectMapper test + BigDecimal constructor guard.
- raw/official-docs/sunset-deprecation-headers-paired-usage — IETF RFC 8594 + Deprecation draft paired 사용 권고 (Sunset 단독 금지)
- raw/official-docs/rfc9110-http-semantics — D15 conditional request(ETag/If-Match/If-None-Match/304/412), D8/D9/D12 transport error 의미론
- raw/official-docs/rfc9111-http-caching — D16
no-store/private/max-agedirective 정의 - raw/official-docs/openapi-spec-3-1-0 — D10 OpenAPI = machine-readable contract
- raw/official-docs/google-aip-185-resource-versioning — D2 major-only
/v1path versioning - raw/official-docs/spring-data-pageable-defaults — D18/D20 Pageable zero-indexed + size default +
DEFAULT_MAX_PAGE_SIZE2000 - raw/official-docs/schema-jackson-unknown-field-handling — Jackson DeserializationFeature default (직렬화/역직렬화 정책 근거)
- raw/official-docs/schema-bigdecimal-money-serialization-java — Java BigDecimal scale/HALF_UP +
new BigDecimal(double)함정 (D3 / SBMS-C1~C4) - raw/official-docs/rfc3339-datetime-utc — IETF RFC 3339 datetime UTC + "Z" suffix (D2 datetime 직렬화 normative 근거)