# 인바운드 HTTP API 실행 플랫폼 `web` 심층 리서치 본 조사에서 `web`은 Spring MVC 설정 묶음이나 공통 `ControllerAdvice`가 아니라, **HTTP 요청이 신뢰 경계를 통과해 Application Use Case에 진입하고, 그 실행 결과가 HTTP 의미론으로 다시 외부에 노출되는 전 과정을 통제하는 플랫폼**으로 정의하는 것이 적절합니다. 특히 플랫폼이 보장해야 할 것은 “요청을 받았다”는 사실이 아니라 다음 증거의 구분입니다. ```text 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-web`과 `spring-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 따라서 실행 모델은 다음처럼 선언하는 것이 가장 명확합니다. ```text 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 권장 호출 구조는 다음과 같습니다. ```text 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` 내부에 넣지 않는 것**을 권고합니다. ```text 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 ``` 의존성 방향은 다음처럼 제한하는 것이 좋습니다. ```text 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 형식은 다음이 적절합니다. ```text /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` | 구현 간단 | 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`는 더 명확하게 구분해야 합니다. ```text 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 요청은 다음 세 계층으로 분리해야 합니다. ```text 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를 사용해야 합니다. ```java 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`에 넣는 방식보다는 다음을 권장합니다. ```text 성공 → 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는 다음처럼 제한합니다. ```json { "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 기본은 다음을 권고합니다. ```text 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를 모두 통과해야 합니다. ```text 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이어야 하며 최소 다음 의미를 포함하거나 서버 쪽 상태로 참조해야 합니다. ```text cursorVersion queryProfile sortValues uniqueTieBreaker filterFingerprint issuedAt integrity MAC ``` 예를 들어 동일 `createdAt` 값을 가진 행이 여러 개 있을 수 있으므로 정렬은 다음처럼 반드시 total order가 되어야 합니다. ```text ORDER BY createdAt DESC, documentId DESC ``` API가 직접 허용할 query vocabulary를 관리해야 합니다. ```text SortFieldCatalog FilterFieldCatalog FilterOperatorCatalog ProjectionProfile IncludeProfile ``` 따라서 다음 API는 금지합니다. ```text ?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를 동일 개념으로 만들 필요는 없습니다. ```text DB version / aggregate version ↓ ETag representation ↓ If-Match ↓ Application expectedVersion ``` Strong ETag를 mutation concurrency token으로 사용하는 것을 권장합니다. ```http GET /api/v1/documents/d1 ETag: "v17" PATCH /api/v1/documents/d1 If-Match: "v17" ``` version이 이미 `v18`이면: ```text 412 Precondition Failed ``` `If-Match`는 strong comparison을 사용하며 state-changing method에서 lost-update 방지를 위한 대표적인 용도로 정의됩니다. citeturn14view3 Create-only PUT도 다음처럼 표현할 수 있습니다. ```http PUT /api/v1/documents/client-generated-id If-None-Match: * ``` Conditional request의 목적과 Idempotency는 구분해야 합니다. ```text 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: ```text tenantId + actor/client identity + operationId or route contract + idempotencyKey ``` 저장 정보: ```text 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을 증명해서는 안 됩니다.** 예를 들어: ```text DB COMMIT 성공 → 프로세스 crash → Redis COMPLETED 기록 실패 ``` 가 발생하면 재시도가 mutation을 또 실행할 수 있습니다. 따라서 DB-local mutation이라면 다음이 가장 강합니다. ```text 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가 되어야 합니다. ```text POST ↓ Application Transaction COMMIT ↓ HTTP response write 시작 ↓ TCP reset ↓ Client sees IOException ``` 이 상황에서 서버가 “client가 받지 못했으므로 rollback”할 방법은 없습니다. 따라서 client 재시도를 안전하게 만드는 수단은 다음 중 하나입니다. ```text 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 따라서 다음 계약이 적절합니다. ```http POST /api/v1/exports → 202 Accepted Location: /api/v1/operations/op_123 Retry-After: 3 ``` ```text GET /api/v1/operations/op_123 PENDING → RUNNING → SUCCEEDED ↘ FAILED ↘ CANCELED ``` Operation DTO: ```text operationId status createdAt startedAt completedAt progress resultLocation problem retryAfter expiresAt ``` **202는 “비동기 thread를 시작했다”는 의미가 아니라 “비동기 작업을 추적할 수 있는 방식으로 접수했다”는 플랫폼 계약**으로 강화하는 것이 좋습니다. 따라서: ```text Controller → @Async 호출 → 202 ``` 는 Stable 구현으로 인정하지 않습니다. Spring의 async execution은 `TaskExecutor` 기반 process-local execution abstraction이므로 durable queue나 crash recovery를 의미하지 않습니다. citeturn18search24 Stable 202 조건은 다음입니다. ```text 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-cache`와 `no-store`는 구분해야 합니다. ```text 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>` | Advanced | | NDJSON | emitter/streaming writer | `Flux` | 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`로 바꿀 수 있습니다. ```text Controller 실행 → 오류 → headers 미전송 → 4xx/5xx + application/problem+json ``` 그러나 headers와 일부 stream frame이 이미 전송된 뒤라면 HTTP status를 200에서 500으로 바꿀 수 없습니다. ```text 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를 만들지 않습니다. ```text 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로 구성해야 합니다. ```text allowed origins → exact allowlist allowed methods → API별 allowlist allowed headers → explicit set exposed headers → 필요한 응답 Header만 credentials → 명시적으로 필요한 API만 preflight max-age → 정책값 ``` 다음은 금지합니다. ```text 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은 다음 순서로 분리합니다. ```text 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의 `ForwardedHeaderFilter`는 `Forwarded` 및 `X-Forwarded-*` 정보를 바탕으로 scheme, host, port 등을 외부 요청 기준으로 변환할 수 있지만, Spring 공식 문서는 application이 header가 악의적 client에서 왔는지 trusted proxy에서 왔는지 자체적으로 알 수 없기 때문에 **신뢰 경계의 proxy가 외부 Forwarded header를 제거하고 자신이 설정해야 한다**고 명시합니다. citeturn10search0 따라서 Host Nginx 구조는 다음처럼 고정하는 것이 좋습니다. ```text 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 계산도 다음처럼 해야 합니다. ```text raw Host raw X-Forwarded-Host ↓ 직접 사용 금지 trusted proxy normalization ↓ NormalizedExternalRequestContext ↓ Location / redirect / absolute Link ``` 고위험 URL, 예를 들어 password reset이나 callback 등은 가능하면 request host를 재조립하지 말고 configured external origin을 사용하는 편이 더 안전합니다. 시험해야 할 공격: ```text 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이 발생하는지 예측할 수 없으므로 다음 값을 함께 관리해야 합니다. ```text 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를 담당합니다. ```text 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과 별개입니다. ```text 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을 완전히 판단할 수 없다는 점**을 반영해야 합니다. 권장 의미 계층은 다음입니다. ```text 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가 먼저 읽어 소비하는 구조는 피해야 합니다. ```text 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 따라서 권장 정책은 다음입니다. ```text 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 방식이 가장 적절합니다. ```text 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 문서에서는 반드시 다음을 표현해야 합니다. ```text 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: ```text method routeTemplate status outcome apiVersion operationName problemCode clientProfile ``` 금지: ```text 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를 넣지 않습니다. ```text timestamp requestId traceId method routeTemplate status duration requestBytes responseBytes apiVersion actorFingerprint // 정책 허용 시 normalized client-IP // 개인정보 정책 적용 ``` body logging은 기본 비활성으로 두는 것이 맞습니다. Audit은 access log와 목적이 다릅니다. ```text 관리자 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이 필요합니다. ```text 같은 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 ``` 가장 중요한 성공 조건은 다음입니다. ```text business commit 후 응답 유실 → 같은 key 재요청 → business mutation 두 번 발생하지 않음 → 이전 result 또는 reconciliation reference 반환 ``` ### Streaming 테스트 ```text 정상 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 테스트 ```text 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가 필요합니다. ```text Client → HTTPS Nginx → HTTP/HTTP2 internal → Spring ``` 이 시험에서 `Location`, secure redirect, external scheme, host, prefix, client IP가 기대값과 일치해야 합니다. ### Abuse·성능·실서버 테스트 ```text 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보다 다음 지표가 중요합니다. ```text 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` API | 비지원 | | WebFlux event-loop에서 blocking JPA | 비지원 | | Web에서 durable SSE replay 저장 | 비지원 → Messaging | | Web에서 binary large file | 비지원 → Fileserver | | Web에서 bidirectional messaging | 비지원 → WebSocket | | 모든 응답 `ApiResponse` | 기본 비지원 | ## 단계별 구현 순서와 완료 조건 이 플랫폼은 한 번에 모든 W1~W4 기능을 구현하기보다 **HTTP 의미론 → 실행 증거 → 운영 기능** 순서로 올리는 것이 위험이 가장 낮습니다. ### 기반 계약 먼저 `web-core-api`, MVC starter, DTO/Controller 규칙, JSON profile, RFC 9457, HTTP status/header policy를 완성합니다. ```text 완료 조건 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를 추가합니다. ```text 완료 조건 /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를 구현합니다. ```text 완료 조건 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`를 구현합니다. ```text 완료 조건 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를 수행합니다. ```text 완료 조건 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을 추가합니다. ```text 완료 조건 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를 활성화합니다. ```text 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` 플랫폼의 핵심 불변식은 다음 일곱 가지로 압축할 수 있습니다. ```text 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 실행 플랫폼**으로 자리 잡게 됩니다.