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도 멱등이 아닌 요청은 원래 요청이 적용되지 않았음을 알 수 있거나 해당 작업 자체가 멱등임을 아는 경우가 아니라면 자동 재시도를 해서는 안 된다고 규정합니다. citeturn14view0
플랫폼 기준선과 책임 경계
기술 기준선
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 최소 요구사항보다 높은 플랫폼 정책으로 두는 것이 좋습니다. citeturn2search3
Spring Boot는 Servlet 기반 Spring MVC와 Reactive Spring WebFlux를 모두 지원합니다. spring-boot-starter-web과 spring-boot-starter-webflux가 함께 존재하면 Boot는 기본적으로 MVC 애플리케이션으로 구성하므로, “둘 다 의존성에 넣고 런타임에서 알아서 고른다”는 구조는 피해야 합니다. citeturn3search0
권장 기준선은 다음과 같습니다.
| 영역 | 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로 취급하는 편이 안전합니다. citeturn2search1
MVC 비동기 응답에 대해서도 주의가 필요합니다. Spring MVC는 Callable, DeferredResult, WebAsyncTask, SseEmitter, reactive return type 등을 지원하지만 Servlet response write 자체는 blocking이며, Spring 문서는 streaming write용 기본 AsyncTaskExecutor가 부하 환경에 적합하지 않다고 명시합니다. citeturn1search0
따라서 실행 모델은 다음처럼 선언하는 것이 가장 명확합니다.
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 기능으로 취급하는 것이 더 정확합니다. citeturn1search1turn8search1
기존 모듈과의 경계
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을 분리할 수 있습니다. citeturn9search3
권장 호출 구조는 다음과 같습니다.
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-idempotency는 IdempotencyStore 같은 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 정책을 명시해야 합니다. citeturn10search0
권장 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라고 해도 구현이 비멱등 부수효과를 추가하면 실제 재호출 안전성은 깨집니다. citeturn14view0
| 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 충돌 위험 때문에 조건부 요청 사용을 권고합니다. citeturn17search1
JSON Patch는 RFC 6902의 application/json-patch+json, JSON Merge Patch는 RFC 7396이 현재 규격이며 RFC 7386을 명시적으로 obsolete합니다. 따라서 신규 문서에서 Merge Patch 기준을 RFC 7386으로 고정하지 말고 RFC 7396으로 업데이트해야 합니다. citeturn17search0turn21view0
권장 등급은 다음과 같습니다.
| 방식 | 장점 | 주요 위험 | 권고 |
|---|---|---|---|
| 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가 없습니다. citeturn14view1turn15view0turn15view1turn15view2
권장 성공 계약은 다음과 같습니다.
| 상황 | 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로 고정하는 것이 일관적입니다. citeturn14view2turn20view0
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가 핵심 응답입니다. citeturn14view3
핵심 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 수준에서 차단하는 편이 적절합니다. citeturn13search1turn13search0turn13search2
기본 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을 무시해야 합니다. citeturn20view0turn20view2
Spring Framework 7의 MVC와 WebFlux는 ProblemDetail, ErrorResponse, ErrorResponseException 및 MVC의 ResponseEntityExceptionHandler 등 RFC 9457 지원을 제공합니다. ProblemDetail.status는 실제 응답 상태 결정에도 사용되고, Spring은 Problem Detail에 application/problem+json을 선호되는 representation으로 제공합니다. citeturn20view3turn8search3
권장 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를 제안합니다. citeturn22view0turn22view1
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를 필수화해야 합니다. citeturn20view1
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 응답을 지원합니다. citeturn8search0turn8search1
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로 정의합니다. citeturn16search5turn16search1
따라서 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 방어의 일부라고 명시합니다. citeturn13search2
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 방지를 위한 대표적인 용도로 정의됩니다. citeturn14view3
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 표준”이라고 문서화하면 안 됩니다. citeturn22view3turn21view1
다만 draft가 정의한 핵심 모델은 플랫폼 설계에 유용합니다. client key와 server-generated fingerprint를 결합하고, 같은 key의 완료된 요청은 원래 결과를 replay하며, 다른 payload로 같은 key를 재사용하지 않는 모델입니다. citeturn22view2
권장 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 자체에서도 중요한 고려 대상임을 보여줍니다. citeturn14view3
202 Accepted와 Operation Resource
RFC 9110의 202는 processing이 완료되지 않았고 최종적으로 실행되지 않을 수도 있음을 뜻하며, HTTP 자체에는 나중에 비동기 결과를 다시 “push”하는 표준 기능이 없다고 명시합니다. citeturn15view0
따라서 다음 계약이 적절합니다.
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를 의미하지 않습니다. citeturn18search24
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가 전역적으로 자동 무효화된다는 보장은 없습니다. citeturn19view1
권장 기본 정책:
| 응답 종류 | 정책 |
|---|---|
| 개인정보·민감한 업무 상태 | 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가 필요하다고 규정합니다. citeturn19view0
또 no-cache와 no-store는 구분해야 합니다.
no-cache
→ 저장 자체를 금지하지 않음
→ 재사용 전에 validation 요구
no-store
→ request/response를 cache에 저장하지 말라는 의미
RFC 9111이 이를 각각 별도 의미로 정의합니다. citeturn19view2
Vary는 response representation이 실제 어떤 request header에 따라 달라졌는지 cache key에 반영하는 수단입니다. Accept, Accept-Language, version header를 사용한다면 해당 차이가 실제 representation을 바꾸는 경우에만 추가해야 합니다. citeturn19view3
실행 스택·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를 사용합니다. citeturn1search0
응답이 아직 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가 포함되지 않을 수 있기 때문입니다. citeturn9search0turn9search2
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 등 다양한 방식을 지원합니다. citeturn9search1
권장 분류:
| 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 수정 권한까지 증명된 것으로 취급하면 안 됩니다. citeturn13search1turn13search0
Forwarded Header와 Nginx
Spring의 ForwardedHeaderFilter는 Forwarded 및 X-Forwarded-* 정보를 바탕으로 scheme, host, port 등을 외부 요청 기준으로 변환할 수 있지만, Spring 공식 문서는 application이 header가 악의적 client에서 왔는지 trusted proxy에서 왔는지 자체적으로 알 수 없기 때문에 신뢰 경계의 proxy가 외부 Forwarded header를 제거하고 자신이 설정해야 한다고 명시합니다. citeturn10search0
따라서 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에 맞게 하나만 선택해야 합니다. citeturn12search0turn12search1turn12search17
동현님이 언급한 /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 위험으로 분류합니다. citeturn13search2
플랫폼 초기 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로 두는 것이 안전합니다. citeturn21view2
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임을 설명합니다. citeturn1search0
또한 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에 특히 유용합니다. citeturn18search2
하지만 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입니다. citeturn17search2turn17search4turn21view3
OpenAPI Generator의 2026년 7월 stable release는 7.24.0이지만 공식 README는 입력 지원을 여전히 OpenAPI 2.0과 3.0 계열로 포괄적으로 표현하고 있고, 2026년 1월 제기된 “OpenAPI 3.2 지원” issue도 별도 enhancement 요청 상태입니다. citeturn18search13turn18search4turn18search0
따라서 권장 정책은 다음입니다.
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에서도 각각 독립 의존성을 가져야 합니다. citeturn21view3
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만 추가하는 편이 좋습니다. citeturn16search3turn16search7
Spring Framework도 server request observation convention을 customization할 수 있습니다. citeturn18search3
권장 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에 의존하지 않는 구성이 가능합니다. citeturn18search15
최소 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에서 확인해야 합니다. citeturn16search26turn16search20
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로 둡니다. citeturn2search3
계약 진화
다음으로 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로 승격하는 것이 좋습니다. citeturn18search2turn18search0
동시성·상태 변경 안전성
그 다음 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 위에 플랫폼이 추가해야 할 핵심 계약입니다. citeturn15view0
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해야 합니다. citeturn10search0
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가 부하 환경에 적합하지 않음을 경고합니다. citeturn1search0
최종 운영 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 실행 플랫폼으로 자리 잡게 됩니다.