Snapshot of the in-flight state that already existed, identically, in both this worktree and the main checkout before this session began: the initial HTTP Client platform implementation (previously untracked), the redis-lab removal, and the JPA / object-storage / notification integration work. Kept separate from this session's HTTP Client review response, which lands in the following commit, so the two bodies of work stay reviewable apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1957 lines
63 KiB
Markdown
1957 lines
63 KiB
Markdown
# HTTP Client Platform 설계서
|
|
|
|
**문서 상태:** 구현 기준선 확정
|
|
**작성 기준일:** 2026-08-08
|
|
**입력 근거:** `Java/Spring 외부 HTTP Client 플랫폼 설계 심층 리서치`
|
|
**대상 저장소:** Spring 기반 Backend Skeleton
|
|
**문서 목적:** 구현 중 추가 설계 판단이나 반복 질문 없이 공개 API, 전송 엔진, Named Client Profile, 시간 예산, 재시도 안전성, 복원력, 인증, 보안, 관측성, 테스트 및 릴리스 조건을 확정한다.
|
|
|
|
---
|
|
|
|
## 1. 요약
|
|
|
|
이 설계는 `httpclient`를 `RestClient`나 `WebClient`를 한 번 감싼 편의 Wrapper가 아니라, 외부 HTTP 호출의 **대상, 연결 자원, 시간 예산, 실행 증거, 재시도 안전성, 인증, 보안, 관측성**을 하나의 계약으로 통제하는 공통 플랫폼으로 정의한다.
|
|
|
|
일반 애플리케이션은 다음 네 단계 중 필요한 최소 단계만 사용한다.
|
|
|
|
1. **H1 Typed Service Client** — `@HttpExchange` 기반 interface를 기본 진입점으로 사용한다.
|
|
2. **H2 Generic Exchange Gateway** — 등록된 Named Client Profile 안에서만 동적 method, path, header, body를 허용한다.
|
|
3. **H3 Dynamic Target Gateway** — 사용자 입력 URL이 필요한 기능을 별도 보안 경계와 SSRF 정책 아래에서 제공한다.
|
|
4. **H4 Native Engine SPI** — Apache, Reactor Netty, JDK, Jetty 고유 API는 플랫폼 내부 또는 Lab에만 공개한다.
|
|
|
|
플랫폼의 핵심 판정은 단순한 `성공/예외`가 아니다.
|
|
|
|
```text
|
|
요청이 실제 서버에 전달됐는가?
|
|
서버가 업무 처리를 완료했을 가능성이 있는가?
|
|
요청 Body를 동일한 의미로 다시 생성할 수 있는가?
|
|
호출이 표준 또는 계약상 멱등한가?
|
|
남은 deadline과 retry budget으로 다음 시도를 완료할 수 있는가?
|
|
```
|
|
|
|
이를 위해 모든 물리 시도는 다음 세 축을 보존한다.
|
|
|
|
```text
|
|
ExecutionEvidence
|
|
├─ NOT_SENT
|
|
├─ SENT_NO_RESPONSE
|
|
├─ RESPONSE_RECEIVED
|
|
└─ PARTIAL_RESPONSE
|
|
|
|
BodyReplayability
|
|
├─ REPLAYABLE
|
|
├─ REOPENABLE
|
|
├─ ONE_SHOT
|
|
└─ UNKNOWN
|
|
|
|
OperationIdempotency
|
|
├─ STANDARD_IDEMPOTENT
|
|
├─ CONTRACT_IDEMPOTENT
|
|
├─ IDEMPOTENCY_KEY_REQUIRED
|
|
└─ NON_IDEMPOTENT
|
|
```
|
|
|
|
최종 구조는 다음과 같다.
|
|
|
|
```text
|
|
Application
|
|
→ Typed Service Client 또는 제한형 Gateway
|
|
→ Named Client Profile
|
|
→ Operation Policy Validation
|
|
→ Effective Deadline
|
|
→ Authentication Materialization
|
|
→ Retry Coordinator
|
|
→ Circuit Breaker
|
|
→ Attempt Rate Limiter
|
|
→ Attempt Bulkhead
|
|
→ RestClient 또는 WebClient
|
|
→ Apache / JDK / Reactor Netty / Jetty
|
|
→ Execution Evidence Classification
|
|
→ Stable Result 또는 Stable Exception
|
|
```
|
|
|
|
---
|
|
|
|
## 2. 목표와 성공 기준
|
|
|
|
### 2.1 목표
|
|
|
|
- 다양한 내부 서비스와 외부 SaaS API를 동일한 운영 기준으로 호출할 수 있게 한다.
|
|
- 일반 서비스 코드는 H1 Typed Client만으로 대부분의 호출을 구현하게 한다.
|
|
- upstream별 connection pool, timeout, 인증, retry, circuit, bulkhead를 서로 격리한다.
|
|
- 비멱등 요청의 중복 실행과 장애 시 retry 폭풍을 구조적으로 차단한다.
|
|
- Blocking과 Reactive 호출을 모두 지원하되 resource lifecycle과 cancellation 의미론을 구분한다.
|
|
- Dynamic URL 호출은 Trusted Client와 완전히 다른 보안 경계로 제공한다.
|
|
- Apache, Reactor Netty, JDK 전송 엔진이 동일한 오류·관측 semantic을 제공하게 한다.
|
|
- 구현자가 timeout, retry, redirect, 인증, TLS, SSRF, streaming 정책을 다시 판단하지 않게 한다.
|
|
|
|
### 2.2 성공 기준
|
|
|
|
| 영역 | 완료 기준 |
|
|
|---|---|
|
|
| 공개 API | 일반 업무 모듈이 Native client를 직접 참조하지 않고 H1 Typed Client를 사용한다. |
|
|
| 설정 | 모든 호출 대상이 `clientName`으로 등록된 Named Client Profile을 가진다. |
|
|
| 시간 예산 | pool acquire부터 retry backoff까지 전체 호출이 effective deadline을 초과하지 않는다. |
|
|
| 실행 증거 | 실패 시 `NOT_SENT`, `SENT_NO_RESPONSE`, `RESPONSE_RECEIVED`, `PARTIAL_RESPONSE` 중 하나를 설명할 수 있다. |
|
|
| Retry | idempotency, body replayability, evidence, status, deadline, retry budget을 모두 통과한 시도만 재실행된다. |
|
|
| Resource | 성공, timeout, decode 실패, size 초과, cancellation에서 connection과 buffer가 회수된다. |
|
|
| 보안 | trust-all, hostname verification 해제, unrestricted Dynamic URL, credential redirect leakage가 차단된다. |
|
|
| 관측성 | 논리 호출과 물리 시도 수가 분리되고 전체 URL·사용자 ID·token이 metric label에 들어가지 않는다. |
|
|
| 호환성 | Spring Framework 6.2와 7.0 지원 범위가 CI 매트릭스로 검증된다. |
|
|
| 전송 엔진 | Apache·JDK blocking과 Reactor Netty reactive가 공통 계약 테스트를 통과한다. |
|
|
| Streaming | 첫 byte가 호출자에게 전달된 이후 투명 retry가 발생하지 않는다. |
|
|
| Dynamic Target | canonicalization, DNS/IP 검증, redirect 재검증, egress 정책이 함께 적용된다. |
|
|
|
|
---
|
|
|
|
## 3. 입력 자료의 제약과 구현 가정
|
|
|
|
첨부 리서치는 설계 방향, 지원 범위, API 초안, 장애 의미론, 테스트 및 구현 순서를 충분히 제공하지만 실제 Backend Skeleton 저장소의 다음 정보는 포함하지 않는다.
|
|
|
|
- root package
|
|
- Java toolchain
|
|
- Gradle 구조
|
|
- Spring Boot BOM
|
|
- 기존 observability·security·resilience 공통 모듈
|
|
- 배포 환경의 proxy, service mesh, egress 정책
|
|
|
|
따라서 이 문서는 실행 가능한 계획을 만들기 위해 다음 구현 기준을 사용한다.
|
|
|
|
| 항목 | 구현 기준 |
|
|
|---|---|
|
|
| Java | Java 21 |
|
|
| 빌드 | Gradle Kotlin DSL 멀티모듈 |
|
|
| root package | `io.backend.skeleton.httpclient` |
|
|
| Spring 기준 | 공통 코드는 Spring Framework 6.2 API 기준으로 컴파일하고 7.0 호환 테스트를 수행한다. |
|
|
| Spring 7 전용 기능 | HTTP Service Group은 독립 선택 모듈로 분리한다. |
|
|
| Spring Boot | host 저장소의 dependency management를 사용하고 라이브러리가 Boot patch version을 직접 고정하지 않는다. |
|
|
| Reactive type | Reactor `Mono`, `Flux`는 reactive integration module에서만 공개한다. |
|
|
| Resilience | Resilience4j를 실행 primitive로 사용하되 HTTP retry 가능성 판정은 플랫폼이 소유한다. |
|
|
| 테스트 | JUnit 5, AssertJ, ArchUnit, MockWebServer, WireMock, Testcontainers, Toxiproxy, BlockHound |
|
|
|
|
실제 저장소가 다른 package 또는 더 높은 Java 기준을 사용하면 경로와 toolchain만 조정한다. 본 문서의 공개 계약, 정책 순서, 오류 의미론은 변경하지 않는다.
|
|
|
|
---
|
|
|
|
## 4. 범위
|
|
|
|
### 4.1 포함 범위
|
|
|
|
- Spring `RestClient`
|
|
- Spring `WebClient`
|
|
- `@HttpExchange` 기반 HTTP Service Client
|
|
- `RestTemplate` 마이그레이션 호환 계층
|
|
- Apache HttpClient 5 blocking transport
|
|
- JDK HttpClient blocking transport
|
|
- Reactor Netty reactive transport
|
|
- Jetty HTTP/3 Experimental transport
|
|
- HTTP/1.1과 HTTP/2 Stable
|
|
- 동기 DTO·header·empty response
|
|
- Reactive `Mono`·`Flux`
|
|
- JSON, XML, text, bytes, form, multipart, octet-stream
|
|
- streaming upload·download
|
|
- SSE
|
|
- redirect, compression, conditional request, Range client semantics
|
|
- proxy와 HTTPS CONNECT
|
|
- connection pool과 lifecycle
|
|
- 단계별 timeout과 전체 deadline
|
|
- retry, retry budget, backoff, jitter, `Retry-After`
|
|
- Circuit Breaker, Bulkhead, Rate Limiter
|
|
- API key, Basic, Bearer, OAuth2 Client, mTLS, request signing SPI
|
|
- TLS 1.2·1.3, custom CA, certificate rotation
|
|
- Dynamic URL SSRF 방어
|
|
- RFC 9457 problem response 변환
|
|
- metric, trace, logging, audit
|
|
- 계약·장애·보안·성능 테스트
|
|
|
|
### 4.2 제외 또는 별도 모듈
|
|
|
|
- WebSocket
|
|
- gRPC
|
|
- GraphQL query·error·subscription 의미론
|
|
- Fileserver의 저장·publish·Range 응답 생성
|
|
- 브라우저 JavaScript HTTP Client
|
|
- API Gateway와 inbound routing
|
|
- 서비스 디스커버리와 client-side load balancing 구현
|
|
- unrestricted Dynamic URL
|
|
- application-facing Native engine access
|
|
- 자동 공유 Cookie Jar
|
|
- TRACE
|
|
- 무제한 redirect
|
|
- one-shot request body의 자동 retry
|
|
- partial response가 호출자에게 전달된 뒤의 투명 retry
|
|
- HTTP/3 공통 Stable 보장
|
|
- request hedging Stable 지원
|
|
- transparent shared response cache
|
|
- trust-all, hostname verification 해제, 평문 fallback
|
|
- Simple request factory의 운영 사용
|
|
- RestTemplate 신규 기능
|
|
|
|
---
|
|
|
|
## 5. 설계 결정
|
|
|
|
| ID | 결정 | 결과 |
|
|
|---|---|---|
|
|
| D-01 | 기본 진입점은 H1 Typed Service Client다. | 일반 업무 코드가 URL, timeout, auth, retry를 매번 조립하지 않는다. |
|
|
| D-02 | H2 Generic Gateway는 등록 profile의 base URL과 정책을 변경할 수 없다. | 범용 호출 기능은 제공하되 정책 우회를 막는다. |
|
|
| D-03 | H3 Dynamic Target Gateway는 별도 모듈·권한·설정으로 제공한다. | Trusted credential, Cookie, default header를 상속하지 않는다. |
|
|
| D-04 | H4 Native API는 플랫폼 내부 SPI다. | 애플리케이션이 engine 설정과 관측성을 우회하지 못한다. |
|
|
| D-05 | 설정 단위는 upstream별 Named Client Profile이다. | pool, timeout, auth, resilience, observability가 upstream마다 격리된다. |
|
|
| D-06 | Blocking 기본은 `RestClient + Apache HC5`, 경량 대안은 JDK HttpClient다. | 세밀한 운영 profile과 의존성 최소화 profile을 모두 제공한다. |
|
|
| D-07 | Reactive·Streaming 기본은 `WebClient + Reactor Netty`다. | backpressure, cancellation, SSE를 안정적으로 제공한다. |
|
|
| D-08 | Jetty와 HTTP/3는 Experimental로 격리한다. | Stable portability와 장애 의미론을 훼손하지 않는다. |
|
|
| D-09 | Retry 가능성은 HTTP method 하나로 결정하지 않는다. | idempotency, idempotency key, body replayability, evidence, deadline, budget을 함께 판정한다. |
|
|
| D-10 | 전체 deadline이 모든 timeout과 retry의 상위 예산이다. | 개별 attempt가 성공해도 전체 사용자 요청 시간을 초과하지 않는다. |
|
|
| D-11 | Retry Coordinator 바깥에서 logical admission을 적용하고, 각 물리 시도는 Circuit → Rate Limiter → Bulkhead를 통과한다. | backoff 중 permit을 점유하지 않고 실제 upstream 요청 수를 제한한다. |
|
|
| D-12 | 첫 response byte를 application에 전달한 뒤에는 transparent retry를 금지한다. | streaming 중복·순서 오류를 차단한다. |
|
|
| D-13 | OAuth2 token 획득은 Spring Security에 위임하되 cache key, refresh single-flight, 401 재호출 규칙은 플랫폼이 고정한다. | 인증 구현을 재작성하지 않으면서 동시 갱신과 중복 호출을 통제한다. |
|
|
| D-14 | TLS 오류 중 trust·hostname·expiry 오류는 영구 오류로 분류한다. | 인증서 오류를 retry하거나 평문으로 fallback하지 않는다. |
|
|
| D-15 | Dynamic Target Stable은 검증한 DNS 결과로 실제 연결을 pin할 수 있는 transport에서만 제공한다. | DNS rebinding과 검사-연결 간 TOCTOU를 줄인다. |
|
|
| D-16 | Spring 표준 `http.client.requests`는 물리 시도 metric으로 유지하고 logical call metric을 추가한다. | retry가 사용자 호출 성공률과 upstream 부하를 왜곡하지 않는다. |
|
|
| D-17 | Spring 6.2 공통 API를 기준으로 하고 Spring 7 전용 Service Group은 선택 모듈로 둔다. | 두 안정 계열을 지원하면서 공통 모듈의 분기를 줄인다. |
|
|
| D-18 | RestTemplate은 migration module에서만 허용한다. | 신규 코드가 deprecated API에 고착되지 않는다. |
|
|
|
|
---
|
|
|
|
## 6. 지원 매트릭스
|
|
|
|
### 6.1 Spring API
|
|
|
|
| API | 등급 | 역할 | 제약 |
|
|
|---|---:|---|---|
|
|
| `RestClient` | Stable | Blocking 요청 실행 | bounded concurrency와 deadline 필수 |
|
|
| `WebClient` | Stable | Reactive·Streaming·SSE | event-loop blocking 금지 |
|
|
| HTTP Service Client | 기본 | 선언형 Typed Client | operation metadata 등록 필수 |
|
|
| `RestTemplate` | Migration only | 기존 호출 이전 | 신규 profile·기능 금지 |
|
|
| Generic Exchange | 제한 | 동적 method·path·body | base URL과 정책 변경 금지 |
|
|
| Dynamic Target | 제한 | 사용자 URL | 별도 SSRF 정책과 credential 미상속 |
|
|
| Native Engine | Internal/Lab | 엔진 고유 기능 | application public API 금지 |
|
|
|
|
### 6.2 전송 엔진
|
|
|
|
| 엔진 | Blocking | Reactive | HTTP/2 | HTTP/3 | Stable 역할 |
|
|
|---|---:|---:|---:|---:|---|
|
|
| Apache HttpClient 5 | 예 | 내부 async 가능 | 예 | 아니오 | Blocking 기본 |
|
|
| JDK HttpClient | 예 | `sendAsync` 가능 | 예 | 아니오 | 경량 Blocking 대안 |
|
|
| Reactor Netty | 제한 | 예 | 예 | Experimental | Reactive 기본 |
|
|
| Jetty HttpClient | sync facade | 예 | 예 | 예 | HTTP/3 Experimental |
|
|
| Simple factory | 예 | 아니오 | 제한 | 아니오 | local test only |
|
|
|
|
### 6.3 HTTP 기능
|
|
|
|
| 기능 | Stable | 제약 |
|
|
|---|---:|---|
|
|
| GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS | 예 | operation idempotency 등록 |
|
|
| TRACE | 아니오 | startup과 runtime에서 차단 |
|
|
| custom method | 제한 | 사전 등록 descriptor 필요 |
|
|
| path·query template | 예 | 문자열 연결 금지, component encoding |
|
|
| absolute URI | H3만 | SSRF 정책 필수 |
|
|
| JSON, XML, text, bytes | 예 | codec와 크기 상한 |
|
|
| form, multipart | 예 | part 수·크기·replayability 계산 |
|
|
| InputStream request | 제한 | one-shot, 자동 retry 금지 |
|
|
| reopenable file request | 예 | 매 시도 새 stream 생성 |
|
|
| DTO response | 예 | decoded size 상한 |
|
|
| InputStream response | 제한 | `AutoCloseable` lifecycle |
|
|
| Reactive body | 예 | cancellation·buffer release |
|
|
| SSE | 예 | setup deadline과 idle timeout 분리 |
|
|
| redirect | 제한 | 기본 off, hop·origin 정책 |
|
|
| compression | 예 | wire·decoded size 모두 제한 |
|
|
| conditional request | 예 | validator 전달 |
|
|
| Range request | 예 | client 의미론만 제공 |
|
|
| trailer | Stable 제외 | engine-specific advanced API |
|
|
| `100-continue` | 선택 | 대용량 replayable body만 |
|
|
| HTTP/1.1 | 예 | fallback |
|
|
| HTTP/2 | 예 | stream concurrency 별도 제한 |
|
|
| HTTP/3 | Experimental | Jetty/Reactor 전용 |
|
|
|
|
---
|
|
|
|
## 7. 전체 아키텍처
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
APP[Application]
|
|
|
|
subgraph PublicAPI[Public API]
|
|
H1[H1 Typed Service Client]
|
|
H2[H2 Generic Exchange]
|
|
H3[H3 Dynamic Target Gateway]
|
|
end
|
|
|
|
subgraph Runtime[Runtime and Policy]
|
|
REG[Client Profile Registry]
|
|
META[Operation Descriptor Registry]
|
|
TARGET[Target Policy]
|
|
DEADLINE[Deadline Calculator]
|
|
AUTH[Authentication Provider]
|
|
RETRY[Retry Coordinator]
|
|
RES[Attempt Resilience]
|
|
ERROR[Error Mapper]
|
|
OBS[Observation]
|
|
end
|
|
|
|
subgraph SpringClients[Spring Client Layer]
|
|
REST[RestClient]
|
|
WEB[WebClient]
|
|
end
|
|
|
|
subgraph Transport[Transport Providers]
|
|
APACHE[Apache HC5]
|
|
JDK[JDK HttpClient]
|
|
REACTOR[Reactor Netty]
|
|
JETTY[Jetty Experimental]
|
|
end
|
|
|
|
APP --> H1
|
|
APP --> H2
|
|
APP --> H3
|
|
H1 --> REG
|
|
H2 --> REG
|
|
H3 --> REG
|
|
REG --> META
|
|
META --> TARGET
|
|
TARGET --> DEADLINE
|
|
DEADLINE --> AUTH
|
|
AUTH --> RETRY
|
|
RETRY --> RES
|
|
RES --> REST
|
|
RES --> WEB
|
|
REST --> APACHE
|
|
REST --> JDK
|
|
WEB --> REACTOR
|
|
WEB --> JETTY
|
|
REST --> ERROR
|
|
WEB --> ERROR
|
|
ERROR --> OBS
|
|
```
|
|
|
|
### 7.1 논리 호출 흐름
|
|
|
|
```text
|
|
1. profileName과 operationName을 해석한다.
|
|
2. profile과 operation descriptor를 immutable snapshot으로 가져온다.
|
|
3. method, URI template, body type, idempotency metadata를 검증한다.
|
|
4. Trusted 또는 Dynamic target policy를 적용한다.
|
|
5. parent deadline과 profile timeout에서 effective deadline을 계산한다.
|
|
6. credential을 materialize한다.
|
|
7. logical admission limit를 통과한다.
|
|
8. Retry Coordinator가 attempt 1을 생성한다.
|
|
9. attempt가 Circuit Breaker → Rate Limiter → Bulkhead를 통과한다.
|
|
10. RestClient 또는 WebClient가 물리 요청을 실행한다.
|
|
11. transport classifier가 stage와 execution evidence를 판정한다.
|
|
12. Retry Eligibility Engine이 다음 시도 여부를 결정한다.
|
|
13. 최종 결과를 `HttpCallResult` 또는 안정 예외로 반환한다.
|
|
14. 성공·실패·cancel 모두에서 response body와 connection을 정리한다.
|
|
```
|
|
|
|
### 7.2 Runtime 세대 교체
|
|
|
|
Named Client Profile은 mutable client를 직접 수정하지 않는다.
|
|
|
|
```text
|
|
ClientRuntimeRegistry
|
|
payment → generation 17
|
|
search → generation 4
|
|
```
|
|
|
|
인증서, secret, base URL 또는 pool 설정이 변경되면 다음 순서로 교체한다.
|
|
|
|
1. 새 immutable `ClientRuntime`을 생성한다.
|
|
2. startup validation과 선택적 connectivity probe를 수행한다.
|
|
3. registry pointer를 새 generation으로 atomic swap한다.
|
|
4. 신규 호출은 새 runtime을 사용한다.
|
|
5. 기존 runtime은 drain timeout 동안 진행 호출을 완료한다.
|
|
6. timeout 후 pool과 connection을 강제 종료한다.
|
|
|
|
이 구조는 mTLS certificate와 OAuth client secret rotation을 connection pool lifecycle과 일치시킨다.
|
|
|
|
---
|
|
|
|
## 8. 모듈 구조
|
|
|
|
```text
|
|
backend-skeleton/
|
|
├── modules/httpclient/
|
|
│ ├── httpclient-core-api/
|
|
│ ├── httpclient-profile/
|
|
│ ├── httpclient-transport-spi/
|
|
│ ├── httpclient-transport-apache/
|
|
│ ├── httpclient-transport-jdk/
|
|
│ ├── httpclient-restclient/
|
|
│ ├── httpclient-resilience/
|
|
│ ├── httpclient-auth/
|
|
│ ├── httpclient-security/
|
|
│ ├── httpclient-observability/
|
|
│ ├── httpclient-transport-reactor-netty/
|
|
│ ├── httpclient-webclient/
|
|
│ ├── httpclient-service-client/
|
|
│ ├── httpclient-dynamic-target/
|
|
│ ├── httpclient-resttemplate-migration/
|
|
│ ├── httpclient-spring7-service-groups/
|
|
│ ├── httpclient-jetty-http3-experimental/
|
|
│ ├── httpclient-spring-boot-starter/
|
|
│ └── httpclient-testkit/
|
|
├── infra/httpclient/
|
|
│ ├── proxy/
|
|
│ ├── tls/
|
|
│ ├── oauth2/
|
|
│ └── toxiproxy/
|
|
└── docs/httpclient/
|
|
```
|
|
|
|
| 모듈 | 책임 | 의존 규칙 |
|
|
|---|---|---|
|
|
| `httpclient-core-api` | 안정 타입, result, evidence, body, 오류 | Spring·Apache·Netty·Resilience4j에 의존하지 않는다. |
|
|
| `httpclient-profile` | Named Client Profile, validation, runtime registry | core-api에만 공개적으로 의존한다. |
|
|
| `httpclient-transport-spi` | blocking·reactive transport provider와 classifier | Spring Web integration type은 이 SPI부터 허용한다. |
|
|
| `httpclient-transport-apache` | Apache HC5 request factory, pool, proxy, TLS hooks | native client를 외부에 반환하지 않는다. |
|
|
| `httpclient-transport-jdk` | JDK request factory와 제한 capability | fine-grained pool이 필요한 profile을 거부한다. |
|
|
| `httpclient-restclient` | Blocking Generic Gateway와 RestClient 실행 pipeline | Apache/JDK provider를 선택한다. |
|
|
| `httpclient-resilience` | deadline, retry, budget, circuit, rate, bulkhead | HTTP-specific retry 판정을 소유한다. |
|
|
| `httpclient-auth` | API key, Basic, Bearer, OAuth2, mTLS identity, signing SPI | token과 secret을 result·log에 노출하지 않는다. |
|
|
| `httpclient-security` | target, URI, redirect, header, body size, TLS 정책 | H1·H2·H3 모두 우회하지 못한다. |
|
|
| `httpclient-observability` | logical·attempt metric, trace, redaction | low-cardinality vocabulary를 소유한다. |
|
|
| `httpclient-transport-reactor-netty` | Reactor connector, pool, timeout, cancel | event-loop blocking을 허용하지 않는다. |
|
|
| `httpclient-webclient` | Reactive Generic Gateway, streaming, SSE | Reactor Context로 operation metadata를 전달한다. |
|
|
| `httpclient-service-client` | `@HttpExchange` proxy, profile·operation annotation | blocking·reactive proxy를 생성한다. |
|
|
| `httpclient-dynamic-target` | canonicalization, DNS/IP pinning, redirect revalidation | trusted credential을 의존하거나 상속하지 않는다. |
|
|
| `httpclient-resttemplate-migration` | 기존 RestTemplate 설정을 RestClient로 이전 | 신규 feature annotation을 제공하지 않는다. |
|
|
| `httpclient-spring7-service-groups` | Spring 7 HTTP Service Group 통합 | Spring 6.2 core에서 완전히 분리한다. |
|
|
| `httpclient-jetty-http3-experimental` | Jetty HTTP/3 connector와 capability matrix | Stable starter가 자동 활성화하지 않는다. |
|
|
| `httpclient-spring-boot-starter` | properties, auto-configuration, validation | production unsafe 설정에서 startup을 실패시킨다. |
|
|
| `httpclient-testkit` | mock·fault·TLS·H2·proxy·OAuth contract fixture | production module에서 의존하지 않는다. |
|
|
|
|
---
|
|
|
|
## 9. 공개 API
|
|
|
|
### 9.1 핵심 식별자
|
|
|
|
```java
|
|
public record ClientProfileName(String value) {
|
|
public ClientProfileName {
|
|
if (value == null || !value.matches("[a-z][a-z0-9-]{1,62}")) {
|
|
throw new IllegalArgumentException("invalid client profile name");
|
|
}
|
|
}
|
|
}
|
|
|
|
public record OperationName(String value) {
|
|
public OperationName {
|
|
if (value == null || !value.matches("[a-z][a-z0-9.-]{1,127}")) {
|
|
throw new IllegalArgumentException("invalid operation name");
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### 9.2 H1 Typed Service Client
|
|
|
|
```java
|
|
public interface HttpServiceRegistry {
|
|
<T> T client(ClientProfileName profileName, Class<T> serviceType);
|
|
}
|
|
|
|
@Target(ElementType.TYPE)
|
|
@Retention(RetentionPolicy.RUNTIME)
|
|
public @interface HttpClientProfile {
|
|
String value();
|
|
}
|
|
|
|
@Target(ElementType.METHOD)
|
|
@Retention(RetentionPolicy.RUNTIME)
|
|
public @interface HttpOperationPolicy {
|
|
String name();
|
|
OperationIdempotency idempotency();
|
|
String retryPolicy() default "none";
|
|
String timeoutPolicy() default "default";
|
|
boolean streaming() default false;
|
|
}
|
|
```
|
|
|
|
```java
|
|
@HttpClientProfile("payment")
|
|
@HttpExchange("/payments")
|
|
public interface PaymentClient {
|
|
|
|
@PostExchange
|
|
@HttpOperationPolicy(
|
|
name = "create-payment",
|
|
idempotency = OperationIdempotency.IDEMPOTENCY_KEY_REQUIRED,
|
|
retryPolicy = "payment-write")
|
|
PaymentResponse create(
|
|
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
|
@RequestBody PaymentRequest request);
|
|
}
|
|
```
|
|
|
|
Typed interface는 다음 조건을 만족해야 startup에 성공한다.
|
|
|
|
- interface에 `@HttpClientProfile`이 존재한다.
|
|
- 모든 method에 안정적인 `operationName`이 존재한다.
|
|
- POST·PATCH는 idempotency를 명시한다.
|
|
- `IDEMPOTENCY_KEY_REQUIRED` method에는 등록된 key parameter가 존재한다.
|
|
- streaming method는 one-shot 여부가 드러나는 wrapper type을 사용한다.
|
|
- 반환형이 blocking인지 reactive인지 하나의 interface에서 모호하지 않다.
|
|
|
|
### 9.3 H2 Generic Exchange
|
|
|
|
```java
|
|
public interface GenericHttpGateway {
|
|
<T> HttpCallResult<T> exchange(
|
|
ClientProfileName profileName,
|
|
HttpOperation operation,
|
|
ResponseType<T> responseType);
|
|
}
|
|
|
|
public interface ReactiveHttpGateway {
|
|
<T> Mono<HttpCallResult<T>> exchange(
|
|
ClientProfileName profileName,
|
|
HttpOperation operation,
|
|
ResponseType<T> responseType);
|
|
}
|
|
```
|
|
|
|
H2가 변경할 수 있는 것은 method, profile 내부 상대 path, query, 승인된 header와 body다. 다음은 변경할 수 없다.
|
|
|
|
- scheme
|
|
- host
|
|
- port
|
|
- proxy
|
|
- TLS trust
|
|
- credential provider
|
|
- hard body limit
|
|
- redirect cross-origin 허용
|
|
- metric naming
|
|
|
|
### 9.4 H3 Dynamic Target
|
|
|
|
```java
|
|
public interface DynamicTargetGateway {
|
|
<T> HttpCallResult<T> exchange(
|
|
DynamicTargetPolicyName policyName,
|
|
URI target,
|
|
HttpOperation operation,
|
|
ResponseType<T> responseType);
|
|
}
|
|
|
|
public interface ReactiveDynamicTargetGateway {
|
|
<T> Mono<HttpCallResult<T>> exchange(
|
|
DynamicTargetPolicyName policyName,
|
|
URI target,
|
|
HttpOperation operation,
|
|
ResponseType<T> responseType);
|
|
}
|
|
```
|
|
|
|
H3는 profile의 API key, OAuth token, Cookie, custom default header를 상속하지 않는다. host별 credential이 필요하면 보안 관리자가 `DynamicCredentialBinding`을 별도로 등록한다.
|
|
|
|
### 9.5 H4 Native SPI
|
|
|
|
다음 형태의 application-facing API는 제공하지 않는다.
|
|
|
|
```java
|
|
ApacheHttpClient nativeApacheClient();
|
|
HttpClient nativeJdkClient();
|
|
reactor.netty.http.client.HttpClient nativeReactorClient();
|
|
WebClient.Builder mutableBuilder();
|
|
RestClient.Builder mutableBuilder();
|
|
```
|
|
|
|
Native 구성은 `TransportProvider` 구현 내부와 Experimental 모듈에서만 접근한다.
|
|
|
|
---
|
|
|
|
## 10. Core 계약
|
|
|
|
### 10.1 Operation
|
|
|
|
```java
|
|
public record HttpOperation(
|
|
OperationName operationName,
|
|
HttpMethod method,
|
|
String uriTemplate,
|
|
Map<String, ?> uriVariables,
|
|
Map<String, List<String>> headers,
|
|
BodySource body,
|
|
OperationIdempotency idempotency,
|
|
Optional<IdempotencyKey> idempotencyKey,
|
|
Optional<Instant> deadline) {
|
|
}
|
|
```
|
|
|
|
`HttpMethod`는 플랫폼 enum을 사용한다. TRACE는 enum에 포함하지 않고 custom method descriptor도 사전 등록해야 한다.
|
|
|
|
### 10.2 BodySource
|
|
|
|
```java
|
|
public sealed interface BodySource permits
|
|
EmptyBody,
|
|
ObjectBody,
|
|
ByteArrayBody,
|
|
ReopenableStreamBody,
|
|
OneShotStreamBody {
|
|
|
|
BodyReplayability replayability();
|
|
OptionalLong knownLength();
|
|
}
|
|
|
|
public record ReopenableStreamBody(
|
|
IOSupplier<InputStream> opener,
|
|
OptionalLong knownLength,
|
|
String mediaType) implements BodySource {
|
|
@Override public BodyReplayability replayability() {
|
|
return BodyReplayability.REOPENABLE;
|
|
}
|
|
}
|
|
|
|
public record OneShotStreamBody(
|
|
InputStream stream,
|
|
OptionalLong knownLength,
|
|
String mediaType) implements BodySource {
|
|
@Override public BodyReplayability replayability() {
|
|
return BodyReplayability.ONE_SHOT;
|
|
}
|
|
}
|
|
```
|
|
|
|
Reactive body는 `httpclient-webclient`의 별도 타입을 사용한다.
|
|
|
|
```java
|
|
public record ReactiveBodySource(
|
|
Supplier<? extends Publisher<DataBuffer>> publisherFactory,
|
|
BodyReplayability replayability,
|
|
OptionalLong knownLength,
|
|
MediaType mediaType) {
|
|
}
|
|
```
|
|
|
|
`Publisher` instance 자체를 받는 API는 one-shot으로 간주한다. retry 가능한 body는 매 시도 새 publisher를 생성하는 factory를 요구한다.
|
|
|
|
### 10.3 ResponseType과 lifecycle
|
|
|
|
```java
|
|
public sealed interface ResponseType<T> permits
|
|
ClassResponseType,
|
|
GenericResponseType,
|
|
ByteArrayResponseType,
|
|
EmptyResponseType {
|
|
}
|
|
|
|
public interface BlockingStreamingResponse extends AutoCloseable {
|
|
HttpStatus status();
|
|
Map<String, List<String>> headers();
|
|
InputStream body();
|
|
@Override void close();
|
|
}
|
|
```
|
|
|
|
Streaming response는 반드시 `AutoCloseable`로 반환한다. `InputStream`만 단독 반환하지 않는다.
|
|
|
|
### 10.4 Result
|
|
|
|
```java
|
|
public record HttpCallResult<T>(
|
|
HttpStatus status,
|
|
Map<String, List<String>> headers,
|
|
T body,
|
|
int attempts,
|
|
Duration elapsed,
|
|
ExecutionEvidence evidence,
|
|
Optional<RemoteProblem> remoteProblem) {
|
|
}
|
|
```
|
|
|
|
2xx 이외 status를 result로 반환할지 예외로 변환할지는 operation policy가 결정한다. 기본 Typed Client는 4xx·5xx를 안정 예외로 변환하고 Generic Gateway는 `StatusHandlingPolicy`를 명시할 수 있다.
|
|
|
|
---
|
|
|
|
## 11. Named Client Profile
|
|
|
|
### 11.1 구성 모델
|
|
|
|
```yaml
|
|
http-clients:
|
|
payment:
|
|
mode: TRUSTED
|
|
base-url: https://payment.example.com
|
|
allowed-hosts: [payment.example.com]
|
|
allowed-ports: [443]
|
|
api: REST_CLIENT
|
|
transport: APACHE
|
|
protocols: [HTTP_2, HTTP_1_1]
|
|
|
|
pool:
|
|
max-total-connections: 100
|
|
max-connections-per-route: 50
|
|
max-pending-acquires: 200
|
|
pending-acquire-timeout: 200ms
|
|
max-idle-time: 30s
|
|
max-life-time: 5m
|
|
validate-after-inactivity: 5s
|
|
eviction-interval: 15s
|
|
|
|
timeout:
|
|
dns: 300ms
|
|
connect: 500ms
|
|
tls-handshake: 1s
|
|
proxy-connect: 500ms
|
|
request-write-idle: 1s
|
|
response-header: 2s
|
|
read-idle: 3s
|
|
total-call: 4s
|
|
streaming-idle: 30s
|
|
|
|
redirect:
|
|
enabled: false
|
|
max-hops: 0
|
|
allow-cross-origin: false
|
|
|
|
request:
|
|
max-body-bytes: 1048576
|
|
compression: false
|
|
|
|
response:
|
|
max-wire-bytes: 5242880
|
|
max-decoded-bytes: 10485760
|
|
allowed-content-types:
|
|
- application/json
|
|
- application/problem+json
|
|
|
|
authentication:
|
|
type: OAUTH2_CLIENT_CREDENTIALS
|
|
registration-id: payment
|
|
scopes: [payments.write]
|
|
audience: payment-api
|
|
|
|
retry:
|
|
policy: payment-write
|
|
max-attempts: 2
|
|
base-backoff: 50ms
|
|
max-backoff: 200ms
|
|
jitter: FULL
|
|
retry-after: HONOR
|
|
budget: payment
|
|
|
|
circuit-breaker: payment
|
|
bulkhead: payment
|
|
rate-limiter: payment-attempts
|
|
|
|
observability:
|
|
operation-name-required: true
|
|
full-url-recording: false
|
|
body-logging: false
|
|
```
|
|
|
|
위 숫자는 플랫폼 default가 아니라 `payment` profile의 명시적 예시다. production profile은 upstream SLO와 부하 계산 없이 숨은 기본값으로 생성되지 않는다.
|
|
|
|
### 11.2 startup validation
|
|
|
|
다음 조건은 startup 실패다.
|
|
|
|
- Trusted profile에 base URL이 없다.
|
|
- `http` scheme이 production profile에서 사용된다.
|
|
- base URL에 userinfo 또는 query가 포함된다.
|
|
- allowed host와 base URL host가 다르다.
|
|
- redirect가 활성화됐는데 max hops가 0이거나 cross-origin credential 정책이 없다.
|
|
- total call timeout이 connect 또는 response header timeout보다 짧다.
|
|
- max decoded bytes가 global hard maximum을 초과한다.
|
|
- JDK transport에 세밀한 pending queue 또는 route pool 보장을 요구한다.
|
|
- HTTP/3를 Stable profile에서 요청한다.
|
|
- Dynamic mode에 default OAuth, API key, Cookie가 설정된다.
|
|
- trust-all, hostname verification off, plaintext fallback이 설정된다.
|
|
- production에서 Simple request factory가 선택된다.
|
|
- POST retry policy가 idempotency 조건 없이 활성화된다.
|
|
|
|
### 11.3 Operation override
|
|
|
|
operation은 profile 값을 더 위험한 방향으로 넓힐 수 없다.
|
|
|
|
```text
|
|
허용:
|
|
- 더 짧은 total timeout
|
|
- 더 작은 response size
|
|
- retry 비활성화
|
|
- stricter content type
|
|
- streaming idle timeout 지정
|
|
|
|
금지:
|
|
- 더 긴 timeout
|
|
- 더 큰 body limit
|
|
- 다른 host
|
|
- 다른 credential
|
|
- cross-origin redirect 활성화
|
|
- non-idempotent retry 강제
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Target·URI·Header 정책
|
|
|
|
### 12.1 Trusted target
|
|
|
|
Trusted profile은 startup에 다음을 검증한다.
|
|
|
|
- URI strict parsing
|
|
- scheme, host, port
|
|
- IDNA canonical host
|
|
- userinfo 없음
|
|
- path base normalization
|
|
- allowed host·port 일치
|
|
- production TLS policy
|
|
|
|
H2 호출자는 상대 URI template만 전달한다. `URI` absolute 값이 들어오면 거부한다.
|
|
|
|
### 12.2 URI encoding
|
|
|
|
- path와 query를 문자열로 연결하지 않는다.
|
|
- template variable은 component별로 encode한다.
|
|
- 이미 인코딩된 값과 raw 값을 동일 API에서 혼용하지 않는다.
|
|
- query value의 민감정보는 log와 trace에서 제거한다.
|
|
- 국제화 host는 Punycode canonical form으로 allowlist와 비교한다.
|
|
- IPv4-mapped IPv6를 원래 IPv4로 정규화한다.
|
|
|
|
### 12.3 Header
|
|
|
|
다음 header는 플랫폼이 소유한다.
|
|
|
|
```text
|
|
Authorization
|
|
Proxy-Authorization
|
|
Host
|
|
Content-Length
|
|
Transfer-Encoding
|
|
Traceparent
|
|
Tracestate
|
|
Baggage
|
|
Cookie (profile opt-in일 때)
|
|
```
|
|
|
|
호출자가 임의로 덮어쓰지 못한다. `Idempotency-Key`는 operation descriptor가 요구할 때만 허용한다. header name·value에 CR 또는 LF가 있으면 요청 전 거부한다.
|
|
|
|
### 12.4 Redirect
|
|
|
|
기본값은 비활성이다.
|
|
|
|
| 상태 | 기본 정책 |
|
|
|---|---|
|
|
| 301, 302, 303 | 자동 method 변환을 신뢰하지 않고 operation별로 명시한다. |
|
|
| 307, 308 | method와 body를 보존하므로 body replayable일 때만 허용한다. |
|
|
| same-origin | max hop과 method 정책 안에서 선택 허용한다. |
|
|
| cross-origin | 기본 거부한다. 허용 시 Authorization, Cookie, API key를 제거한다. |
|
|
| Dynamic Target | 각 hop을 새로운 target으로 canonicalize·resolve·IP 검증한다. |
|
|
|
|
---
|
|
|
|
## 13. Transport SPI
|
|
|
|
### 13.1 Blocking provider
|
|
|
|
```java
|
|
public interface BlockingTransportProvider {
|
|
TransportId id();
|
|
BlockingTransportCapabilities capabilities();
|
|
ClientHttpRequestFactory create(
|
|
ClientProfile profile,
|
|
TransportLifecycleListener listener);
|
|
TransportFailureClassifier failureClassifier();
|
|
}
|
|
```
|
|
|
|
### 13.2 Reactive provider
|
|
|
|
```java
|
|
public interface ReactiveTransportProvider {
|
|
TransportId id();
|
|
ReactiveTransportCapabilities capabilities();
|
|
ClientHttpConnector create(
|
|
ClientProfile profile,
|
|
TransportLifecycleListener listener);
|
|
TransportFailureClassifier failureClassifier();
|
|
}
|
|
```
|
|
|
|
### 13.3 공통 원칙
|
|
|
|
- provider는 native client를 반환하지 않는다.
|
|
- capability가 profile 요구사항보다 약하면 startup에 실패한다.
|
|
- transport exception은 public API에 직접 노출하지 않는다.
|
|
- transport가 `NOT_SENT`를 증명할 수 없으면 `SENT_NO_RESPONSE` 또는 보수적 unknown reason으로 분류한다.
|
|
- response body를 소비·close하지 않은 경우 connection 재사용 여부를 명시한다.
|
|
- client runtime 종료 시 신규 retry를 금지하고 진행 호출을 drain한다.
|
|
|
|
### 13.4 Apache profile
|
|
|
|
- 전체·route별 connection 제한
|
|
- pending acquire timeout
|
|
- max idle, max lifetime
|
|
- validate after inactivity
|
|
- background eviction
|
|
- proxy와 CONNECT
|
|
- custom TLS strategy
|
|
- HTTP/1.1·2
|
|
- blocking response lifecycle
|
|
|
|
### 13.5 JDK profile
|
|
|
|
- 의존성 최소화 profile
|
|
- HTTP/1.1·2
|
|
- sync send 기반
|
|
- 세밀한 pool queue·route limit을 요구하지 않는 경우만 사용
|
|
- Dynamic Target Stable에서 제외
|
|
- streaming body close·cancel contract 검증
|
|
|
|
### 13.6 Reactor Netty profile
|
|
|
|
- provider를 upstream별로 분리한다.
|
|
- max connections, pending acquire, idle, lifetime, eviction을 설정한다.
|
|
- DNS, connect, TLS, proxy, response timeout을 stage별로 계측한다.
|
|
- event-loop에서 blocking codec·file I/O를 금지한다.
|
|
- cancellation에서 inbound buffer를 release하고 connection을 반환 또는 폐기한다.
|
|
|
|
### 13.7 Jetty HTTP/3
|
|
|
|
- feature flag와 별도 module이 필요하다.
|
|
- Stable starter가 자동 구성하지 않는다.
|
|
- QUIC native dependency와 TLS 1.3을 요구한다.
|
|
- HTTP/3 failure를 공통 evidence로 변환하는 contract suite를 통과해야 Beta로 승격한다.
|
|
|
|
---
|
|
|
|
## 14. Connection Pool과 동시성
|
|
|
|
### 14.1 pool과 bulkhead 분리
|
|
|
|
HTTP/1.1은 connection과 in-flight 요청 수가 가까울 수 있지만 HTTP/2는 하나의 connection에 여러 stream을 multiplex한다. 따라서 다음을 독립 설정으로 둔다.
|
|
|
|
```text
|
|
connection pool limit
|
|
pending acquire queue limit
|
|
HTTP/2 stream capacity
|
|
logical admission limit
|
|
attempt bulkhead concurrency
|
|
```
|
|
|
|
### 14.2 pool 설정
|
|
|
|
| 설정 | 의미 |
|
|
|---|---|
|
|
| `maxTotalConnections` | runtime 전체 socket 상한 |
|
|
| `maxConnectionsPerRoute` | 한 upstream route 상한 |
|
|
| `maxPendingAcquires` | 대기 요청 메모리 상한 |
|
|
| `pendingAcquireTimeout` | pool·stream 대기 상한 |
|
|
| `maxIdleTime` | 유휴 연결 제거 |
|
|
| `maxLifeTime` | DNS·LB 변경과 인증서 rotation 반영 |
|
|
| `validateAfterInactivity` | stale·half-open 연결 검사 |
|
|
| `evictionInterval` | background cleanup |
|
|
| `shutdownTimeout` | drain 후 강제 종료 시각 |
|
|
|
|
### 14.3 DNS와 기존 연결
|
|
|
|
DNS TTL만으로 pooled connection이 새 IP로 전환된다고 가정하지 않는다. `maxLifeTime`과 eviction을 함께 사용하고, DNS 변경 contract test에서 일정 시간 내 새 endpoint로 전환되는지 확인한다.
|
|
|
|
---
|
|
|
|
## 15. Timeout과 Deadline
|
|
|
|
### 15.1 단계별 timeout
|
|
|
|
| 타입 | 시작과 종료 |
|
|
|---|---|
|
|
| DNS | hostname resolve 시작부터 결과 |
|
|
| Pool Acquire | queue 진입부터 connection 또는 stream 확보 |
|
|
| Connect | socket connect 시작부터 성공 |
|
|
| TLS Handshake | TCP 이후 TLS·ALPN 완료 |
|
|
| Proxy Connect | proxy socket 또는 CONNECT 완료 |
|
|
| Request Write Idle | request chunk 진행이 없는 시간 |
|
|
| Response Header | request 전송 후 final header 수신까지 |
|
|
| Read Idle | response chunk 사이 무진행 시간 |
|
|
| Total Call | 최초 논리 호출부터 모든 retry·backoff 종료까지 |
|
|
| Streaming Idle | 장기 stream event 사이 무진행 시간 |
|
|
| Shutdown | runtime drain 시작부터 강제 종료까지 |
|
|
|
|
### 15.2 effective deadline
|
|
|
|
```text
|
|
effectiveDeadline = min(parentDeadline, now + profile.totalCallTimeout)
|
|
remaining = effectiveDeadline - now - safetyMargin
|
|
attemptBudget = remaining - plannedBackoff - cleanupReserve
|
|
```
|
|
|
|
다음이면 새 attempt를 시작하지 않는다.
|
|
|
|
- `remaining <= minimumAttemptBudget`
|
|
- 다음 backoff 이후 attempt budget이 없다.
|
|
- body가 replayable하지 않다.
|
|
- ambiguous execution이고 operation이 안전하지 않다.
|
|
- retry budget이 고갈됐다.
|
|
- circuit이 open이다.
|
|
- runtime이 draining 상태다.
|
|
|
|
### 15.3 Streaming
|
|
|
|
Streaming은 연결 설정 단계와 연결 유지 단계를 분리한다.
|
|
|
|
```text
|
|
setupDeadline
|
|
→ response headers 수신
|
|
→ streamingIdleTimeout
|
|
→ optional maxStreamDuration
|
|
```
|
|
|
|
일반 total timeout을 SSE 전체 수명에 적용하지 않는다.
|
|
|
|
---
|
|
|
|
## 16. 실행 증거
|
|
|
|
### 16.1 public evidence
|
|
|
|
| Evidence | 의미 | 예 |
|
|
|---|---|---|
|
|
| `NOT_SENT` | 서버에 요청이 전달되지 않았음을 증명 | profile 거부, pool timeout, DNS 실패, connect 실패, request 전 TLS 실패 |
|
|
| `SENT_NO_RESPONSE` | 일부 또는 전체 요청을 보냈으나 final header를 받지 못함 | partial write, response header timeout, reset |
|
|
| `RESPONSE_RECEIVED` | final HTTP header를 받음 | 2xx, 4xx, 5xx, redirect |
|
|
| `PARTIAL_RESPONSE` | header와 body 일부를 받음 | decode 중 reset, streaming 중단 |
|
|
|
|
### 16.2 stage
|
|
|
|
```java
|
|
public enum AttemptStage {
|
|
VALIDATION,
|
|
AUTHENTICATION,
|
|
POOL_ACQUIRE,
|
|
DNS,
|
|
CONNECT,
|
|
TLS_HANDSHAKE,
|
|
PROXY_CONNECT,
|
|
REQUEST_HEADERS,
|
|
REQUEST_BODY,
|
|
RESPONSE_HEADERS,
|
|
RESPONSE_BODY,
|
|
COMPLETE
|
|
}
|
|
```
|
|
|
|
### 16.3 보수적 분류
|
|
|
|
- `NOT_SENT`는 증명 가능한 stage 실패에서만 사용한다.
|
|
- engine generic I/O exception은 false `NOT_SENT`로 만들지 않는다.
|
|
- request body write가 시작됐으면 기본 `SENT_NO_RESPONSE`다.
|
|
- response header를 받았으면 status와 무관하게 `RESPONSE_RECEIVED`다.
|
|
- body 일부가 application에 전달됐으면 `PARTIAL_RESPONSE`다.
|
|
- HTTP/2 `REFUSED_STREAM`과 GOAWAY last-stream-id는 내부 protocol evidence로 보존하고 안전한 경우 `NOT_SENT`에 준해 retry한다.
|
|
|
|
---
|
|
|
|
## 17. Retry
|
|
|
|
### 17.1 판정 입력
|
|
|
|
```java
|
|
public record RetryContext(
|
|
OperationIdempotency idempotency,
|
|
Optional<IdempotencyKey> idempotencyKey,
|
|
BodyReplayability replayability,
|
|
ExecutionEvidence evidence,
|
|
FailureCategory failureCategory,
|
|
Optional<HttpStatus> responseStatus,
|
|
Optional<Duration> retryAfter,
|
|
int attempt,
|
|
Duration remainingDeadline,
|
|
RetryBudgetSnapshot budget) {
|
|
}
|
|
```
|
|
|
|
### 17.2 판정 결과
|
|
|
|
```java
|
|
public sealed interface RetryDecision permits
|
|
RetryAllowed,
|
|
RetryDenied,
|
|
AmbiguousFailure {
|
|
}
|
|
```
|
|
|
|
### 17.3 기본 규칙
|
|
|
|
| 상황 | 기본 판정 |
|
|
|---|---|
|
|
| validation·auth configuration failure | retry 금지 |
|
|
| pool·DNS·connect failure | body 재생 가능하고 deadline·budget이 있으면 허용 |
|
|
| certificate·hostname failure | retry 금지 |
|
|
| request body 일부 송신 | standard 또는 contract idempotent가 아니면 ambiguous |
|
|
| response header timeout | read-only 또는 idempotency contract가 있을 때만 허용 |
|
|
| 408 | replayability·deadline 조건으로 제한 |
|
|
| 425 | early data 없이 한 번만 제한 retry |
|
|
| 429 | `Retry-After`, deadline, budget 내에서 허용 |
|
|
| 500 | 기본 금지, upstream policy가 transient로 등록한 경우만 |
|
|
| 502·503·504 | 안전한 operation에 제한 허용 |
|
|
| 401 | credential invalidation 후 최대 1회, 안전한 body와 operation만 |
|
|
| partial response | application 전달 전 read-only buffering에서만 제한 |
|
|
| one-shot body | retry 금지 |
|
|
| first byte delivered | retry 금지 |
|
|
|
|
### 17.4 Retry budget
|
|
|
|
upstream별 token bucket을 사용한다.
|
|
|
|
```text
|
|
원 요청 성공·실패 수에 비례한 retry token 공급
|
|
물리 retry마다 token 소비
|
|
budget 고갈 시 즉시 최종 실패
|
|
```
|
|
|
|
metric은 logical call 수와 physical attempt 수를 분리한다.
|
|
|
|
### 17.5 Backoff
|
|
|
|
- exponential backoff
|
|
- full 또는 decorrelated jitter
|
|
- max backoff
|
|
- `Retry-After` 상한
|
|
- deadline보다 긴 대기 금지
|
|
- backoff 중 bulkhead permit과 connection을 보유하지 않음
|
|
|
|
---
|
|
|
|
## 18. Resilience 실행 순서
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
A[Operation Validation] --> B[Effective Deadline]
|
|
B --> C[Authentication]
|
|
C --> D[Logical Admission]
|
|
D --> E[Retry Coordinator]
|
|
E --> F{Circuit Open?}
|
|
F -- Yes --> X[Fail Fast]
|
|
F -- No --> G[Attempt Rate Limiter]
|
|
G --> H[Attempt Bulkhead]
|
|
H --> I[HTTP Attempt]
|
|
I --> J[Evidence Classification]
|
|
J --> K{Retry Safe?}
|
|
K -- Yes --> L[Backoff + Jitter]
|
|
L --> E
|
|
K -- No --> M[Result or Stable Error]
|
|
```
|
|
|
|
### 18.1 역할
|
|
|
|
| 기능 | 보호 대상 |
|
|
|---|---|
|
|
| Logical admission | retry coordinator와 대기 객체의 과도한 생성 |
|
|
| Circuit Breaker | 실패하거나 느린 upstream 호출 |
|
|
| Attempt Rate Limiter | 외부 API의 물리 요청 quota |
|
|
| Attempt Bulkhead | in-flight 물리 요청과 thread·stream capacity |
|
|
| Retry Budget | 장애 중 추가 요청 총량 |
|
|
| Total Deadline | 사용자 요청의 전체 시간 예산 |
|
|
|
|
### 18.2 Blocking과 Reactive
|
|
|
|
- Blocking Apache/JDK는 semaphore 또는 bounded executor bulkhead를 사용한다.
|
|
- Reactive는 event-loop를 thread-pool bulkhead로 감싸지 않고 semaphore concurrency를 사용한다.
|
|
- blocking token acquisition이나 secret load는 event-loop에서 실행하지 않는다.
|
|
|
|
---
|
|
|
|
## 19. 오류 모델
|
|
|
|
```text
|
|
HttpClientException
|
|
├─ HttpConfigurationException
|
|
├─ HttpTargetRejectedException
|
|
├─ HttpDnsException
|
|
├─ HttpPoolAcquireTimeoutException
|
|
├─ HttpConnectException
|
|
├─ HttpProxyException
|
|
├─ HttpTlsException
|
|
├─ HttpRequestWriteException
|
|
├─ HttpResponseTimeoutException
|
|
├─ HttpResponseTruncatedException
|
|
├─ HttpRemoteErrorException
|
|
├─ HttpProblemDetailException
|
|
├─ HttpRedirectRejectedException
|
|
├─ HttpAuthenticationException
|
|
├─ HttpSerializationException
|
|
├─ HttpResponseTooLargeException
|
|
├─ HttpDeadlineExceededException
|
|
├─ HttpCircuitOpenException
|
|
├─ HttpBulkheadRejectedException
|
|
├─ HttpRateLimitRejectedException
|
|
└─ HttpAmbiguousExecutionException
|
|
```
|
|
|
|
### 19.1 공통 metadata
|
|
|
|
```java
|
|
public record HttpFailureMetadata(
|
|
ClientProfileName clientName,
|
|
OperationName operationName,
|
|
HttpMethod method,
|
|
String uriTemplate,
|
|
ExecutionEvidence evidence,
|
|
BodyReplayability replayability,
|
|
AttemptStage stage,
|
|
boolean retryable,
|
|
int attempt,
|
|
Duration elapsed,
|
|
Duration remainingDeadline,
|
|
Optional<HttpStatus> status,
|
|
Optional<String> traceId) {
|
|
}
|
|
```
|
|
|
|
다음은 예외 message나 public metadata에 포함하지 않는다.
|
|
|
|
- 전체 URL
|
|
- query value
|
|
- 실제 path variable
|
|
- request·response body
|
|
- Authorization, Cookie, API key
|
|
- idempotency key 원문
|
|
- client secret
|
|
- resolved IP의 metric label
|
|
|
|
### 19.2 RFC 9457
|
|
|
|
`application/problem+json`은 다음 필드를 제한 크기로 보존한다.
|
|
|
|
```text
|
|
type
|
|
title
|
|
status
|
|
detail
|
|
instance
|
|
등록된 extension allowlist
|
|
```
|
|
|
|
HTTP response status가 authoritative다. body의 `status`로 실제 status를 덮어쓰지 않는다. `detail`, `instance`, extension은 log에 기본 기록하지 않는다.
|
|
|
|
---
|
|
|
|
## 20. 인증
|
|
|
|
### 20.1 지원 방식
|
|
|
|
| 방식 | 등급 | 정책 |
|
|
|---|---:|---|
|
|
| None | Stable | 명시 profile |
|
|
| Basic | 제한 | TLS 필수, secret provider |
|
|
| API Key Header | Stable | header name allowlist |
|
|
| API Key Query | 승인 필요 | provider 요구 시만 |
|
|
| Static Bearer | 제한 | 짧은 TTL과 rotation |
|
|
| OAuth2 Client Credentials | Stable | M2M 기본 |
|
|
| Authorization Code authorized client | 지원 | principal을 명시 전달 |
|
|
| token relay | 제한 | audience·scope 확인 |
|
|
| Token Exchange | 선택 | audience 축소·delegation |
|
|
| mTLS | Stable | TLS identity profile |
|
|
| Request Signing | SPI | provider별 module |
|
|
| Proxy Authentication | Stable | target auth와 분리 |
|
|
|
|
### 20.2 credential provider
|
|
|
|
```java
|
|
public interface RequestCredentialProvider {
|
|
CredentialType type();
|
|
RequestCredentials resolve(CredentialRequest request);
|
|
}
|
|
|
|
public interface ReactiveRequestCredentialProvider {
|
|
CredentialType type();
|
|
Mono<RequestCredentials> resolve(CredentialRequest request);
|
|
}
|
|
```
|
|
|
|
### 20.3 OAuth2 token cache
|
|
|
|
cache key는 다음을 포함한다.
|
|
|
|
```text
|
|
registrationId
|
|
principalClass
|
|
scopeSet
|
|
audience
|
|
tenantBoundary
|
|
mTLSCertificateIdentity
|
|
```
|
|
|
|
동일 key의 refresh는 single-flight로 수행한다. token endpoint는 target upstream과 별도 Named Client Profile을 사용한다.
|
|
|
|
### 20.4 401 재호출
|
|
|
|
- token을 한 번 invalidate한다.
|
|
- refresh 후 최대 한 번만 재호출한다.
|
|
- body가 replayable해야 한다.
|
|
- operation이 read-only이거나 인증 실패가 side effect 전 반환된다는 계약이 있어야 한다.
|
|
- one-shot upload와 ambiguous write에는 적용하지 않는다.
|
|
|
|
---
|
|
|
|
## 21. TLS와 인증서 rotation
|
|
|
|
### 21.1 허용
|
|
|
|
- TLS 1.2·1.3
|
|
- hostname verification
|
|
- JVM trust store
|
|
- profile별 custom CA
|
|
- profile별 client certificate
|
|
- mTLS
|
|
- SNI와 ALPN
|
|
- 새 runtime generation으로 certificate rotation
|
|
|
|
### 21.2 금지
|
|
|
|
- trust-all TrustManager
|
|
- hostname verification 비활성화
|
|
- 인증서 오류 무시
|
|
- production self-signed 자동 신뢰
|
|
- HTTPS 실패 후 HTTP fallback
|
|
- key material의 config file·log 기록
|
|
|
|
### 21.3 오류 분류
|
|
|
|
| 오류 | retry |
|
|
|---|---:|
|
|
| unknown CA | 금지 |
|
|
| hostname mismatch | 금지 |
|
|
| expired certificate | 금지 |
|
|
| revoked certificate | 금지 |
|
|
| protocol mismatch | profile 오류로 금지 |
|
|
| transient handshake timeout | deadline과 policy 안에서 제한 |
|
|
| client certificate 없음 | 금지 |
|
|
|
|
---
|
|
|
|
## 22. Dynamic Target와 SSRF
|
|
|
|
### 22.1 처리 순서
|
|
|
|
```text
|
|
1. URI strict parse
|
|
2. scheme allowlist
|
|
3. userinfo·invalid port 거부
|
|
4. host IDNA canonicalization
|
|
5. host allowlist 또는 suffix policy
|
|
6. 모든 A·AAAA resolve
|
|
7. 각 주소를 canonical IP로 정규화
|
|
8. loopback, link-local, private, ULA, metadata 대역 검사
|
|
9. 검증한 주소로 실제 connection pinning
|
|
10. response size·content policy 적용
|
|
11. redirect마다 1~10을 반복
|
|
```
|
|
|
|
### 22.2 기본 금지 주소
|
|
|
|
- IPv4·IPv6 loopback
|
|
- link-local
|
|
- RFC1918 private address
|
|
- IPv6 ULA
|
|
- unspecified·multicast
|
|
- IPv4-mapped IPv6의 차단 대상
|
|
- cloud metadata endpoint
|
|
- 조직이 정의한 internal CIDR
|
|
|
|
### 22.3 transport 제한
|
|
|
|
Dynamic Target Stable은 validated resolver 또는 validated address pinning을 제공하는 Apache와 Reactor Netty에서 먼저 지원한다. JDK와 Jetty는 동일 보장을 contract test로 증명하기 전까지 H3에서 사용할 수 없다.
|
|
|
|
### 22.4 redirect credential
|
|
|
|
origin이 변경되면 다음을 제거한다.
|
|
|
|
```text
|
|
Authorization
|
|
Proxy-Authorization
|
|
Cookie
|
|
API key header
|
|
custom sensitive header
|
|
```
|
|
|
|
Dynamic profile에는 Cookie Jar를 기본 생성하지 않는다.
|
|
|
|
### 22.5 네트워크 계층
|
|
|
|
애플리케이션 검증만으로 충분하다고 간주하지 않는다. Kubernetes NetworkPolicy, service mesh egress, firewall, proxy ACL 중 하나 이상의 네트워크 제어를 운영 완료 조건으로 요구한다.
|
|
|
|
---
|
|
|
|
## 23. Streaming과 대용량 Body
|
|
|
|
### 23.1 request replayability
|
|
|
|
| Body | Replayability |
|
|
|---|---|
|
|
| immutable `byte[]` | REPLAYABLE |
|
|
| DTO + deterministic codec | REPLAYABLE |
|
|
| reopenable file/resource supplier | REOPENABLE |
|
|
| one `InputStream` instance | ONE_SHOT |
|
|
| publisher factory | 선언값에 따름 |
|
|
| publisher instance | ONE_SHOT |
|
|
| multipart | 가장 약한 part와 동일 |
|
|
|
|
### 23.2 response lifecycle
|
|
|
|
- blocking stream은 `AutoCloseable` response wrapper로 반환한다.
|
|
- reactive body는 consume, cancel, error에서 buffer를 release한다.
|
|
- content length를 신뢰하지 않고 실제 wire bytes와 decoded bytes를 측정한다.
|
|
- gzip·deflate 응답은 압축 전후 상한을 각각 적용한다.
|
|
- decode error와 size 초과에서도 connection을 회수하거나 명시적으로 폐기한다.
|
|
|
|
### 23.3 first-byte boundary
|
|
|
|
```text
|
|
response header 수신
|
|
→ 내부 buffer에 아직 byte 미전달
|
|
→ read-only operation은 제한 retry 가능
|
|
→ application InputStream read 또는 Flux onNext 발생
|
|
→ transparent retry 영구 금지
|
|
```
|
|
|
|
### 23.4 SSE
|
|
|
|
```java
|
|
public interface ReactiveSseGateway {
|
|
<T> Flux<ServerSentEvent<T>> connect(
|
|
ClientProfileName profileName,
|
|
SseOperation operation,
|
|
ResponseType<T> eventType);
|
|
}
|
|
```
|
|
|
|
- setup deadline
|
|
- streaming idle timeout
|
|
- `Last-Event-ID` 재연결은 operation opt-in
|
|
- reconnect에도 retry budget 적용
|
|
- application cancel 시 connection close
|
|
|
|
---
|
|
|
|
## 24. HTTP protocol 세부 정책
|
|
|
|
### 24.1 HTTP/2
|
|
|
|
- connection 수와 stream concurrency를 분리한다.
|
|
- max concurrent streams를 metric으로 노출한다.
|
|
- `REFUSED_STREAM`은 peer 미처리 증거로 제한 retry할 수 있다.
|
|
- GOAWAY의 last stream ID 이후 요청만 peer 미처리로 분류한다.
|
|
- stream reset 원인을 stable failure category로 변환한다.
|
|
- connection coalescing은 host·certificate·security policy를 검증한 profile에서만 허용한다.
|
|
|
|
### 24.2 HTTP/3
|
|
|
|
- TLS 1.3 필수
|
|
- UDP·QUIC 네트워크 경로 테스트
|
|
- proxy·egress 지원 별도 매트릭스
|
|
- Stable H1/H2 API의 result·error semantic을 재사용
|
|
- 별도 `experimental=true`와 startup acknowledgment 요구
|
|
|
|
### 24.3 Proxy
|
|
|
|
- target auth와 proxy auth를 분리한다.
|
|
- proxy connect timeout을 별도 metric으로 기록한다.
|
|
- HTTPS CONNECT 실패를 target TLS 실패로 오분류하지 않는다.
|
|
- `NO_PROXY` 환경변수가 production allowlist를 우회하지 못하게 한다.
|
|
- service mesh retry가 활성화되면 application retry owner 검사를 수행한다.
|
|
|
|
---
|
|
|
|
## 25. 관측성
|
|
|
|
### 25.1 metric
|
|
|
|
| 이름 | 의미 |
|
|
|---|---|
|
|
| `http.client.requests` | 물리 attempt timer |
|
|
| `http.client.logical.calls` | 사용자 논리 호출 timer |
|
|
| `http.client.attempts` | attempt counter |
|
|
| `http.client.retry.count` | retry 이유별 수 |
|
|
| `http.client.retry.exhausted` | retry 소진 |
|
|
| `http.client.ambiguous` | 결과 모호성 |
|
|
| `http.client.timeout` | timeout stage |
|
|
| `http.client.request.bytes` | request wire bytes |
|
|
| `http.client.response.bytes` | response wire·decoded bytes |
|
|
| `http.client.active` | 진행 중 attempt |
|
|
| `http.client.pool.connections` | active·idle connection |
|
|
| `http.client.pool.pending` | pool 대기 |
|
|
| `http.client.pool.acquire.duration` | pool 대기 시간 |
|
|
| `http.client.dns.duration` | DNS 시간 |
|
|
| `http.client.connect.duration` | connect 시간 |
|
|
| `http.client.tls.duration` | TLS 시간 |
|
|
| `http.client.circuit.state` | circuit 상태 |
|
|
| `http.client.bulkhead.rejected` | bulkhead 거절 |
|
|
| `http.client.rate_limit.rejected` | local rate 거절 |
|
|
| `http.client.oauth.refresh` | token refresh 결과 |
|
|
| `http.client.ssrf.rejected` | dynamic target 거절 |
|
|
|
|
### 25.2 low-cardinality tag
|
|
|
|
허용:
|
|
|
|
```text
|
|
clientName
|
|
operationName
|
|
method
|
|
uriTemplate
|
|
status
|
|
outcome
|
|
transport
|
|
protocol
|
|
timeoutType
|
|
retryReason
|
|
evidence
|
|
circuitState
|
|
```
|
|
|
|
금지:
|
|
|
|
```text
|
|
full URL
|
|
query parameter
|
|
path variable value
|
|
user ID
|
|
tenant ID 원문
|
|
resolved IP
|
|
API key
|
|
token
|
|
Cookie
|
|
idempotency key
|
|
request·response body
|
|
exception message
|
|
```
|
|
|
|
### 25.3 trace
|
|
|
|
```text
|
|
http.client.operation logical internal span
|
|
└─ http.client.request attempt 1 CLIENT span
|
|
└─ http.client.request attempt 2 CLIENT span
|
|
```
|
|
|
|
- W3C Trace Context
|
|
- Baggage allowlist
|
|
- Dynamic target는 기본 trace propagation off
|
|
- retry reason과 evidence를 span event로 기록
|
|
- credential과 remote error body는 attribute에 기록하지 않음
|
|
|
|
### 25.4 logging
|
|
|
|
- 시도마다 WARN을 남기지 않는다.
|
|
- 최종 실패 한 번을 구조화 로그로 남긴다.
|
|
- retry attempt는 DEBUG 또는 trace event다.
|
|
- URL은 template과 profile name만 남긴다.
|
|
- body logging은 production에서 off다.
|
|
- header는 이름 allowlist, 값은 redaction policy를 적용한다.
|
|
|
|
---
|
|
|
|
## 26. Spring 통합
|
|
|
|
### 26.1 RestClient
|
|
|
|
- profile마다 immutable RestClient를 생성한다.
|
|
- Apache 또는 JDK request factory를 선택한다.
|
|
- default header는 credential과 trace보다 먼저 고정하지 않는다.
|
|
- request interceptor는 operation context와 attempt context를 읽는다.
|
|
- response extractor는 body lifecycle과 size를 통제한다.
|
|
|
|
### 26.2 WebClient
|
|
|
|
- profile마다 immutable WebClient를 생성한다.
|
|
- Reactor Netty provider를 upstream별로 분리한다.
|
|
- filter chain은 context에서 operation metadata를 가져온다.
|
|
- body cancel·discard hook을 등록한다.
|
|
- `.block()`을 public API 내부에서 호출하지 않는다.
|
|
|
|
### 26.3 HTTP Service Client
|
|
|
|
`HttpServiceRegistry`는 다음 작업을 수행한다.
|
|
|
|
1. interface annotation scan
|
|
2. method descriptor 생성
|
|
3. signature validation
|
|
4. RestClient 또는 WebClient proxy 생성
|
|
5. operation context wrapper proxy 생성
|
|
6. blocking은 try/finally로 context 제거
|
|
7. reactive는 Reactor Context에 descriptor 주입
|
|
|
|
### 26.4 Spring 7 Service Group
|
|
|
|
Spring 7 전용 모듈은 여러 service interface가 같은 profile을 공유하도록 group integration을 제공한다. 공통 계약과 profile validation은 그대로 재사용한다.
|
|
|
|
### 26.5 RestTemplate migration
|
|
|
|
Migration module은 다음만 제공한다.
|
|
|
|
- 기존 request factory와 message converter를 조사하는 audit 도구
|
|
- RestTemplate에서 RestClient builder로 이전하는 adapter
|
|
- deprecated usage report
|
|
- 동일 동작 contract test
|
|
|
|
신규 retry, Dynamic URL, HTTP/3 기능은 RestTemplate 경로에 추가하지 않는다.
|
|
|
|
---
|
|
|
|
## 27. Spring Boot Starter
|
|
|
|
### 27.1 auto-configuration
|
|
|
|
```text
|
|
HttpClientProfileAutoConfiguration
|
|
HttpClientTransportAutoConfiguration
|
|
HttpClientResilienceAutoConfiguration
|
|
HttpClientAuthenticationAutoConfiguration
|
|
HttpClientSecurityAutoConfiguration
|
|
HttpClientObservationAutoConfiguration
|
|
HttpServiceClientAutoConfiguration
|
|
DynamicTargetAutoConfiguration
|
|
```
|
|
|
|
### 27.2 startup guard
|
|
|
|
- production unsafe TLS 설정 탐지
|
|
- Simple factory 차단
|
|
- profile capability mismatch
|
|
- duplicate client name
|
|
- operation name duplicate
|
|
- H1 interface annotation 누락
|
|
- POST/PATCH idempotency 누락
|
|
- Dynamic credential 상속
|
|
- unsupported HTTP/3 Stable 설정
|
|
- response size hard max 위반
|
|
- retry owner 중복 선언
|
|
|
|
### 27.3 actuator
|
|
|
|
관리 endpoint는 값 원문을 숨기고 다음만 제공한다.
|
|
|
|
```text
|
|
profile name
|
|
runtime generation
|
|
transport
|
|
protocol
|
|
pool state
|
|
circuit state
|
|
credential type
|
|
TLS profile ID
|
|
last reload outcome
|
|
capability warnings
|
|
```
|
|
|
|
base URL 전체, credential, trust store path, resolved IP는 공개하지 않는다.
|
|
|
|
---
|
|
|
|
## 28. 테스트 전략
|
|
|
|
### 28.1 test topology
|
|
|
|
| 도구 | 용도 |
|
|
|---|---|
|
|
| MockWebServer | deterministic request·response contract |
|
|
| WireMock | stateful status, redirect, OAuth fixture |
|
|
| Toxiproxy | latency, reset, bandwidth, half-open |
|
|
| TLS test server | CA, hostname, expiry, mTLS |
|
|
| HTTP/2 server | GOAWAY, REFUSED_STREAM, reset |
|
|
| Forward proxy | CONNECT, auth, target failure |
|
|
| OAuth2 server | token expiry, refresh race, rotation |
|
|
| Testcontainers | isolated proxy·server runtime |
|
|
| BlockHound | event-loop blocking 검출 |
|
|
| ArchUnit | module·native type 경계 |
|
|
|
|
### 28.2 계약 테스트
|
|
|
|
- method와 URI encoding
|
|
- header ownership과 CRLF 차단
|
|
- JSON·XML·form·multipart
|
|
- empty, generic, streaming response
|
|
- redirect 301·302·303·307·308
|
|
- compression과 decoded size
|
|
- conditional request와 Range
|
|
- HTTP/1.1·2
|
|
- Apache·JDK·Reactor 공통 result·error semantic
|
|
|
|
### 28.3 timeout·failure
|
|
|
|
- DNS timeout
|
|
- pool saturation
|
|
- connect refused·blackhole
|
|
- TLS timeout·trust·hostname
|
|
- slow request receiver
|
|
- response header delay
|
|
- body idle
|
|
- total deadline
|
|
- streaming idle
|
|
- shutdown 중 신규 retry 금지
|
|
|
|
### 28.4 retry·resilience
|
|
|
|
- GET connect failure
|
|
- PUT body replay
|
|
- POST idempotency key 있음·없음
|
|
- partial write
|
|
- 408, 425, 429, 500, 502, 503, 504
|
|
- Retry-After
|
|
- budget exhaustion
|
|
- circuit half-open
|
|
- rate limiter와 retry attempt 수
|
|
- bulkhead permit 반환
|
|
- service mesh 중복 retry configuration guard
|
|
|
|
### 28.5 security
|
|
|
|
- loopback, private, link-local, ULA, metadata
|
|
- IPv4-mapped IPv6
|
|
- IDNA host
|
|
- DNS rebinding
|
|
- public→private redirect
|
|
- Authorization·Cookie leakage
|
|
- trust-all bean startup failure
|
|
- hostname mismatch
|
|
- mTLS certificate 없음·rotation
|
|
- CRLF header
|
|
- compressed bomb
|
|
- JSON nesting·XML entity
|
|
|
|
### 28.6 streaming lifecycle
|
|
|
|
- response 미소비
|
|
- partial read 후 close
|
|
- decode failure
|
|
- size limit
|
|
- reactive cancel
|
|
- DataBuffer release
|
|
- slow subscriber backpressure
|
|
- first byte 이후 retry 없음
|
|
- SSE idle와 Last-Event-ID reconnect
|
|
- event-loop blocking 없음
|
|
|
|
### 28.7 observability
|
|
|
|
- logical call 1, attempt N
|
|
- retry reason
|
|
- evidence
|
|
- URI template cardinality
|
|
- 전체 URL label 없음
|
|
- token·API key 마스킹
|
|
- dynamic target trace propagation off
|
|
|
|
### 28.8 성능
|
|
|
|
- pool·bulkhead saturation
|
|
- HTTP/2 stream saturation
|
|
- 대용량 upload·download
|
|
- gzip decoded size
|
|
- concurrent OAuth refresh
|
|
- runtime generation rotation
|
|
- shutdown drain
|
|
- heap·direct memory·thread 상한
|
|
|
|
---
|
|
|
|
## 29. 호환성 인증 매트릭스
|
|
|
|
| 프로파일 | CI 빈도 | 릴리스 Gate |
|
|
|---|---|---|
|
|
| Spring Framework 6.2 latest patch | 모든 PR·release | 필수 |
|
|
| Spring Framework 7.0 latest patch | release | 필수 |
|
|
| Apache HC5 + RestClient | 모든 PR | 필수 |
|
|
| JDK HttpClient + RestClient | 모든 PR | 필수 |
|
|
| Reactor Netty + WebClient | 모든 PR | 필수 |
|
|
| Jetty HTTP/3 | nightly | Experimental 비차단 |
|
|
| HTTP/1.1 | 모든 PR | 필수 |
|
|
| HTTP/2 | release | 필수 |
|
|
| Forward proxy | release | 지원 선언 시 필수 |
|
|
| OAuth2 Client Credentials | 모든 PR | 필수 |
|
|
| mTLS | release | 지원 선언 시 필수 |
|
|
| Dynamic Target Apache | security suite | 필수 |
|
|
| Dynamic Target Reactor | security suite | 필수 |
|
|
| Toxiproxy failure suite | nightly·release | 필수 |
|
|
|
|
---
|
|
|
|
## 30. 운영 설정과 기본 정책
|
|
|
|
### 30.1 숨은 운영 기본값 금지
|
|
|
|
production Named Client Profile은 다음을 명시해야 한다.
|
|
|
|
```text
|
|
base URL
|
|
transport
|
|
total timeout
|
|
response header timeout
|
|
pool 또는 concurrency limit
|
|
request·response body hard limit
|
|
authentication type
|
|
retry policy 또는 none
|
|
redirect policy
|
|
TLS profile
|
|
```
|
|
|
|
미설정 시 매우 큰 framework default로 조용히 동작하지 않고 startup에 실패한다.
|
|
|
|
### 30.2 retry owner
|
|
|
|
application HTTP Client, 외부 SDK, service mesh 중 하나만 retry owner가 된다. starter는 known mesh annotation 또는 설정을 읽어 중복 retry를 경고하거나 strict mode에서 실패시킨다.
|
|
|
|
### 30.3 shutdown
|
|
|
|
```text
|
|
runtime state RUNNING → DRAINING
|
|
신규 logical call 거부 또는 새 generation으로 routing
|
|
진행 attempt 완료
|
|
신규 retry 금지
|
|
shutdown timeout
|
|
남은 call cancel
|
|
pool close
|
|
```
|
|
|
|
---
|
|
|
|
## 31. 비지원 범위의 runtime 강제
|
|
|
|
문서만으로 금지하지 않고 다음 guard를 코드로 둔다.
|
|
|
|
| 비지원 | 강제 방식 |
|
|
|---|---|
|
|
| TRACE | method registry에서 부재·runtime reject |
|
|
| unrestricted absolute URL | H2 parser에서 reject |
|
|
| trust-all | bean·SSLContext validator startup fail |
|
|
| hostname verification off | transport capability validator fail |
|
|
| production Simple factory | environment guard fail |
|
|
| one-shot retry | Retry Eligibility Engine deny |
|
|
| partial stream retry | first-byte marker deny |
|
|
| full URL metric | observation convention test |
|
|
| Dynamic credential inheritance | configuration validator fail |
|
|
| RestTemplate 신규 기능 | module dependency·ArchUnit rule |
|
|
| native client exposure | public API signature ArchUnit rule |
|
|
| HTTP/3 Stable | profile validator fail |
|
|
|
|
---
|
|
|
|
## 32. 릴리스 단계
|
|
|
|
### 32.1 Core Alpha
|
|
|
|
- core types
|
|
- Named Client Profile
|
|
- stable exceptions
|
|
- transport SPI
|
|
- testkit
|
|
- deadline model
|
|
- target·header·body guard
|
|
|
|
완료 조건: core module의 public API와 configuration validation contract가 통과한다.
|
|
|
|
### 32.2 Blocking Beta
|
|
|
|
- Apache HC5
|
|
- JDK HttpClient
|
|
- RestClient Generic Gateway
|
|
- H1 blocking Typed Client
|
|
- connection pool·timeout
|
|
- error mapping
|
|
|
|
완료 조건: Apache와 JDK가 공통 Blocking contract suite를 통과한다.
|
|
|
|
### 32.3 Resilience RC
|
|
|
|
- execution evidence
|
|
- retry eligibility
|
|
- retry budget
|
|
- circuit·rate·bulkhead
|
|
- RFC 9457
|
|
- OAuth2·TLS
|
|
|
|
완료 조건: duplicate POST, partial write, 429, pool saturation, token refresh race가 통과한다.
|
|
|
|
### 32.4 Security Release
|
|
|
|
- Trusted target validation
|
|
- Dynamic Target Apache
|
|
- DNS/IP pinning
|
|
- redirect revalidation
|
|
- SSRF security suite
|
|
|
|
완료 조건: loopback·private·metadata·rebind·redirect 공격이 모두 차단된다.
|
|
|
|
### 32.5 Reactive Release
|
|
|
|
- Reactor Netty
|
|
- WebClient Gateway
|
|
- Reactive Typed Client
|
|
- streaming upload·download
|
|
- SSE
|
|
- cancellation·backpressure
|
|
|
|
완료 조건: buffer leak, event-loop blocking, first-byte retry, stream idle suite가 통과한다.
|
|
|
|
### 32.6 Extended Release
|
|
|
|
- Spring Boot starter
|
|
- Spring 7 Service Groups
|
|
- RestTemplate migration
|
|
- proxy·HTTP/2 advanced evidence
|
|
- support matrix와 runbook
|
|
|
|
### 32.7 Experimental
|
|
|
|
- Jetty HTTP/3
|
|
- Reactor HTTP/3 profile
|
|
- Native engine Lab
|
|
- request hedging Lab
|
|
|
|
---
|
|
|
|
## 33. 완료 정의
|
|
|
|
플랫폼은 다음이 코드와 CI로 증명될 때 완료된다.
|
|
|
|
| 영역 | 증명 조건 |
|
|
|---|---|
|
|
| API | 주요 호출이 Typed Client로 구현되고 H2·H3 사용이 별도 권한으로 제한된다. |
|
|
| 경계 | H1~H4가 timeout, host, TLS, auth, size, observation을 우회하지 못한다. |
|
|
| Engine | Apache·JDK·Reactor가 동일 result·exception metadata를 제공한다. |
|
|
| Deadline | pool·DNS·connect·TLS·retry backoff를 포함한 전체 시간이 effective deadline 이내다. |
|
|
| Retry | 모든 추가 attempt가 idempotency·replayability·evidence·deadline·budget으로 설명된다. |
|
|
| Ambiguity | 비멱등 `SENT_NO_RESPONSE`가 `HttpAmbiguousExecutionException`으로 구분된다. |
|
|
| Resource | body 미소비, decode 오류, cancel, size 초과 후에도 pool과 buffer가 회수된다. |
|
|
| Auth | token refresh single-flight, 401 최대 1회, secret rotation이 검증된다. |
|
|
| TLS | trust-all과 hostname 검증 해제가 startup에서 차단된다. |
|
|
| SSRF | canonicalization, DNS/IP, redirect, egress 테스트가 통과한다. |
|
|
| Streaming | first byte 이후 transparent retry가 0회다. |
|
|
| Observability | logical call과 attempt가 분리되고 forbidden label이 없다. |
|
|
| Failure | DNS, pool, TLS, reset, partial response, HTTP/2 GOAWAY를 재현한다. |
|
|
| Performance | 설정된 thread, heap, direct memory, pool, retry budget 상한을 넘지 않는다. |
|
|
| Compatibility | Spring 6.2·7.0과 지원 transport matrix가 release CI에 연결된다. |
|
|
| Documentation | support matrix, configuration reference, security guide, runbook, migration guide가 코드와 일치한다. |
|
|
|
|
---
|
|
|
|
## 34. 최종 구현 기준
|
|
|
|
이 설계의 최종 원칙은 다음과 같다.
|
|
|
|
> HTTP 기능을 최대한 많이 열어두되, 호출자가 URL·timeout·retry·credential·TLS·resource lifecycle을 임의로 조립하게 하지 않는다. 일반 호출은 Typed Client와 Named Client Profile을 사용하고, 플랫폼은 요청이 실제로 실행됐을 가능성과 다시 실행해도 되는지를 증거 기반으로 판정한다.
|
|
|
|
구현 우선순위는 다음으로 고정한다.
|
|
|
|
```text
|
|
Core 계약
|
|
→ Named Client Profile
|
|
→ Transport SPI와 Testkit
|
|
→ Deadline·Target·Observability
|
|
→ Apache·JDK Blocking
|
|
→ RestClient와 Typed Client
|
|
→ Execution Evidence와 Retry
|
|
→ Resilience
|
|
→ Error·Auth·TLS
|
|
→ Dynamic Target SSRF
|
|
→ Reactor Netty·WebClient
|
|
→ Streaming·SSE
|
|
→ Starter·Migration·Compatibility
|
|
→ HTTP/3 Experimental
|
|
```
|