Files
clean-architecture-backend-…/docs/web-superpowers-package/research/source-web-deep-research.md
T

58 KiB

인바운드 HTTP API 실행 플랫폼 web 심층 리서치

본 조사에서 web은 Spring MVC 설정 묶음이나 공통 ControllerAdvice가 아니라, HTTP 요청이 신뢰 경계를 통과해 Application Use Case에 진입하고, 그 실행 결과가 HTTP 의미론으로 다시 외부에 노출되는 전 과정을 통제하는 플랫폼으로 정의하는 것이 적절합니다.

특히 플랫폼이 보장해야 할 것은 “요청을 받았다”는 사실이 아니라 다음 증거의 구분입니다.

HTTP_RECEIVED
→ REQUEST_NORMALIZED
→ ROUTE_SELECTED
→ REQUEST_BOUND
→ REQUEST_VALIDATED
→ REQUEST_ADMITTED
→ APPLICATION_STARTED
→ APPLICATION_COMMITTED
→ RESPONSE_HEADERS_COMMITTED
→ RESPONSE_PARTIALLY_WRITTEN
→ RESPONSE_WRITE_COMPLETED_LOCALLY
→ CLIENT_OBSERVATION_UNKNOWN

마지막 단계가 중요합니다. HTTP 서버가 소켓에 응답을 성공적으로 썼다는 사실만으로 클라이언트가 최종 응답을 관찰했다는 사실까지 증명할 수는 없습니다. 따라서 상태 변경의 재호출 안전성은 네트워크 응답 성공 여부가 아니라 Application Commit과 Idempotency/Reconciliation 증거에서 확보해야 합니다. RFC 9110도 멱등이 아닌 요청은 원래 요청이 적용되지 않았음을 알 수 있거나 해당 작업 자체가 멱등임을 아는 경우가 아니라면 자동 재시도를 해서는 안 된다고 규정합니다. citeturn14view0

플랫폼 기준선과 책임 경계

기술 기준선

2026년 8월 기준으로 Spring Boot 4.1.0은 Java 17 이상을 요구하고 Spring Framework 7.0.8 이상을 사용하며, Servlet 컨테이너 기준으로 Tomcat 11.0.x와 Jetty 12.1.x를 지원합니다. 따라서 Backend Skeleton의 Java 21 + Spring Boot 4.1 BOM 기준은 타당합니다. Java 21은 Boot 최소 요구사항보다 높은 플랫폼 정책으로 두는 것이 좋습니다. citeturn2search3

Spring Boot는 Servlet 기반 Spring MVC와 Reactive Spring WebFlux를 모두 지원합니다. spring-boot-starter-webspring-boot-starter-webflux가 함께 존재하면 Boot는 기본적으로 MVC 애플리케이션으로 구성하므로, “둘 다 의존성에 넣고 런타임에서 알아서 고른다”는 구조는 피해야 합니다. citeturn3search0

권장 기준선은 다음과 같습니다.

영역 Stable 기준 판단
Java 21 플랫폼 최소선
Spring Boot 4.1.0 BOM 전체 Spring 버전의 Source of Truth
Spring Framework Boot 관리 7.0.x 별도 버전 override 금지
MVC 기본 Stable 일반 업무 API
MVC Server Tomcat 우선, Jetty 호환 Lane 둘 다 실제 서버 시험
WebFlux 별도 Stable 선택 Profile 완전 Reactive 서비스 중심
WebFlux Server Reactor Netty 우선 별도 Netty 통합시험
Virtual Thread MVC Advanced Java 21+, 부하 시험 통과 후 서비스별 사용
HTTP Semantics RFC 9110·9111 Status, Method, Conditional, Cache
Error RFC 9457 ProblemDetail 기반
OpenAPI Stable 3.1.2 권고 3.2 tooling maturity 때문
OpenAPI 3.2 Experimental compatibility lane Streaming 표현 검증
테스트 Mock + 실제 Server + Nginx Mock-only Stable 선언 금지

Spring Boot는 Java 21 이상에서 spring.threads.virtual.enabled=true를 사용해 virtual-thread 기반 task execution을 구성할 수 있습니다. 다만 MVC의 blocking 모델을 virtual thread로 바꾼다고 데이터베이스 풀, 외부 API 동시성, 메모리, admission 한계까지 사라지는 것은 아니므로 별도 Profile로 취급하는 편이 안전합니다. citeturn2search1

MVC 비동기 응답에 대해서도 주의가 필요합니다. Spring MVC는 Callable, DeferredResult, WebAsyncTask, SseEmitter, reactive return type 등을 지원하지만 Servlet response write 자체는 blocking이며, Spring 문서는 streaming write용 기본 AsyncTaskExecutor가 부하 환경에 적합하지 않다고 명시합니다. citeturn1search0

따라서 실행 모델은 다음처럼 선언하는 것이 가장 명확합니다.

PLATFORM_THREAD_MVC
    → W1 기본

VIRTUAL_THREAD_MVC
    → W3 Advanced
    → blocking dependency가 많은 서비스의 선택지

REACTIVE_WEBFLUX
    → W3 Stable 선택
    → reactive DB / reactive HTTP / streaming chain

WEBFLUX + blocking JPA
    → Event Loop 직접 호출 금지
    → 명시적 blocking-offload profile 없이는 금지

WebFlux 자체는 non-blocking I/O와 Reactive Streams backpressure를 중심으로 설계되어 있습니다. Framework 7은 blocking controller execution을 별도 executor로 넘기는 기능도 제공하므로 “WebFlux에서 JPA는 절대 기술적으로 불가능하다”기보다는 Stable WebFlux Profile에서는 event-loop blocking을 금지하고, blocking bridge를 별도 Advanced 기능으로 취급하는 것이 더 정확합니다. citeturn1search1turn8search1

기존 모듈과의 경계

web이 소유해야 하는 것은 HTTP 표현과 인바운드 실행 정책입니다. 트랜잭션, 저장소, 메시지 durability, 인증 원천 등을 흡수하면 다시 거대한 공통 모듈이 됩니다.

연계 모듈 web 소유 상대 모듈 소유
security 인증 결과를 Actor/Tenant HTTP Context로 연결, CORS·CSRF integration Token 검증, 세션, Role·Permission 원천
httpclient inbound request outbound HTTP, retry, circuit breaker
graphql HTTP endpoint 입구·공통 security integration GraphQL parsing, schema, resolver, GraphQL error
grpc 일반 HTTP resource API Protobuf RPC
websocket Upgrade까지의 HTTP security/context connection/session/message protocol
fileserver metadata·ticket·reference binary upload/download, Range
jpa·mongodb Use Case 호출 transaction, query, lock
redis rate limit/idempotency SPI 호출 atomic counter, TTL, failure semantics
messaging 202, Operation/Command 접수 표현 durable enqueue, ACK, retry, replay, DLQ
notification notification command 접수 HTTP 표현 실제 delivery lifecycle

특히 Controller → JpaRepository, Controller → MongoTemplate, Controller → WebClient retry, Controller → Kafka publish/ACK, Controller → MinIO SDK, Controller-level 업무 @Transactional은 기본 금지 대상으로 두는 것이 좋습니다. Spring Security 역시 service layer에 method security를 적용할 수 있으므로 Route authorization과 실제 Use Case·Object authorization을 분리할 수 있습니다. citeturn9search3

권장 호출 구조는 다음과 같습니다.

HTTP
 ↓
Web Edge Policy
 ↓
Controller Adapter
 ↓
Request DTO
 ↓
Application Command / Query
 ↓
Application Use Case
 ↓
JPA / MongoDB / HTTP Client / Messaging / Object Storage
 ↓
Application Result + Commit Evidence
 ↓
Response DTO / Operation Resource
 ↓
HTTP Status + Header + Representation

공개 계층과 모듈 구조

기능 등급은 제안된 W1~W4가 적절합니다.

계층 기본 공개 대상 기능
W1 Standard HTTP API 모든 서비스 Controller, DTO, validation, Problem Details, URI/status/header, pagination, versioning, OpenAPI
W2 Advanced HTTP API 상태 변경·대용량 목록·stream Idempotency, conditional mutation, async operation, SSE/NDJSON, admission
W3 Transport Extension 특수 workload WebFlux, virtual threads, functional endpoint, container-specific tuning
W4 Contract/Admin Plane 플랫폼·운영 OpenAPI diff, route inventory, deprecation usage, admin/debug endpoints

모듈 구조도 제안한 방향이 적합하되, Redis/JPA와 직접 결합하는 구현은 web-idempotency 내부에 넣지 않는 것을 권고합니다.

modules/web/
├── web-core-api
├── web-contract
├── web-validation
├── web-error
├── web-pagination
├── web-idempotency          // SPI + HTTP policy
├── web-versioning
├── web-security-integration
├── web-observability
├── web-openapi
├── web-mvc
├── web-webflux
├── web-streaming
├── web-admin
├── web-spring-boot-starter-mvc
├── web-spring-boot-starter-webflux
├── web-testkit-core
├── web-testkit-mvc
├── web-testkit-webflux
└── web-testkit-contract

의존성 방향은 다음처럼 제한하는 것이 좋습니다.

web-core-api
      ↑
web-contract / error / pagination / versioning / idempotency
      ↑
 ┌───────────────┬────────────────┐
web-mvc       web-webflux     web-openapi
 ↑                 ↑
starter-mvc     starter-webflux

web-core-api에는 HttpServletRequest, ServerWebExchange, Reactor Mono/Flux를 넣지 않습니다. web-idempotencyIdempotencyStore 같은 capability interface만 정의하고 JPA/Redis adapter는 integration 계층에서 제공합니다. 그래야 Web → Persistence 역결합이 생기지 않습니다.

HTTP 계약과 외부 API 진화 규칙

URI와 Method

기본 URI 형식은 다음이 적절합니다.

/api/v1/documents/{documentId}

ID는 외부 식별자이며 DB PK, 파일 경로, 내부 저장 형태를 보장하지 않아야 합니다.

Spring Framework 7에서는 과거 MVC의 암묵적 trailing-slash matching이 제거됐고 별도의 URL normalization 기능을 사용하도록 방향이 바뀌었습니다. 따라서 /documents/documents/를 우연히 동일하게 취급하지 말고 canonical URI를 하나로 정해 redirect 또는 reject 정책을 명시해야 합니다. citeturn10search0

권장 URI 규칙은 다음과 같습니다.

항목 Stable 정책
Case path는 case-sensitive
Trailing slash canonical form 하나만
Duplicate slash 자동 합치기보다 reject/edge normalize
%2F encoded slash ID 내부에서 기본 금지
Matrix parameter 기본 비지원
Query order 의미 없음. Cursor 등 일부 opaque value만 예외
Identifier opaque string
DB column/field name 노출 금지
external redirect target raw user URL 사용 금지

Method는 HTTP 정의에 맞춰 사용해야 합니다. RFC 9110에서 GET·HEAD 같은 safe method와 PUT·DELETE 같은 idempotent method는 다른 개념이며, idempotent method라고 해도 구현이 비멱등 부수효과를 추가하면 실제 재호출 안전성은 깨집니다. citeturn14view0

Method 플랫폼 용도 등급
GET resource/query Stable
HEAD GET metadata Stable
POST create/command Stable, mutation idempotency profile 필요
PUT 전체 교체/create-at-known-URI Stable
PATCH 부분 갱신 Stable typed DTO, RFC patch formats Advanced
DELETE 삭제 Stable
OPTIONS CORS/Allow Stable
TRACE 일반 API 차단 비지원
CONNECT 일반 API 차단 비지원
custom method 별도 승인 Experimental

PATCH 정책

PATCH 자체는 RFC 5789가 정의하고 있으며, PATCH는 기본적으로 safe도 idempotent도 아닙니다. RFC는 concurrent patch 충돌 위험 때문에 조건부 요청 사용을 권고합니다. citeturn17search1

JSON Patch는 RFC 6902의 application/json-patch+json, JSON Merge Patch는 RFC 7396이 현재 규격이며 RFC 7386을 명시적으로 obsolete합니다. 따라서 신규 문서에서 Merge Patch 기준을 RFC 7386으로 고정하지 말고 RFC 7396으로 업데이트해야 합니다. citeturn17search0turn21view0

권장 등급은 다음과 같습니다.

방식 장점 주요 위험 권고
Typed Update DTO validation·권한·OpenAPI 명확 DTO 증가 Stable 기본
JSON Merge Patch null/delete 표현 간결 object/array 세밀 제어 약함 Advanced
JSON Patch add/remove/replace/test 등 정밀 pointer 권한, 순서, 배열 복잡성 Advanced
Map<String,Object> 구현 간단 mass assignment, validation·schema 붕괴 비지원

PATCH mutation은 가능한 한 If-Match와 함께 사용해야 합니다.

Status와 Header 계약

RFC 9110은 HTTP 상태와 조건부 요청의 핵심 의미를 정의합니다. 201은 새 resource 생성을 나타내며 생성된 primary resource URI를 Location으로 반환하는 것이 일반적인 계약입니다. 202는 요청이 처리되도록 받아들여졌지만 처리가 완료되지 않았다는 뜻이고, 204와 304에는 response content가 없습니다. citeturn14view1turn15view0turn15view1turn15view2

권장 성공 계약은 다음과 같습니다.

상황 Status Header/Body
Resource 조회 200 DTO + ETag 가능
Resource 생성 201 Location + representation 권장
비동기 durable 작업 접수 202 Location: /operations/{id}
성공, 반환 representation 없음 204 Body 절대 없음
Conditional GET not modified 304 Body 없음
Range response 206 일반 JSON API가 아닌 fileserver profile 중심

오류·조건 상태는 다음처럼 고정하는 것이 좋습니다.

상황 Status
malformed JSON·잘못된 scalar 형식 400
인증 없음/무효 401
인증됐으나 권한 없음 403
존재 은닉 보안 정책 404
resource 없음 404
method 불허 405
Accept 불지원 406
영구 삭제된 resource를 의도적으로 모델링 410
body limit 초과 413
media type 불지원 415
syntactically valid 후 transport semantic validation 실패 422
business state conflict 409
HTTP precondition 불충족 412
rate/quota 초과 429
global admission/capacity 불가 503
gateway/upstream 잘못된 응답 502
gateway/upstream timeout 504
예상하지 못한 내부 실패 500

특히 400 vs 422는 프로젝트마다 흔히 흔들리는 영역입니다. RFC 9110의 422는 Content-Type과 syntax 자체는 이해했지만 포함된 instructions를 처리할 수 없는 경우를 뜻하며, RFC 9457도 validation error 예시에 422를 사용합니다. 따라서 JSON parse/type binding 실패는 400, 정상 parse 후 Bean/transport validation 실패는 422로 고정하는 것이 일관적입니다. citeturn14view2turn20view0

409 vs 412는 더 명확하게 구분해야 합니다.

If-Match / If-Unmodified-Since 등 HTTP precondition 실패
→ 412 Precondition Failed

HTTP conditional header와 무관한 도메인 상태 충돌
→ 409 Conflict

RFC 9110의 If-Match는 특히 state-changing method에서 lost update 방지를 위해 사용되며 조건이 false이면 412가 핵심 응답입니다. citeturn14view3

핵심 Header 정책은 다음과 같습니다.

Header 정책
Location 201 resource, 202 operation
Content-Location 반환 representation의 식별 위치가 필요한 경우만
ETag cache/concurrency validator
If-Match mutation optimistic concurrency
If-None-Match GET cache, create-only
Last-Modified 시간 기반 validator가 충분한 resource
Cache-Control 모든 민감·cacheable endpoint 정책 명시
Vary representation이 실제로 달라지는 request header만
Retry-After 429·503/temporary admission profile
Deprecation deprecated API
Sunset endpoint 종료 예정
Link deprecation docs, operation 관계 등
Allow 405/OPTIONS
Content-Language localized representation
Content-Type 명시

Request Binding·Codec·Validation

요청은 다음 세 계층으로 분리해야 합니다.

HTTP Parsing
→ URI/Header/JSON syntax와 scalar conversion

Transport Validation
→ DTO 길이·범위·형식·개수·cross-field

Application Validation
→ 존재 여부·업무 상태·권한·도메인 invariant

OWASP API Security Top 10은 object-level authorization, object-property-level authorization 및 resource consumption을 핵심 API 위험으로 다룹니다. 따라서 Entity 직접 binding과 unrestricted field update는 Web Platform 수준에서 차단하는 편이 적절합니다. citeturn13search1turn13search0turn13search2

기본 JSON Request Profile은 다음을 권고합니다.

입력 특성 권장 정책
Unknown property mutation request에서 기본 reject
Duplicate JSON key reject
trailing token reject
unknown enum reject
enum case case-sensitive
polymorphic deserialization allowlist discriminator 없이는 금지
empty string → null coercion 기본 금지
number → string coercion 기본 금지
string → number coercion 명시적 converter 없이는 금지
JSON depth hard limit
array elements hard limit
string bytes transport absolute limit + field validation
collection element @Valid/element constraint
nested object nested validation
cross-field DTO-level validator
DB lookup transport validator에서 금지

@RequestBody UserEntity 같은 binding은 mass assignment와 persistence representation 노출을 동시에 일으키므로 다음처럼 request-specific DTO를 사용해야 합니다.

CreateUserRequest
UpdateUserRequest
UserResponse

Wire type도 중앙 Manifest로 고정합니다.

Java 의미 HTTP JSON 표현 권고
Instant RFC 3339/ISO-8601 UTC Z 문자열
OffsetDateTime offset 포함 ISO 문자열
LocalDate YYYY-MM-DD
Duration ISO-8601 duration
UUID canonical string
BigDecimal schema에 scale/precision 명시
long JS safe range 초과 가능 시 string wire profile
Enum Java enum name과 분리 가능한 stable wire name
URI string + URI format
Locale BCP 47 language tag

Response와 Problem Details

모든 성공 response를 ApiResponse<T>에 넣는 방식보다는 다음을 권장합니다.

성공
→ Resource/Collection/Operation DTO

오류
→ RFC 9457 Problem Details

RFC 9457은 application/problem+json, type, title, status, detail, instance와 problem-specific extension을 정의하며, 클라이언트는 알 수 없는 extension을 무시해야 합니다. citeturn20view0turn20view2

Spring Framework 7의 MVC와 WebFlux는 ProblemDetail, ErrorResponse, ErrorResponseException 및 MVC의 ResponseEntityExceptionHandler 등 RFC 9457 지원을 제공합니다. ProblemDetail.status는 실제 응답 상태 결정에도 사용되고, Spring은 Problem Detail에 application/problem+json을 선호되는 representation으로 제공합니다. citeturn20view3turn8search3

권장 Problem Catalog는 다음과 같습니다.

code 기본 status
MALFORMED_REQUEST 400
BINDING_FAILED 400
VALIDATION_FAILED 422
AUTHENTICATION_REQUIRED 401
ACCESS_DENIED 403
RESOURCE_NOT_FOUND 404
RESOURCE_CONFLICT 409
PRECONDITION_FAILED 412
IDEMPOTENCY_KEY_REQUIRED 400
IDEMPOTENCY_KEY_REUSED 422
IDEMPOTENCY_REQUEST_IN_PROGRESS 409
RATE_LIMITED 429
ADMISSION_REJECTED 503
DEPENDENCY_FAILURE 502/503
DEPENDENCY_TIMEOUT 504
INTERNAL_ERROR 500

Idempotency의 422/409 구분은 만료된 IETF draft이기는 하지만 상호운용성 참고 가치가 있습니다. 해당 draft는 동일 key에 다른 payload를 재사용하면 422, 원 요청이 아직 진행 중인 상태에서 같은 key가 오면 409를 제안합니다. citeturn22view0turn22view1

Problem payload는 다음처럼 제한합니다.

{
  "type": "https://hyeonworks.com/problems/validation",
  "title": "Invalid request",
  "status": 422,
  "code": "VALIDATION_FAILED",
  "instance": "/problems/01J...",
  "traceId": "...",
  "errors": [
    {
      "pointer": "/title",
      "code": "SIZE",
      "message": "..."
    }
  ]
}

RFC 9457은 Problem Details를 내부 디버깅 도구로 사용하지 말라고 경고하며 stack dump나 구현 세부사항을 노출하지 않아야 한다고 명시합니다. 또한 body의 status와 실제 HTTP status가 불일치할 수 있는 위험을 특별히 지적합니다. 따라서 stack trace, SQL, Mongo query, host, token, provider raw error, PII를 Problem Detail에 넣지 않고 실제 status/body status 일치 contract test를 필수화해야 합니다. citeturn20view1

API Versioning·Deprecation

Spring Framework 7에는 MVC와 WebFlux 모두 API versioning 기능이 있으며 version을 request header, query parameter, path segment, media type parameter에서 선택할 수 있습니다. 또한 deprecation handler는 표준 Deprecation, Sunset, Link 응답을 지원합니다. citeturn8search0turn8search1

Backend Skeleton 기본은 다음을 권고합니다.

Major version
→ path
→ /api/v1

Minor/Patch
→ URL version 증가 없음
→ additive compatible evolution

Path version이 좋은 이유는 Gateway, Nginx, OpenAPI snapshot, traffic inventory, access log에서 버전이 명시적으로 보이기 때문입니다. Header version은 내부 API처럼 URL 안정성이 특히 중요한 경우의 선택 Profile로 두는 것이 좋습니다.

Deprecation은 2025년 RFC 9745로 표준화되었으며 deprecation date를 전달하고 관련 documentation을 Link로 연결할 수 있습니다. Sunset은 RFC 8594가 resource가 향후 이용 불가능해질 예상 시점을 알리는 header로 정의합니다. citeturn16search5turn16search1

따라서 API 제거 조건은 단순 날짜가 아니라 다음 gate를 모두 통과해야 합니다.

OpenAPI breaking diff
+ Deprecated route usage = 허용 기준 이하
+ Sunset 기간 경과
+ Client owner 확인
+ consumer contract 통과
+ rollback 가능

Collection·Concurrency·Idempotency·비동기 작업

Pagination·Filter·Sort·Projection

목록 API는 DB pagination API를 HTTP에 그대로 노출하면 안 됩니다.

방식 적합한 용도 기본 등급
Page 관리자 화면, total count 필요 Stable 제한형
Slice 일반 목록, 다음 페이지 존재 여부 Stable
Keyset Cursor 대규모·시간순·높은 insert rate Stable 권장
raw offset/limit 작은 데이터 제한적

Cursor는 opaque token이어야 하며 최소 다음 의미를 포함하거나 서버 쪽 상태로 참조해야 합니다.

cursorVersion
queryProfile
sortValues
uniqueTieBreaker
filterFingerprint
issuedAt
integrity MAC

예를 들어 동일 createdAt 값을 가진 행이 여러 개 있을 수 있으므로 정렬은 다음처럼 반드시 total order가 되어야 합니다.

ORDER BY createdAt DESC, documentId DESC

API가 직접 허용할 query vocabulary를 관리해야 합니다.

SortFieldCatalog
FilterFieldCatalog
FilterOperatorCatalog
ProjectionProfile
IncludeProfile

따라서 다음 API는 금지합니다.

?sort=${databaseColumn}
?filter=${JPQL}
?filter=${Mongo BSON}
?include=*
?limit=2147483647

초기 플랫폼 profile로는 예를 들어 defaultLimit=50, hardMaxLimit=200 정도에서 시작하되 서비스별 performance test로 조정하는 방식을 권고합니다. 숫자 자체보다 중요한 것은 hard maximum이 존재하고 API 계약에 포함되는 것입니다. OWASP도 records per page, execution timeout, upload size와 같은 resource limit이 API resource-consumption 방어의 일부라고 명시합니다. citeturn13search2

Conditional Request와 Optimistic Concurrency

HTTP validator는 application version과 연결할 수 있지만 HTTP와 DB를 동일 개념으로 만들 필요는 없습니다.

DB version / aggregate version
        ↓
ETag representation
        ↓
If-Match
        ↓
Application expectedVersion

Strong ETag를 mutation concurrency token으로 사용하는 것을 권장합니다.

GET /api/v1/documents/d1
ETag: "v17"

PATCH /api/v1/documents/d1
If-Match: "v17"

version이 이미 v18이면:

412 Precondition Failed

If-Match는 strong comparison을 사용하며 state-changing method에서 lost-update 방지를 위한 대표적인 용도로 정의됩니다. citeturn14view3

Create-only PUT도 다음처럼 표현할 수 있습니다.

PUT /api/v1/documents/client-generated-id
If-None-Match: *

Conditional request의 목적과 Idempotency는 구분해야 합니다.

If-Match
→ "내가 읽은 버전이 아직 최신인가?"

Idempotency-Key
→ "이 업무 명령을 이미 실행했는가?"

둘은 대체 관계가 아니라 동시에 필요한 경우가 많습니다.

Idempotency와 완료 불명확성

2026년 8월 현재 Idempotency-Key는 확정된 RFC가 아닙니다. 최신 공개된 draft-ietf-httpapi-idempotency-key-header-07은 2025년 10월 15일 발행됐고 2026년 4월 18일 만료되었습니다. 따라서 Idempotency-Key라는 이름은 충분히 실용적인 compatibility profile이지만 “IETF HTTP 표준”이라고 문서화하면 안 됩니다. citeturn22view3turn21view1

다만 draft가 정의한 핵심 모델은 플랫폼 설계에 유용합니다. client key와 server-generated fingerprint를 결합하고, 같은 key의 완료된 요청은 원래 결과를 replay하며, 다른 payload로 같은 key를 재사용하지 않는 모델입니다. citeturn22view2

권장 scope:

tenantId
+ actor/client identity
+ operationId or route contract
+ idempotencyKey

저장 정보:

key scope
request fingerprint
processing state
application result identity
response status
response header allowlist
response body or durable result reference
createdAt
completedAt
expiresAt

상태 모델은 사용자 제안보다 한 단계 더 세밀하게 두는 것이 좋습니다.

상태 의미
ABSENT key 없음
PROCESSING 실행 소유권 확보
APPLICATION_COMMITTED 업무 commit 증거 있음
COMPLETED_REPLAYABLE HTTP 결과 replay 가능
FAILED_RETRYABLE 업무 commit 없다고 증명 가능
COMPLETION_UNKNOWN commit 여부 자체가 불명확
EXPIRED replay guarantee 기간 종료

여기서 가장 중요한 구현 규칙이 있습니다.

Redis의 Idempotency record와 JPA business transaction이 서로 다른 atomic resource이면 Redis의 COMPLETED 플래그만으로 DB commit을 증명해서는 안 됩니다.

예를 들어:

DB COMMIT 성공
→ 프로세스 crash
→ Redis COMPLETED 기록 실패

가 발생하면 재시도가 mutation을 또 실행할 수 있습니다.

따라서 DB-local mutation이라면 다음이 가장 강합니다.

Business mutation
+ Idempotency execution record
= 같은 DB transaction에서 commit

Redis는 빠른 concurrent-request exclusion이나 read-through replay cache에 사용할 수 있지만, DB transaction의 유일한 commit evidence로 삼으려면 별도의 transactional protocol이 필요합니다. 외부 메시지나 다른 저장소까지 걸친 작업이라면 operation/outbox/reconciliation을 사용해야 합니다.

요청 실행 증거와 Retry 판정

HTTP 플랫폼에서 핵심 질문에 답하기 위해 다음 evidence model을 명시적으로 두는 것을 권고합니다.

Evidence Application 진입 Commit 클라이언트 Retry
REQUEST_REJECTED 아니오 아니오 수정 또는 정책에 따라
REQUEST_NOT_EXECUTED 아니오라고 증명 아니오 안전
APPLICATION_STARTED 불명 멱등 증거 없으면 자동 재시도 금지
APPLICATION_ROLLED_BACK 아니오라고 증명 정책에 따라 가능
APPLICATION_COMMITTED 동일 command 재실행 금지, replay/reconcile
RESPONSE_HEADERS_COMMITTED 보통 별도 evidence 필요 HTTP status만 보고 판정 금지
PARTIAL_RESPONSE_DELIVERED 별도 판단 stream resume contract 필요
CLIENT_COMPLETION_UNKNOWN 예 가능 예 가능 idempotency/reconciliation 필요

특히 다음 시나리오는 플랫폼의 필수 fault test가 되어야 합니다.

POST
 ↓
Application Transaction COMMIT
 ↓
HTTP response write 시작
 ↓
TCP reset
 ↓
Client sees IOException

이 상황에서 서버가 “client가 받지 못했으므로 rollback”할 방법은 없습니다. 따라서 client 재시도를 안전하게 만드는 수단은 다음 중 하나입니다.

Idempotency Key
Client-generated resource ID
Conditional Create
Durable Operation Resource
Reconciliation GET

RFC 9110에는 If-Match 실패 시 서버가 해당 변경이 이미 이전 요청에서 성공했음을 검증할 수 있는 특정 경우 기존 성공을 인지할 수 있도록 하는 의미론도 있어, “응답 유실 후 재호출”이라는 문제가 HTTP 자체에서도 중요한 고려 대상임을 보여줍니다. citeturn14view3

202 Accepted와 Operation Resource

RFC 9110의 202는 processing이 완료되지 않았고 최종적으로 실행되지 않을 수도 있음을 뜻하며, HTTP 자체에는 나중에 비동기 결과를 다시 “push”하는 표준 기능이 없다고 명시합니다. citeturn15view0

따라서 다음 계약이 적절합니다.

POST /api/v1/exports
→ 202 Accepted
Location: /api/v1/operations/op_123
Retry-After: 3
GET /api/v1/operations/op_123

PENDING
→ RUNNING
→ SUCCEEDED
       ↘ FAILED
       ↘ CANCELED

Operation DTO:

operationId
status
createdAt
startedAt
completedAt
progress
resultLocation
problem
retryAfter
expiresAt

202는 “비동기 thread를 시작했다”는 의미가 아니라 “비동기 작업을 추적할 수 있는 방식으로 접수했다”는 플랫폼 계약으로 강화하는 것이 좋습니다.

따라서:

Controller
→ @Async 호출
→ 202

는 Stable 구현으로 인정하지 않습니다. Spring의 async execution은 TaskExecutor 기반 process-local execution abstraction이므로 durable queue나 crash recovery를 의미하지 않습니다. citeturn18search24

Stable 202 조건은 다음입니다.

Durable command/operation 저장 성공
또는
Business DB transaction + transactional outbox 성공

그 이후에만
→ 202

HTTP Cache

RFC 9111은 HTTP cache freshness, validation, Cache-Control, Vary, authenticated response caching, unsafe method 이후 invalidation을 정의합니다. unsafe method의 성공 응답은 통과한 cache에서 target URI를 무효화하지만 관련 모든 resource가 전역적으로 자동 무효화된다는 보장은 없습니다. citeturn19view1

권장 기본 정책:

응답 종류 정책
개인정보·민감한 업무 상태 private, no-store
public immutable resource public, max-age=..., immutable
public mutable resource ETag + freshness/conditional GET
사용자별이지만 browser private cache 허용 private, max-age=... 명시
인증 요청을 shared cache에 저장 명시적 검토 없이는 금지

RFC 9111은 Authorization이 있는 요청에 대한 응답을 shared cache가 재사용하려면 이를 허용하는 explicit cache directive가 필요하다고 규정합니다. citeturn19view0

no-cacheno-store는 구분해야 합니다.

no-cache
→ 저장 자체를 금지하지 않음
→ 재사용 전에 validation 요구

no-store
→ request/response를 cache에 저장하지 말라는 의미

RFC 9111이 이를 각각 별도 의미로 정의합니다. citeturn19view2

Vary는 response representation이 실제 어떤 request header에 따라 달라졌는지 cache key에 반영하는 수단입니다. Accept, Accept-Language, version header를 사용한다면 해당 차이가 실제 representation을 바꾸는 경우에만 추가해야 합니다. citeturn19view3

실행 스택·Streaming·보안·Proxy

MVC와 WebFlux Streaming

Streaming은 성공 envelope나 일반 ProblemDetail과 다른 계약이 필요합니다.

방식 MVC WebFlux 등급
Async single response Callable, DeferredResult, WebAsyncTask Mono Stable
SSE SseEmitter Flux<ServerSentEvent<?>> Advanced
NDJSON emitter/streaming writer Flux<T> Advanced
JSON Sequence custom writer reactive writer Advanced
streamed giant JSON array 가능 가능 기본 비권장
Bidirectional WebSocket module WebSocket module Web 범위 밖

Spring MVC는 remote client disconnect를 항상 즉시 callback으로 알려주는 Servlet API가 없기 때문에 streaming response에서는 heartbeat/주기적 write를 통해 disconnect를 감지해야 합니다. 또한 MVC streaming write는 blocking thread를 사용합니다. citeturn1search0

응답이 아직 commit되지 않은 시점의 오류는 일반 ProblemDetail로 바꿀 수 있습니다.

Controller 실행
→ 오류
→ headers 미전송
→ 4xx/5xx + application/problem+json

그러나 headers와 일부 stream frame이 이미 전송된 뒤라면 HTTP status를 200에서 500으로 바꿀 수 없습니다.

HTTP/1.1 200 OK
Content-Type: text/event-stream

event: item
...

[application error]

이 경우 계약은 transport별로 달라야 합니다.

Stream Commit 후 오류 표현
SSE typed event: error 전송 후 close, 가능할 때
NDJSON terminal error record profile 또는 abrupt EOF
JSON Sequence typed error record/terminal marker
plain streamed JSON truncation을 정상 완료와 구분하기 어려워 장기 stream에서 비권장

따라서 모든 stream protocol에는 정상 종료 marker와 비정상 EOF의 차이를 정의해야 합니다.

Replay가 필요하면 Web이 자체 이벤트 DB를 만들지 않습니다.

SSE Last-Event-ID
        ↓
Web adapter
        ↓
Messaging/Event Log resume cursor

SSE의 Last-Event-ID는 resume hint일 뿐이며 durable history가 자동으로 생기는 것은 아닙니다.

CORS·CSRF·Authorization

Spring Security는 CORS가 Security보다 먼저 처리되어야 한다고 명시합니다. 브라우저의 preflight request에는 일반적인 session cookie가 포함되지 않을 수 있기 때문입니다. citeturn9search0turn9search2

Production CORS는 다음처럼 allowlist profile로 구성해야 합니다.

allowed origins     → exact allowlist
allowed methods     → API별 allowlist
allowed headers     → explicit set
exposed headers     → 필요한 응답 Header만
credentials         → 명시적으로 필요한 API만
preflight max-age   → 정책값

다음은 금지합니다.

Origin reflection
* + credentials
production 전체 경로 unrestricted CORS

CSRF는 “REST이므로 무조건 disable”이 아니라 credential transport에 따라 결정해야 합니다. Spring Security는 browser-based application에 CSRF protection을 제공하며 cookie/session repository 등 다양한 방식을 지원합니다. citeturn9search1

권장 분류:

Authentication CSRF
Session Cookie 필수
BFF cookie 필수
Browser cookie + bearer 혼합 cookie-authenticated mutation 보호
Authorization Header only, cookie credential 없음 threat model 검토 후 disable 가능
non-browser service-to-service bearer 일반적으로 CSRF 대상 아님

Authorization은 다음 순서로 분리합니다.

Authentication
→ Route/Function Permission
→ Application Use Case Permission
→ Object Authorization
→ Property Authorization
→ Tenant Isolation

OWASP는 object-level authorization과 object-property-level authorization을 별개의 주요 API 위험으로 분류하기 때문에 @PreAuthorize("hasRole('EDITOR')")가 통과했다고 특정 documentId 수정 권한까지 증명된 것으로 취급하면 안 됩니다. citeturn13search1turn13search0

Forwarded Header와 Nginx

Spring의 ForwardedHeaderFilterForwardedX-Forwarded-* 정보를 바탕으로 scheme, host, port 등을 외부 요청 기준으로 변환할 수 있지만, Spring 공식 문서는 application이 header가 악의적 client에서 왔는지 trusted proxy에서 왔는지 자체적으로 알 수 없기 때문에 신뢰 경계의 proxy가 외부 Forwarded header를 제거하고 자신이 설정해야 한다고 명시합니다. citeturn10search0

따라서 Host Nginx 구조는 다음처럼 고정하는 것이 좋습니다.

Internet Client
    ↓
Nginx
    ├─ incoming Forwarded / X-Forwarded-* 제거
    ├─ authoritative client address 계산
    ├─ Forwarded 또는 X-Forwarded-* 재설정
    └─ trusted internal connection
          ↓
Spring Boot
    ↓
한 가지 Forwarded processing strategy만 적용

Spring Boot는 forwarded header 처리를 위한 전략을 제공하므로 NONE, container-native 방식, Spring Framework 기반 방식을 topology에 맞게 하나만 선택해야 합니다. citeturn12search0turn12search1turn12search17

동현님이 언급한 /api, /dev-api, X-Forwarded-Prefix까지 고려하면 **trusted Nginx 환경에서는 Framework-based normalization을 우선 검증하고, direct-access profile에서는 NONE**을 두는 접근이 좋습니다.

외부 URL 계산도 다음처럼 해야 합니다.

raw Host
raw X-Forwarded-Host
      ↓ 직접 사용 금지

trusted proxy normalization
      ↓
NormalizedExternalRequestContext
      ↓
Location / redirect / absolute Link

고위험 URL, 예를 들어 password reset이나 callback 등은 가능하면 request host를 재조립하지 말고 configured external origin을 사용하는 편이 더 안전합니다.

시험해야 할 공격:

Host injection
X-Forwarded-Host spoof
X-Forwarded-For spoof
scheme spoof
port spoof
prefix duplication
encoded path confusion
open redirect

Resource Budget·Rate Limit·Admission

Web Platform은 다음 resource를 무한대로 허용해서는 안 됩니다. OWASP도 CPU, memory, bandwidth, upload size, request rate, records per page, third-party cost 제한 부족을 API resource consumption 위험으로 분류합니다. citeturn13search2

플랫폼 초기 baseline 예시는 다음 정도가 합리적인 출발점입니다. 이는 RFC 기본값이 아니라 조직 정책값이며 Nginx·Tomcat·Jetty·Netty 및 실제 payload 통계로 조정해야 합니다.

Budget Standard 초기값 예
URI 8 KiB
전체 request headers 16 KiB
query parameter count 100
standard JSON body 1 MiB
large JSON profile 8 MiB 이하 별도 승인
JSON nesting depth 64
generic array elements 1,000
multipart part count 20
sync request hard duration 30 s 이하, route deadline은 더 짧게
standard response 수 MiB 내
streaming buffer bounded, connection별 별도 profile

Nginx limit보다 애플리케이션이 훨씬 큰 값을 갖거나 반대가 되면 어느 계층에서 413/timeout이 발생하는지 예측할 수 없으므로 다음 값을 함께 관리해야 합니다.

Nginx
  body/header/timeout/idle

Spring Boot server
  header/body/form limits

JSON codec
  depth/string/number

Application
  collection/filter/page limits

Rate limit은 Web에서 HTTP 표현을, Redis 등이 원자적 quota capability를 담당합니다.

RateLimiter capability
→ decision
→ Web mapping
→ 429 + ProblemDetail + Retry-After

2026년 8월 현재 새 RateLimit/RateLimit-Policy field 사양도 아직 RFC가 아니라 2026년 5월 23일 발행된 Internet-Draft -11 상태이며 2026년 11월 24일 만료 예정입니다. 따라서 429와 Retry-After는 Stable, 새 RateLimit fields는 호환/Experimental profile로 두는 것이 안전합니다. citeturn21view2

Admission은 rate limit과 별개입니다.

Rate Limit
→ 일정 기간 사용량 제어

Admission
→ 지금 실행할 capacity가 있는가?

권장 구분:

상황 응답
특정 Actor/IP/Tenant quota 429
route quota 429
전체 write concurrency 포화 503
expensive query concurrency 포화 503
bounded queue timeout 503 + Retry-After 가능

무한 queue는 latency를 숨기므로 maxConcurrent + small bounded queue + queueTimeout 방식이 적절합니다.

Filter·Interceptor·Advice 실행 모델

제안된 논리 순서는 대체로 맞지만, Spring의 실제 실행 단계에서 Route 선택 전에는 route-specific admission이나 method-specific authorization을 완전히 판단할 수 없다는 점을 반영해야 합니다.

권장 의미 계층은 다음입니다.

Edge Phase
  Forwarded normalization
  → CORS
  → trace/request-id
  → authentication
  → global request budget/admission

Routing Phase
  → route mapping
  → API version resolution

Handler Policy Phase
  → route-specific authorization
  → route-specific admission/rate limit
  → idempotency requirement
  → precondition policy

Binding Phase
  → decode
  → bind
  → transport validation

Application Phase
  → controller adapter
  → use case
  → object/tenant authorization
  → transaction

Outbound Phase
  → response mapping
  → cache/ETag
  → Problem Details if response uncommitted
  → metric/access log

MVC에서는 Servlet Filter, Spring Security chain, HandlerInterceptor, argument resolver, ControllerAdvice 등이 이 논리 계층에 대응합니다. Async MVC는 최초 request thread에서 반환된 후 ASYNC redispatch가 발생할 수 있으므로 filter의 dispatcher type과 “한 요청당 한 번” 관측 정책을 반드시 통합시험해야 합니다. Spring 문서도 async dispatch가 별도의 Servlet dispatch임을 설명합니다. citeturn1search0

또한 request body를 logging/idempotency filter가 먼저 읽어 소비하는 구조는 피해야 합니다.

Bad:
Filter
→ read entire InputStream
→ String
→ hash/log
→ Controller

Preferred:
bounded codec/binder
→ typed request
→ canonical operation fingerprint
→ idempotency gateway
→ use case

Idempotency fingerprint를 raw JSON byte 단위로 할지 semantic DTO 단위로 할지도 계약으로 고정해야 합니다. 일반 업무 API에는 route + normalized path inputs + semantic command DTO의 deterministic fingerprint가 더 다루기 쉽습니다. JSON whitespace나 object field order 차이 때문에 동일 업무 요청이 다른 fingerprint가 되는 문제를 줄일 수 있기 때문입니다.

OpenAPI·계약 관리·관측성

OpenAPI 기준선

OpenAPI의 최신 공개 규격은 3.2.0이며 2025년 9월 19일 발표되었습니다. 3.2에는 sequential media type의 streaming을 더 정확하게 표현하기 위한 itemSchema가 도입되어 SSE, NDJSON/JSON sequence 성격의 API에 특히 유용합니다. citeturn18search2

하지만 Stable 산출물을 바로 3.2.0으로 올리는 것은 아직 권고하지 않습니다.

현재 springdoc-openapi 문서는 Spring Boot 4를 지원하는 3.x 계열과 2026년 7월 기준 stable 3.1.0을 안내하고 있지만 지원 범위를 일반적으로 “OpenAPI 3”이라고 표현합니다. springdoc 자체도 Spring Framework 팀이 유지하는 공식 Spring 프로젝트가 아니라 community project입니다. citeturn17search2turn17search4turn21view3

OpenAPI Generator의 2026년 7월 stable release는 7.24.0이지만 공식 README는 입력 지원을 여전히 OpenAPI 2.0과 3.0 계열로 포괄적으로 표현하고 있고, 2026년 1월 제기된 “OpenAPI 3.2 지원” issue도 별도 enhancement 요청 상태입니다. citeturn18search13turn18search4turn18search0

따라서 권장 정책은 다음입니다.

Stable artifact
→ OpenAPI 3.1.2

Compatibility lane
→ OpenAPI 3.2.0

3.2 승격 조건
→ springdoc output
→ Swagger/Scalar render
→ linter
→ diff tool
→ selected Java/TS generators
→ generated client compile
→ SSE/NDJSON schema
모두 통과

즉, “최신 스펙”과 “조직 Stable 계약 format”을 분리합니다.

Contract-first vs Code-first

Web 플랫폼에는 Hybrid 방식이 가장 적절합니다.

Controller + DTO + Validation
         ↓
Runtime generated OpenAPI
         ↓
Normalize
         ↓
Approved Snapshot
         ↓
CI Lint / Breaking Diff
         ↓
Client generation

그 결과 runtime annotation이 API contract의 유일한 source가 되지도 않고, 반대로 구현과 분리된 YAML이 계속 drift하는 것도 방지할 수 있습니다.

Release Gate:

Gate 실패 조건
Route inventory 문서에 없는 public route
OpenAPI generation runtime 생성 실패
OpenAPI lint 조직 규칙 위반
breaking diff 비승인 breaking change
Problem schema catalog 불일치
generated client compile 실패
consumer contract 기존 client scenario 실패
deprecated API owner/usage gate 불충족

OpenAPI 문서에서는 반드시 다음을 표현해야 합니다.

success statuses
Problem Details
validation constraints
security scheme
pagination cursor
ETag / If-Match
Idempotency-Key profile
Deprecation
202 Operation Resource
content negotiation
SSE/NDJSON profile

Swagger UI/Scalar는 운영 기능이지 public API contract 자체가 아닙니다.

환경 권고
Local 허용
Test 허용
Dev 인증 후
Staging 관리자
Prod 기본 비활성 또는 admin plane

springdoc은 MVC와 WebFlux 각각의 starter를 제공하므로 Web Platform에서도 각각 독립 의존성을 가져야 합니다. citeturn21view3

Metric·Trace·Access Log

Spring Boot는 MVC와 WebFlux의 HTTP server request를 자동 계측하며 기본 metric 이름으로 http.server.requests를 사용합니다. Spring Boot observability는 Micrometer Observation을 metrics와 traces의 공통 abstraction으로 사용합니다. 따라서 플랫폼이 HTTP 계측 전체를 재구현하는 대신 tag vocabulary와 cardinality, problem/idempotency/admission 관련 custom observation만 추가하는 편이 좋습니다. citeturn16search3turn16search7

Spring Framework도 server request observation convention을 customization할 수 있습니다. citeturn18search3

권장 metric dimension:

method
routeTemplate
status
outcome
apiVersion
operationName
problemCode
clientProfile

금지:

raw URL
query string
userId
tenantId raw
resourceId
Idempotency-Key
access token
cookie
request body

추가 플랫폼 metric:

Metric 의미
active requests 현재 실행
request/response bytes payload
validation failures transport quality
problem count typed error
admission rejects overload
rate-limit rejects quota
idempotency new/replay/conflict mutation safety
operation accepted/completed async lifecycle
active streams streaming pressure
stream duration connection age
slow consumers streaming bottleneck
client disconnect partial delivery

Access log는 metrics보다 높은 cardinality를 허용할 수 있지만 secret/PII를 넣지 않습니다.

timestamp
requestId
traceId
method
routeTemplate
status
duration
requestBytes
responseBytes
apiVersion
actorFingerprint      // 정책 허용 시
normalized client-IP  // 개인정보 정책 적용

body logging은 기본 비활성으로 두는 것이 맞습니다.

Audit은 access log와 목적이 다릅니다.

관리자 API
강제 삭제
권한 변경
redrive
sunset 변경
idempotency 수동 해제

는 별도 durable audit stream으로 보내야 합니다.

테스트 전략과 지원 매트릭스

Contract·기능·오류 테스트

MockMvc나 WebTestClient는 빠른 Web-layer 검증에 적합하지만 WebTestClient 자체도 mock request/response와 실제 running server 두 방식 모두를 지원합니다. 따라서 network semantics가 필요한 검증을 mock에 의존하지 않는 구성이 가능합니다. citeturn18search15

최소 Release Gate는 다음 매트릭스를 가져야 합니다.

영역 필수 시나리오
Routing 존재/미존재 path, method, trailing slash, encoded path
Negotiation bad Content-Type, bad Accept, language
Binding missing/null/empty/duplicate
JSON malformed, unknown field, duplicate key, depth
Validation field/nested/collection/cross-field
Success 200/201/202/204
Error 400/401/403/404/409/412/422/429/5xx
Headers Location, ETag, Cache-Control, Vary, Allow
HEAD GET metadata 일치, body 없음
Conditional 304, If-Match, If-None-Match
Problem type/code/status/pointer, no stack
Versioning old/new/default/unsupported
Deprecation Deprecation/Sunset/Link
Pagination limit, cursor, stable sort
OpenAPI snapshot, breaking diff

Idempotency·응답 유실 테스트

이 영역은 일반 Controller test와 별개로 fault-injection이 필요합니다.

같은 key + 같은 payload 직렬 요청
같은 key + 같은 payload 동시 요청
같은 key + 다른 payload
PROCESSING 중 process crash
business commit 직전 crash
business commit 직후 crash
idempotency record commit 전 crash
response headers 전 reset
response body 일부 후 reset
response 완전 write 후 client-side timeout
TTL 경계
expired key reuse
operation reconciliation

가장 중요한 성공 조건은 다음입니다.

business commit 후 응답 유실
→ 같은 key 재요청
→ business mutation 두 번 발생하지 않음
→ 이전 result 또는 reconciliation reference 반환

Streaming 테스트

정상 complete
empty stream
slow consumer
server producer burst
bounded buffer
heartbeat
idle timeout
max stream age
client disconnect
server shutdown
partial item 후 exception
terminal error frame
abrupt EOF
SSE Last-Event-ID
resume success
resume gap
event log unavailable

특히 200이 이미 commit된 이후 application error가 발생해도 테스트가 500을 기대하면 안 됩니다. 이 경우 transport stream contract를 검증해야 합니다.

Security·Proxy 테스트

CORS preflight
credentialed CORS
disallowed Origin
CSRF token missing/invalid
BOLA
property authorization
mass assignment
tenant bypass

raw Host injection
Forwarded injection
X-Forwarded-For spoof
X-Forwarded-Prefix spoof
scheme confusion
absolute Location poisoning
open redirect

Nginx를 포함한 실제 topology test가 필요합니다.

Client
→ HTTPS Nginx
→ HTTP/HTTP2 internal
→ Spring

이 시험에서 Location, secure redirect, external scheme, host, prefix, client IP가 기대값과 일치해야 합니다.

Abuse·성능·실서버 테스트

header limit
URI limit
body limit
deep JSON
huge arrays
decompression bomb
slow request body
slow response consumer
connection flood
request flood
write flood
expensive query flood

서버별 lane:

Stack 필수
MVC + Tomcat Stable gate
MVC + Jetty compatibility gate
MVC + Virtual Thread Advanced performance gate
WebFlux + Reactor Netty WebFlux Stable gate
Nginx + chosen server production topology gate

Spring Boot는 Tomcat, Jetty, Reactor Netty를 포함해 graceful shutdown을 지원하고 shutdown grace 동안 새 request를 받지 않으면서 in-flight 요청을 처리하는 기능을 제공합니다. 다만 실제 request rejection 동작은 web server와 persistent connection에 따라 차이가 있으므로 실제 server에서 확인해야 합니다. citeturn16search26turn16search20

Performance test에는 평균 latency보다 다음 지표가 중요합니다.

p50 / p95 / p99 latency
throughput
active requests
queued requests
rejected requests
heap
GC
thread count
virtual thread count
event-loop saturation
DB connection pool
response write latency
stream buffer
client disconnect rate

Stable·Advanced·Experimental·비지원

최종 공개 범위는 다음과 같이 정리하는 것을 권고합니다.

기능 등급
MVC Controller/DTO Stable
Tomcat MVC Stable 기본
Jetty MVC Stable compatibility
Validation Stable
RFC 9457 Problem Details Stable
path major version Stable
pagination/Slice Stable
keyset cursor Stable
ETag/conditional GET Stable
If-Match mutation Stable
OpenAPI 3.1.2 Stable
Idempotency for registered mutation Stable capability
Idempotency-Key를 “IETF 표준”으로 선언 금지
durable 202 operation Stable W2
SSE Advanced
NDJSON Advanced
JSON Merge Patch RFC 7396 Advanced
JSON Patch RFC 6902 Advanced
WebFlux Stable 선택 Profile
Functional WebFlux endpoints Advanced
MVC virtual threads Advanced
CBOR/XML Optional Advanced
OpenAPI 3.2 output Experimental
new RateLimit header draft Experimental
raw ServletRequest 도메인 사용 비지원
raw ServerWebExchange 도메인 사용 비지원
Controller transaction 비지원
Entity 직접 request/response 비지원
arbitrary Map<String,Object> API 비지원
WebFlux event-loop에서 blocking JPA 비지원
Web에서 durable SSE replay 저장 비지원 → Messaging
Web에서 binary large file 비지원 → Fileserver
Web에서 bidirectional messaging 비지원 → WebSocket
모든 응답 ApiResponse<T> 기본 비지원

단계별 구현 순서와 완료 조건

이 플랫폼은 한 번에 모든 W1~W4 기능을 구현하기보다 HTTP 의미론 → 실행 증거 → 운영 기능 순서로 올리는 것이 위험이 가장 낮습니다.

기반 계약

먼저 web-core-api, MVC starter, DTO/Controller 규칙, JSON profile, RFC 9457, HTTP status/header policy를 완성합니다.

완료 조건

MVC + Tomcat 실제 서버 기동
MVC/WebFlux starter 동시 존재 시 fail-fast
Entity response/request 정적 검사 또는 ArchUnit 규칙
400/404/405/406/415/422/500 Problem contract
201 Location
204 no-body
HEAD semantics
Content-Type/Accept tests

Spring Boot 4.1.0 + Framework 7.0.8 조합을 BOM의 유일한 Spring 버전 source로 둡니다. citeturn2search3

계약 진화

다음으로 API versioning, deprecation, OpenAPI snapshot/diff, pagination catalog를 추가합니다.

완료 조건

/api/v1 route inventory
OpenAPI 3.1.2 snapshot
breaking diff CI
generated client compile
Deprecation/Sunset response
cursor tamper test
sort/filter allowlist

OpenAPI 3.2는 별도 compatibility job으로만 생성해 봅니다. OAS 3.2의 streaming 표현력은 분명히 향상됐지만 현재 generator/tooling lane을 통과한 뒤 Stable로 승격하는 것이 좋습니다. citeturn18search2turn18search0

동시성·상태 변경 안전성

그 다음 ETag/If-Match와 idempotency를 구현합니다.

완료 조건

GET → ETag
If-Match success/failure
create-only conditional
Idempotency-Key scope
request fingerprint
concurrent same-key exclusion
same-key different request rejection
same-key result replay
DB commit + idempotency evidence atomicity
response-loss fault test

여기서 Commit Evidence를 구현하지 못한 상태에서 자동 retry를 제공하면 안 됩니다.

비동기 Operation

Messaging/outbox와 연결해 202를 구현합니다.

완료 조건

durable acceptance 후에만 202
Operation Resource
poll
resultLocation
failure Problem
cancellation
expiration
restart recovery
duplicate submission

RFC 9110의 202가 처리 완료 자체를 보장하지 않는다는 점 때문에, 이 durable operation model이 HTTP 위에 플랫폼이 추가해야 할 핵심 계약입니다. citeturn15view0

Security·Proxy·Budget

Nginx를 실제로 붙인 topology test를 수행합니다.

완료 조건

trusted proxy boundary
Forwarded stripping
external URL
CORS
CSRF
object/property auth
body/header/path limits
rate limit
admission
429/503
slow client tests

Spring이 권고하는 것처럼 forwarded headers는 애플리케이션에서 임의로 신뢰하지 않고 경계 proxy가 sanitize해야 합니다. citeturn10search0

Streaming과 WebFlux

기본 Unary-style HTTP API가 안정화된 뒤 streaming을 추가합니다.

완료 조건

SSE heartbeat
idle timeout
max stream age
bounded buffering
partial-write error contract
client disconnect
shutdown drain
Messaging resume integration
Reactor Netty real-server tests

MVC streaming은 별도 production executor가 없으면 Stable로 선언하지 않습니다. Spring도 기본 async executor가 부하 환경에 적합하지 않음을 경고합니다. citeturn1search0

최종 운영 Gate

마지막으로 W4를 활성화합니다.

Route Inventory
OpenAPI Publish
Breaking Diff
Deprecated Route Usage
Problem Catalog Inventory
Metric Cardinality Test
Access Log Redaction Test
Audit Event Test
Fault Injection
Load Test
Nginx Contract Test
Graceful Shutdown Test

최종적으로 이 web 플랫폼의 핵심 불변식은 다음 일곱 가지로 압축할 수 있습니다.

HTTP Status는 업무 결과를 숨기지 않는다.

Controller는 Application Use Case Adapter다.
Transaction이나 Persistence 경계가 아니다.

Application Commit과 HTTP Response Delivery는 별개의 증거다.

상태 변경 재시도는 Status Code가 아니라
Idempotency Evidence로 판단한다.

ETag/If-Match는 동시성 제어이고
Idempotency는 중복 실행 제어다.

Response가 commit된 Streaming에서는
ProblemDetail로 HTTP status를 다시 바꿀 수 없다.

HTTP cache, Proxy, Security, OpenAPI, Observability도
Controller 외부의 부가 기능이 아니라
공개 HTTP 계약의 일부다.

이 원칙을 기준으로 하면 이번 web은 “Spring MVC 공통 코드”가 아니라, 요청이 어디까지 실행됐는지, mutation이 실제로 commit됐는지, 재호출이 안전한지, partial response 이후 무엇을 복구할 수 있는지를 명시적으로 판정하는 인바운드 HTTP 실행 플랫폼으로 자리 잡게 됩니다.