init: 클린 기반 auth 서버 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:30:18 +09:00
parent 471db0203d
commit 8a1ac1e769
3642 changed files with 275893 additions and 1 deletions
@@ -0,0 +1,300 @@
# External API Client Structure 기준
## 1. 목적
이 문서는 외부 API 호출용 client 구조와 책임 분리를 정의한다.
이 문서의 목표는 다음과 같다.
- 외부 연동 코드를 application/domain에서 분리한다
- HTTP client 선택 기준을 일관되게 만든다
- request/response DTO, mapper, exception translation 위치를 명확히 한다
- 관측 가능성, 설정, 인증 헤더 주입, 공통 customization을 한곳에 모은다
## 2. 근거 수준
- Official: Spring Framework / Spring Boot 공식 문서에서 직접 확인되는 내용
- Official + Practice: 공식 지원 방식 위에 일반적인 실무 연동 구조를 결합한 내용
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
## 3. 기본 원칙
### 3.1 외부 API client는 infrastructure adapter다
Spring은 RestClient, WebClient, HTTP Service Client를 모두 “원격 HTTP 서비스 호출” 도구로 제공한다. 즉, 이들은 비즈니스 로직이 아니라 외부 시스템 경계 접근 수단이다. 이 프로젝트에서는 외부 API client를 기본적으로 infrastructure 레이어의 adapter로 본다.
프로젝트 규칙:
- 외부 API client는 infrastructure/integration adapter에 둔다
- application/domain이 RestClient, WebClient, ResponseEntity, HTTP status, provider-specific DTO를 직접 다루지 않는다
- 외부 호출은 port/adapter 경계를 통해 사용한다
### 3.2 외부 API 연동 구조의 기본 단위는 “adapter + DTO + mapper + translation”이다
Spring이 HTTP client 자체는 제공하지만, 어떤 DTO를 어떻게 매핑하고 어떤 예외로 번역할지는 애플리케이션이 책임져야 한다. 따라서 이 프로젝트는 외부 API client를 단순한 HTTP 호출 클래스가 아니라 연동 adapter 패키지로 다룬다.
프로젝트 기본 구조:
- client adapter
- external request DTO
- external response DTO
- mapper / translator
- provider-specific exception translation
- 설정(properties / builder customization)
### 3.3 외부 연동 계약과 내부 모델은 분리한다
Spring의 message conversion과 HTTP service interface는 DTO를 손쉽게 직렬화/역직렬화해 주지만, 그것이 곧 외부 DTO를 내부 모델처럼 써도 된다는 뜻은 아니다. 이 프로젝트에서는 외부 API request/response DTO를 내부 application/domain 모델과 분리한다.
프로젝트 규칙:
- 외부 API request/response DTO는 provider contract 전용 타입이다
- application/domain은 외부 DTO를 직접 알지 않는다
- adapter 경계에서 내부 command/result 또는 domain 값으로 변환한다
## 4. HTTP client 선택 기준
### 4.1 imperative 애플리케이션 기본값은 RestClient
Spring Boot는 비리액티브 애플리케이션이면 RestClient 또는 RestTemplate를 사용할 수 있다고 설명하고, Spring Framework는 RestClient를 동기식 fluent API 로 설명하며, RestTemplate는 현재 RestClient 쪽이 더 권장되는 방향이라고 명시한다.
프로젝트 규칙:
- 일반 Spring MVC / imperative 애플리케이션의 외부 HTTP 호출 기본값은 RestClient
- 새 코드에서 RestTemplate를 기본 선택지로 두지 않는다
- 동기 블로킹 호출이 자연스러운 use case에는 RestClient를 우선한다
### 4.2 reactive 애플리케이션 또는 진짜 non-blocking 경계에는 WebClient
Spring Boot는 WebFlux 기반 non-blocking reactive 애플리케이션이면 WebClient 사용을 권장한다. WebClient는 fully reactive client이며, Boot는 WebClient.Builder를 미리 구성해서 제공한다.
프로젝트 규칙:
- 애플리케이션 자체가 reactive이거나, non-blocking end-to-end 흐름이 필요한 경우 WebClient
- 단순히 최신 API라는 이유만으로 imperative 서비스에 WebClient를 기본 도입하지 않는다
- reactive client를 도입할 때는 호출부부터 반환 타입, backpressure, timeout 모델까지 함께 고려한다
### 4.3 HTTP Service Client는 선언적 계약이 분명할 때 허용한다
Spring은 @HttpExchange, @GetExchange, @PostExchange 등으로 정의한 인터페이스에 프록시를 붙이는 HTTP Service Client를 공식 지원하고, Boot는 이를 import하고 group으로 묶는 기능도 제공한다.
프로젝트 규칙:
- 외부 API 계약이 안정적이고 메서드 시그니처가 선언적으로 잘 드러나는 경우 HTTP Service Client 허용
- 다만 복잡한 동적 요청 조립, 세밀한 에러 처리, 낮은 수준의 HTTP 제어가 많으면 RestClient/WebClient를 우선 검토한다
- 선언형 인터페이스를 쓰더라도 adapter 경계와 DTO 분리 규칙은 그대로 유지한다
## 5. Builder / 공통 구성 규칙
### 5.1 Boot가 자동 구성한 builder를 주입해서 사용한다
Spring Boot는 WebClient.Builder와 RestClient.Builder를 prototype bean으로 자동 구성하고, 이를 주입해 사용하는 것을 강하게 권장한다. Boot가 제공하는 builder를 사용해야 HTTP resource 공유, codec 반영, 적절한 request factory, 그리고 관측/계측이 함께 적용된다. RestClient.create()를 직접 쓰면 auto-configuration과 customizer 적용이 따라오지 않는다.
프로젝트 규칙:
- RestClient.Builder / WebClient.Builder는 주입받아 사용한다
- RestClient.create() / WebClient.builder()를 코드 곳곳에서 직접 호출하는 것을 기본 금지한다
- 공통 관측, SSL, codec, 인증 헤더, timeout 설정을 우회하지 않는다
### 5.2 공통 customization은 builder/customizer/group에 둔다
Spring Boot는 RestClientCustomizer, WebClient.Builder, SSL bundle 적용, HTTP Service client group 등을 통해 공통 구성을 모을 수 있다고 설명한다. HTTP Service group은 URL뿐 아니라 timeout, SSL, auth customization 같은 공통 특성을 공유할 수 있다.
프로젝트 규칙:
- base URL, timeout, SSL, 공통 header, user-agent, auth header 삽입은 공통 구성으로 관리
- client마다 같은 interceptor/filter/header 삽입 로직을 복붙하지 않는다
- provider 단위의 공통 설정은 group 또는 전용 configuration으로 묶는다
### 5.3 builder는 “전역 기본값 + 클라이언트별 좁은 추가 설정” 구조로 쓴다
Spring Boot 문서는 RestClient.Builder customization은 범위를 좁게 적용할수록 좋고, builder가 stateful이므로 필요하면 clone을 고려하라고 설명한다.
프로젝트 규칙:
- 전역 공통값은 customizer/configuration
- 특정 provider에만 필요한 설정은 그 adapter 구성 지점에서 추가
- 하나의 builder를 여러 외부 시스템에 무비판적으로 뒤섞어 쓰지 않는다
## 6. 패키지 / 타입 구조 규칙
### 6.1 provider별 또는 capability별로 구조를 분리한다
프로젝트 규칙:
- 외부 시스템이 다르면 패키지를 분리한다
- 하나의 외부 시스템 안에서도 계약이 크면 capability 단위로 나눌 수 있다
권장 예:
```text
integration/keycloak/...
integration/payment/...
integration/email/...
```
또는
```text
integration/keycloak/token/...
integration/keycloak/user/...
```
### 6.2 한 adapter는 한 외부 계약 또는 한 capability를 담당한다
프로젝트 규칙:
- 하나의 client class가 외부 시스템 전체를 거대한 god client처럼 다루지 않는다
- 토큰 발급, 사용자 조회, 세션 폐기처럼 책임이 다르면 분리한다
- 다만 지나치게 잘게 쪼개서 공통 설정이 흩어지지 않게 provider 구성과 capability 구성을 함께 본다
### 6.3 외부 DTO, 내부 결과, 매퍼를 분리한다
프로젝트 규칙:
- *Request, *Response는 외부 계약용 DTO
- *Result, *Command, *FailureReason 등은 내부용 모델
- DTO → 내부 결과 변환은 mapper/translator가 담당
- application/domain은 외부 JSON 필드명과 provider-specific enum을 모른다
## 7. 인증 / 헤더 / URL 규칙
### 7.1 base URL은 코드 하드코딩이 아니라 설정 기반으로 둔다
Spring Boot는 HTTP Service groups에서 logical name과 property 기반 URL lookup을 사용하는 방향을 설명하며, absolute URL 하드코딩은 production에 이상적이지 않다고 말한다.
프로젝트 규칙:
- base URL은 properties/configuration으로 관리
- 코드 안 https://... 하드코딩을 기본 금지
- 환경별 URL 차이는 설정으로 해결한다
### 7.2 인증 헤더 삽입은 adapter 공통 레이어에서 처리한다
Spring 문서는 RestClient에 default header, interceptor, request initializer를 둘 수 있고, HTTP Service group에도 authorization header 삽입 같은 customization을 연결할 수 있다고 설명한다.
프로젝트 규칙:
- Authorization, API key, user-agent, correlation header는 공통 client 구성에서 삽입
- business 로직에서 매번 header를 조립하지 않는다
- 토큰 갱신/획득 로직도 provider adapter 경계에 둔다
### 7.3 URI template와 path variable을 우선 사용한다
Spring Framework는 RestClient, WebClient, RestTemplate가 URI template와 URI builder를 지원한다고 설명한다.
프로젝트 규칙:
- string concatenation으로 URL을 만들지 않는다
- path/query 조립은 template / builder 방식으로 처리한다
- query parameter 의미가 드러나게 작성한다
## 8. 반환 / 예외 / 번역 규칙
### 8.1 adapter는 ResponseEntity, raw status, client exception을 그대로 위로 올리지 않는다
Spring의 client는 HTTP status, body, exception을 직접 다룰 수 있지만, application/domain이 그 디테일을 그대로 보게 두면 외부 계약이 내부 계층으로 번진다.
프로젝트 규칙:
- adapter는 내부 결과 타입 또는 port 계약 타입을 반환한다
- application은 WebClientResponseException, HttpStatusCodeException, ClientResponse 같은 타입을 직접 다루지 않는다
- HTTP status 해석은 adapter 안에서 끝낸다
### 8.2 provider-specific 실패는 integration exception으로 번역한다
프로젝트 규칙:
- 외부 401/403/404/409/5xx를 그대로 application에 노출하지 않는다
- provider-specific error body는 integration exception 또는 내부 failure reason으로 번역한다
- 예외 번역 상세 규칙은 별도 exception-translation.md에서 source of truth로 둔다
### 8.3 2xx만 성공으로 보는 단순 규칙을 넘어서 provider 계약을 해석한다
프로젝트 규칙:
- HTTP 200이어도 business failure payload이면 실패로 번역할 수 있다
- 반대로 일부 4xx가 provider 계약상 “정상적인 부재/중복 상태”라면 내부 의미로 적절히 번역한다
- 성공/실패 판정 기준은 provider contract 단위로 명시한다
## 9. DTO / 직렬화 규칙
### 9.1 외부 요청/응답 DTO는 provider contract에 맞춘다
Spring은 RestClient, WebClient, HTTP Service Client 모두 message conversion으로 DTO를 JSON과 매핑한다. 이 DTO는 provider JSON 계약에 맞춰야 하며, 내부 표준 DTO와 동일할 필요가 없다.
프로젝트 규칙:
- 외부 JSON 필드명은 외부 DTO에서만 해결한다
- provider-specific field naming, enum, optionality는 외부 DTO에 국소화한다
- 내부 모델 필드명을 외부 계약 때문에 바꾸지 않는다
### 9.2 외부 응답 파싱 정책은 first-party API보다 더 lenient할 수 있다
Spring/Jackson 조합은 DTO 역직렬화를 유연하게 지원한다. 외부 시스템은 필드 추가/응답 shape 변화가 일어날 수 있으므로, third-party response DTO는 first-party API request DTO보다 lenient 정책을 택할 수 있다. 이 세부 기준은 별도 serialization 문서에서 다루되, external client 구조에서도 이 방향을 따른다.
프로젝트 규칙:
- 외부 response DTO는 unknown field 허용 가능
- 외부 request DTO는 provider 요구에 맞춰 엄격하게 작성
- 내부 domain/application DTO와 정책을 섞지 않는다
## 10. 관측 가능성 규칙
### 10.1 외부 API client는 관측 가능해야 한다
Spring Boot Actuator는 RestTemplate, WebClient, RestClient의 HTTP client instrumentation을 지원하고, 이를 위해 auto-configured builder를 사용하라고 설명한다. Spring Framework observability 문서는 기본 저카디널리티 키로 method, uri template, client.name, status, outcome, error를 정의한다.
프로젝트 규칙:
- 외부 API client는 auto-configured builder를 통해 관측 가능성을 확보한다
- metrics/traces/logs에서 최소한 client.name, method, uri template, status, error를 추적 가능하게 한다
- raw full URL과 payload 전문을 로그 기본값으로 남기지 않는다
### 10.2 URI template를 유지한다
Spring observability 문서는 low cardinality key로 uri template를 쓰고, host/port를 제외한 template 개념을 사용한다.
프로젝트 규칙:
- 외부 호출 관측에서는 가능한 한 URI template를 유지한다
- /users/123 같은 실제 path 대신 /users/{id} 같은 템플릿이 추적 가능하게 한다
- 메트릭 태그에 고카디널리티 path를 그대로 쓰지 않는다
## 11. 문서 간 경계
이 문서는 구조와 책임 분리를 다룬다. 아래 주제의 세부 규칙은 별도 문서를 source of truth로 둔다.
- timeout
- retry
- idempotency
- serialization/deserialization
- fallback
- exception translation
이 문서는 위 주제들을 다시 처음부터 반복하지 않고, 외부 API client 구조 안에서 어디에 둘지만 정의한다.
## 12. 금지 규칙
다음은 기본 금지다.
- controller/application/domain에서 RestClient/WebClient 직접 호출
- RestClient.create() / WebClient.builder()를 여기저기서 직접 생성
- base URL 하드코딩
- 외부 DTO를 내부 application/domain 메서드 시그니처에 그대로 전달
- ResponseEntity, raw status code, client exception을 그대로 내부에 전파
- giant external client 하나에 모든 provider capability 몰아넣기
- 인증 헤더/공통 header를 business code에서 매번 조립
- provider payload 전문을 기본 로그로 남기기
## 13. 체크리스트
다음 질문에 “예”로 답할 수 있어야 한다.
- 이 외부 연동 코드는 infrastructure adapter에 위치하는가?
- 애플리케이션 성격에 맞게 RestClient/WebClient/HTTP Service Client를 선택했는가?
- auto-configured builder를 주입해 사용하고 있는가?
- base URL, auth, timeout, SSL, 공통 customization이 공통 구성에 모여 있는가?
- 외부 request/response DTO와 내부 모델이 분리되어 있는가?
- adapter가 provider-specific HTTP 디테일을 내부로 누수시키지 않는가?
- 관측 가능성(metrics/traces/logs)이 확보되어 있는가?
+318
View File
@@ -0,0 +1,318 @@
# Fallback 기준
## 1. 목적
이 문서는 외부 API / integration 호출 실패 시 fallback을 어떻게 적용할지 정의한다.
이 문서의 목표는 다음과 같다.
- fallback을 retry와 구분한다
- 어떤 외부 의존성을 soft dependency로 바꿀 수 있는지 판단 기준을 만든다
- fallback 결과가 비즈니스 의미를 왜곡하지 않게 한다
- 캐시, 정적 기본값, 제한된 기능, 비동기 전환 같은 대체 전략을 일관되게 사용한다
## 2. 근거 수준
- Official: Spring Cloud CircuitBreaker, Resilience4j, AWS Well-Architected 공식 문서에서 직접 확인되는 내용
- Official + Practice: 공식 기능 위에 일반적인 graceful degradation 운영 관행을 결합한 내용
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
## 3. 기본 원칙
### 3.1 fallback은 retry의 연장이 아니라 별도 결과 전략이다
Spring Cloud CircuitBreaker는 원래 실행 코드와 별도로 fallback function을 받으며, 실패 시 그 fallback이 실행된다고 설명합니다. Resilience4j도 fallback method를 try/catch와 유사한 대체 경로로 설명합니다. 즉 fallback은 “한 번 더 시도”가 아니라 “다른 결과 경로”입니다.
프로젝트 규칙:
- retry는 원래 호출을 다시 시도하는 것
- fallback은 원래 호출을 포기하고 다른 결과를 내는 것
- 두 개념을 한 문서/한 코드 블록에서 섞어 흐리게 만들지 않는다
### 3.2 fallback은 hard dependency를 soft dependency로 바꿀 수 있을 때만 사용한다
AWS Well-Architected는 graceful degradation을 통해 적용 가능한 hard dependency를 soft dependency로 바꾸라고 권장합니다. 의존성이 unhealthy해도 컴포넌트가 degraded mode로 계속 동작할 수 있어야 한다는 뜻입니다.
프로젝트 규칙:
- fallback은 “이 의존성이 없어도 핵심 기능이 여전히 의미 있게 동작하는가?”를 먼저 묻는다
- 핵심 정합성/보안/결제 확정처럼 정답이 아니면 안 되는 기능에는 fallback을 기본 금지한다
- 보조 기능, 부가 정보, 랭킹, 추천, 프로필 부가 데이터, 캐시 가능한 조회는 fallback 후보가 될 수 있다
### 3.3 fallback은 실패를 숨기지 말고 degraded mode를 명시해야 한다
Spring Cloud CircuitBreaker의 fallback은 예외를 받아 대체 결과를 만들 수 있습니다. AWS도 degraded response를 반환하되, 그것이 대체 응답이라는 사실을 이해하고 설계해야 한다고 보는 맥락입니다. 즉 fallback은 “조용히 다른 값을 넣기”가 아니라 품질 저하 상태를 의도적으로 선택하는 것입니다.
프로젝트 규칙:
- fallback 결과는 내부적으로 추적 가능해야 한다
- 운영 로그/메트릭에서 fallback 발생 여부를 구분할 수 있어야 한다
- 호출자가 알아야 하는 degraded semantics를 숨기지 않는다
## 4. 언제 fallback을 허용하는가
### 4.1 허용 가능한 대표 경우
프로젝트 규칙:
다음은 fallback 후보가 될 수 있다.
- 외부 추천 시스템 실패 시 빈 추천 목록 반환
- 외부 프로필 부가 정보 실패 시 핵심 프로필만 반환
- 외부 공개키/JWK 조회 실패 시 짧은 TTL 캐시값 사용
- 외부 feature flag 조회 실패 시 안전한 기본값 사용
- 외부 알림 발송 실패 시 outbox 적재 후 비동기 재시도 전환
- 외부 검색/랭킹 실패 시 기본 정렬 결과 반환
이 방향은 AWS가 말하는 graceful degradation, 즉 정적 응답·사전 결정된 대체 응답으로 hard dependency를 soft dependency로 바꾸는 사고와 맞습니다.
### 4.2 기본적으로 허용하지 않는 경우
프로젝트 규칙:
다음은 기본적으로 fallback을 금지한다.
- 결제 승인/캡처/환불 확정
- 인증/인가의 핵심 판정
- 비밀번호 변경/토큰 발급/보안 민감 작업
- 재고 차감/정산 반영/회원 상태 확정
- 법적/감사적 정합성이 필요한 기록 확정
- “성공처럼 보이면 안 되는” 핵심 command
이 경우는 graceful degradation보다 명시적 실패가 더 안전하다. AWS의 graceful degradation도 모든 dependency를 soft dependency로 바꾸라는 뜻은 아니며, “applicable hard dependencies”에 한정합니다.
## 5. fallback 종류
### 5.1 정적 기본값 fallback
AWS는 predetermined static response를 fallback 예시로 듭니다.
프로젝트 규칙:
- 추천 없음 → 빈 리스트
- 부가 배지 없음 → 빈 값
- 외부 설명문 없음 → 기본 문구
처럼 명백히 안전한 기본값만 허용
핵심 비즈니스 의미를 바꾸는 가짜 성공값은 금지
### 5.2 캐시 기반 fallback
프로젝트 규칙:
- 최근 성공 응답을 짧은 TTL로 캐시해 두고 외부 장애 시 사용 가능
- 단, stale 허용 범위가 문서화돼야 한다
- 캐시 fallback은 조회성 데이터에 우선 적용한다
- 오래된 데이터를 최신 사실처럼 보이게 하면 안 된다
### 5.3 기능 축소(degraded mode) fallback
프로젝트 규칙:
- 외부 부가 서비스가 죽으면 핵심 기능만 제공
- 예:
- “프로필 상세 + 외부 배지” → “프로필 상세만”
- “개인화 추천 + 일반 목록” → “일반 목록만”
- 기능 축소 후에도 결과 의미가 일관돼야 한다
### 5.4 비동기 전환 fallback
프로젝트 규칙:
- 외부 동기 호출 실패 시 즉시 실패 대신 outbox/queue 적재 후 비동기 처리로 전환할 수 있다
- 예:
- 이메일 전송 요청 → “접수됨” 응답 후 비동기 발송
- 단, 이 경우 API 의미가 “즉시 완료”가 아니라 “접수”로 바뀌므로 계약이 명확해야 한다
### 5.5 fallback 없이 명시적 실패
프로젝트 규칙:
- fallback이 어색하거나 의미를 왜곡하면 실패가 정답이다
- “fallback이 없으면 덜 우아해 보인다”는 이유로 억지 fallback을 두지 않는다
- 실패가 더 정직한 경우에는 실패를 택한다
## 6. 설계 규칙
### 6.1 fallback 결과는 원래 결과와 같은 의미를 가장하지 않는다
Spring Cloud CircuitBreaker fallback은 예외를 받아 대체 결과를 리턴할 수 있지만, 그 결과가 원래 외부 호출 성공과 동일한 의미를 가진다고 보장하지는 않습니다.
프로젝트 규칙:
- fallback 응답을 “정상 외부 응답”처럼 위장하지 않는다
- 내부 result 모델에서 degraded 여부를 표현할 수 있으면 표현한다
- API 바깥으로 드러나야 하는 경우에는 metadata/flag로 구분 가능하게 한다
### 6.2 fallback은 provider-specific 예외보다 내부 의미로 판단한다
프로젝트 규칙:
fallback 조건은 SocketTimeoutException, WebClientResponseException 같은 저수준 타입 그 자체보다
- ExternalProfileTemporaryFailure
- RecommendationProviderUnavailable
같은 내부 번역 예외 기준으로 두는 편을 선호한다
adapter가 provider 예외를 먼저 번역하고, 상위 integration service가 fallback 여부를 판단할 수 있다
### 6.3 fallback은 조용한 데이터 오염을 만들면 안 된다
프로젝트 규칙:
- 캐시 fallback은 stale 가능성을 고려한다
- 기본값 fallback은 진짜 부재와 fallback 결과를 혼동시키지 않는다
- 외부 검증 실패를 내부 성공으로 바꾸는 fallback을 금지한다
## 7. 위치 규칙
### 7.1 fallback은 integration adapter 바로 위 또는 integration service 경계에 둔다
Spring Cloud CircuitBreaker fallback은 wrapped supplier를 대체하는 함수로 붙습니다. 실무적으로도 fallback은 외부 호출 의미를 가장 잘 아는 integration 경계에 두는 것이 맞습니다.
프로젝트 규칙:
- fallback은 external client adapter 바로 위의 integration service에서 우선 검토
- controller/application/domain에 provider-aware fallback 로직을 두지 않는다
- domain이 fallback 존재를 알아야 하는 구조를 기본 금지한다
### 7.2 controller에서 fallback 결과를 직접 조립하지 않는다
프로젝트 규칙:
- controller는 fallback 여부를 판단하는 위치가 아니다
- 외부 의존성 실패와 대체 전략은 integration 경계에서 끝낸다
- controller는 최종 내부 result만 받아 응답으로 번역한다
## 8. Circuit Breaker와의 관계
### 8.1 fallback은 circuit breaker와 함께 쓰일 수 있다
Spring Cloud CircuitBreaker는 fallback function을 공식 지원하고, OpenFeign + CircuitBreaker 문서도 fallback class를 둘 수 있다고 설명합니다.
프로젝트 규칙:
- 회로 차단기와 fallback을 함께 사용하는 것은 허용
- 단, circuit open 상태라고 항상 fallback이 정답인 것은 아니다
- “실패를 빠르게 차단”과 “대체 결과 제공”은 별도 결정으로 본다
### 8.2 circuit open fallback과 단일 호출 실패 fallback을 구분한다
프로젝트 규칙:
- 일시적 단일 실패에서의 fallback
- circuit open 상태에서의 fallback
- 은 운영 의미가 다를 수 있다
- observability에서는 이 둘을 구분할 수 있어야 한다
## 9. cache/staleness 규칙
### 9.1 캐시 fallback은 staleness budget이 있어야 한다
프로젝트 규칙:
- 캐시 fallback은 “얼마나 오래된 값을 허용할지”가 먼저 정해져야 한다
- 무기한 stale fallback 금지
- provider 데이터 성격에 따라 허용 TTL을 문서화한다
### 9.2 stale 데이터는 최신 사실처럼 취급하지 않는다
프로젝트 규칙:
- 외부 프로필, 환율, 추천, 재고성 정보는 stale일 수 있다
- stale 허용이 어려운 정보에는 캐시 fallback을 두지 않는다
- stale 사용 사실이 내부적으로 추적 가능해야 한다
## 10. observability 규칙
### 10.1 fallback 발생은 반드시 관측 가능해야 한다
AWS의 graceful degradation은 장애 시 soft dependency로 동작을 바꾸는 것이므로, 운영자는 fallback이 언제 얼마나 발생하는지 알아야 합니다. Spring Cloud CircuitBreaker fallback도 Throwable을 인자로 받아 원인과 함께 처리할 수 있습니다.
프로젝트 규칙:
fallback 발생 로그/메트릭을 남긴다
최소한 다음을 구분 가능해야 한다
- provider
- operation
- fallback type(정적/캐시/비동기 전환 등)
- 원인 예외
- 최종 결과(success degraded / fail)
### 10.2 fallback 후 성공은 ERROR로 기록하지 않는다
프로젝트 규칙:
- fallback이 적용돼 요청이 의미 있게 처리됐다면 기본 WARN 또는 INFO
- 최종 실패만 대표 ERROR
- fallback 성공을 장애처럼 과장하지 않는다
- 다만 fallback 비율이 높아지면 경고 신호로 본다
### 10.3 fallback 비율은 SLO/품질 지표로 본다
프로젝트 규칙:
- 성공률만 보지 않고 fallback rate도 본다
- “서비스는 성공했지만 품질은 저하된 상태”를 따로 추적한다
- fallback이 많으면 upstream 문제 또는 timeout/retry 설정 문제를 의심한다
## 11. 예외와 응답 규칙
### 11.1 fallback 결과가 있으면 예외를 그대로 밖으로 던지지 않는다
Resilience4j fallback도 실패를 대체 결과로 바꾸는 구조입니다.
프로젝트 규칙:
- fallback이 최종 결과를 제공하면 외부 예외를 그대로 상위 계층에 올리지 않는다
- 대신 degraded result를 반환한다
- 원인 예외는 observability에 남긴다
### 11.2 fallback이 불가능하면 실패를 번역해 올린다
프로젝트 규칙:
- fallback이 적용되지 않거나 의미가 없으면 integration exception으로 번역해 올린다
- “fallback도 실패했는데 기본값으로 성공처럼 처리”를 금지한다
## 12. 다른 문서와의 경계
이 문서는 fallback만 다룬다.
아래 주제의 source of truth는 별도 문서다.
- timeout
- retry
- outbound idempotency
- serialization/deserialization
- exception translation
이 문서는 위 내용을 반복하지 않고, 외부 의존성 실패 시 어떤 대체 결과를 허용할지만 정의한다.
## 13. 금지 규칙
다음은 기본 금지다.
- 핵심 정합성/보안/결제 확정에 억지 fallback 적용
- fallback 결과를 원래 정상 결과처럼 위장
- stale 데이터 무기한 사용
- controller에서 provider-aware fallback 구현
- fallback 발생을 관측하지 않음
- fallback으로 실패를 전부 숨김
- provider 저수준 예외 타입에 강하게 결합된 fallback 분기
- 캐시/정적 기본값이 비즈니스 의미를 왜곡하는데도 사용
## 14. 체크리스트
다음 질문에 “예”로 답할 수 있어야 한다.
- 이 외부 의존성은 soft dependency로 바꿔도 되는가?
- fallback 결과가 비즈니스 의미를 왜곡하지 않는가?
- fallback 종류(정적/캐시/기능 축소/비동기 전환)가 명확한가?
- stale 허용 범위가 문서화돼 있는가?
- fallback 발생이 로그/메트릭에서 관측 가능한가?
- fallback 성공을 실패처럼 ERROR로 과장하지 않는가?
- controller/application/domain이 아니라 integration 경계에 fallback이 있는가?
+316
View File
@@ -0,0 +1,316 @@
# Integration Idempotency 기준
## 1. 목적
이 문서는 외부 API / integration 호출에서 outbound idempotency 를 어떻게 다룰지 정의한다.
이 문서의 목표는 다음과 같다.
- 외부 provider가 제공하는 idempotency 기능을 안전하게 사용한다
- timeout, partial failure, 응답 유실 상황에서 중복 side effect 를 막는다
- 우리 내부 idempotency key와 provider idempotency key의 관계를 명확히 한다
- 외부 API 재시도 시 어떤 조건에서 같은 key를 재사용해야 하는지 정한다
## 2. 근거 수준
- Official: IETF HTTPAPI draft, Stripe, PayPal 공식 문서에서 직접 확인되는 내용
- Official + Practice: 공식 규약 위에 일반적인 연동 운영 관행을 결합한 내용
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
## 3. 기본 원칙
### 3.1 이 문서는 inbound가 아니라 outbound idempotency를 다룬다
IETF 초안은 Idempotency-Key를 클라이언트가 서버에 보내는 중복 방지 키로 설명한다. 우리 서비스가 provider를 호출할 때는, 우리가 그 provider 입장에서 “클라이언트”가 된다. 따라서 이 문서는 “사용자가 우리 API를 다시 호출하는 상황”이 아니라, “우리가 외부 provider에 같은 요청을 다시 보내는 상황”을 다룬다.
프로젝트 규칙:
- inbound idempotency와 outbound idempotency를 같은 문서로 뒤섞지 않는다
- 이 문서는 provider 호출용 키 생성/재사용/저장/오류 처리 규칙만 정의한다
### 3.2 outbound idempotency의 핵심 목적은 “같은 외부 side effect를 한 번만 일으키는 것”이다
Stripe는 생성/수정 요청에 idempotency key를 사용하면 연결 오류나 응답 유실이 있어도 같은 요청을 안전하게 반복할 수 있다고 설명한다. PayPal도 POST 호출에서 PayPal-Request-Id를 사용하면 서버가 중복 생성/처리를 피할 수 있다고 설명한다. 즉 outbound idempotency는 “같은 외부 요청 의도”를 다시 보내더라도 provider 쪽에서 한 번만 처리되게 만드는 장치다.
프로젝트 규칙:
- 외부 생성/확정/발급/결제/전송 같은 side effect 호출에는 outbound idempotency를 기본 검토한다
- “응답을 못 받았으니 다시 보내자” 상황에서 중복 side effect가 나지 않아야 한다
### 3.3 retry와 outbound idempotency는 함께 설계한다
Stripe는 네트워크 오류가 나더라도 같은 idempotency key를 써서 다시 보내면 중복 생성 위험을 줄일 수 있다고 설명한다. 반대로 key 없이 같은 POST를 다시 보내면 중복 호출이 될 수 있다. PayPal도 PayPal-Request-Id를 생략하면 요청이 중복될 수 있다고 설명한다. 따라서 retry는 outbound idempotency와 분리해서 설계할 수 없다.
프로젝트 규칙:
- side effect가 있는 외부 API retry는 outbound idempotency 검토 없이 자동화하지 않는다
- timeout 이후 retry 전략은 반드시 provider idempotency 지원 여부와 함께 본다
## 4. 언제 필요한가
### 4.1 기본 검토 대상
프로젝트 규칙:
다음은 outbound idempotency 기본 검토 대상이다.
- 결제 승인/확정/캡처
- 토큰/세션/쿠폰/번호 발급
- 이메일/SMS/웹훅 발송 요청
- 외부 시스템에 리소스를 생성하는 POST
- 외부 상태를 irreversible 하게 바꾸는 요청
- timeout 이후 retry 가능성이 높은 provider 호출
Stripe와 PayPal의 공식 idempotency 문서도 이런 POST 중심 side effect 요청을 주된 대상으로 설명한다.
### 4.2 기본 검토 대상이 아닌 경우
프로젝트 규칙:
다음은 outbound idempotency header를 기본값으로 요구하지 않는다.
- 단순 GET 조회
- provider가 이미 HTTP 의미상 idempotent한 PUT/DELETE만 제공하는 경우
- 읽기 전용 상태 확인 API
- side effect가 없는 health/ping/check API
Stripe도 GET/DELETE에는 idempotency key를 보내도 의미가 없다고 안내한다.
## 5. provider key와 내부 key의 관계
### 5.1 provider가 공식 idempotency key를 지원하면 그 계약을 우선 따른다
Stripe는 Idempotency-Key 헤더를, PayPal은 PayPal-Request-Id 헤더를 공식 지원한다. PayPal은 API call type마다 고유해야 한다고도 설명한다. 따라서 provider가 지원하는 공식 키 규약이 있으면 그 규약을 먼저 따른다.
프로젝트 규칙:
- provider 공식 idempotency header가 있으면 그 이름과 제약을 그대로 따른다
- 우리 내부 표준 헤더 이름을 provider에 억지로 강요하지 않는다
- adapter가 provider별 차이를 캡슐화한다
### 5.2 내부 idempotency key와 provider idempotency key는 같을 수도, 다를 수도 있다
IETF 초안과 Stripe 문서는 key를 클라이언트가 생성하는 고유 값으로 설명하지만, 실제 운영에서는 우리 내부 command id 와 provider 전송용 key 를 같은 값으로 쓸지 별도 매핑할지 설계 선택이 있다. PayPal은 API call type 단위 고유성을 요구하므로, 단순히 “사용자 요청 ID 하나”를 모든 provider operation에 그대로 쓰는 방식은 맞지 않을 수 있다.
프로젝트 규칙:
- 내부 command id와 provider key를 1:1로 매핑할 수는 있다
- 하지만 provider가 operation scope를 다르게 요구하면 별도 provider key를 만든다
- 내부 키와 provider 키를 무조건 동일시하지 않는다
권장 예:
- 내부 키: outboundCommandId
- provider 키: (provider, operation, outboundCommandId) 기반 생성
### 5.3 provider key scope는 provider 계약을 따른다
PayPal은 PayPal-Request-Id가 “요청마다 그리고 API call type마다” 고유해야 한다고 설명한다. Stripe도 endpoint와 파라미터가 다르면 idempotency error가 난다고 설명한다. 즉 key scope는 provider마다 다를 수 있다.
프로젝트 규칙:
- 같은 key를 다른 provider operation에 재사용하지 않는다
- 같은 provider라도 다른 endpoint/call type에 key 재사용 여부를 provider 계약 기준으로 판단한다
- scope는 최소한 provider + operation + key 수준으로 본다
## 6. 키 생성 규칙
### 6.1 키는 우리가 생성한다
Stripe는 V4 UUID 또는 충분한 entropy를 가진 랜덤 문자열을 권장하고, 민감정보를 key로 쓰지 말라고 말한다. PayPal도 UUID 사용을 권장한다.
프로젝트 규칙:
- provider key는 우리 서비스가 생성한다
- 권장 형식은 UUID v4 또는 이에 준하는 opaque random string
- 이메일, 전화번호, 사용자명, 주문번호 같은 의미 있는 PII를 key에 넣지 않는다
### 6.2 키는 “같은 외부 요청 의도”에만 재사용한다
Stripe는 동일 key에 대해 원래 요청과 들어온 파라미터를 비교하고, 다르면 에러를 반환한다고 설명한다. 따라서 key는 장기 식별자가 아니라 같은 요청의 재전송용 식별자 여야 한다.
프로젝트 규칙:
- 같은 provider 호출을 다시 보낼 때만 같은 key를 재사용한다
- 요청 의미가 달라지면 새 key를 생성한다
- key를 “사용자별 고정 키”처럼 쓰지 않는다
## 7. 저장 규칙
### 7.1 outbound provider 호출에도 내부적으로 key 매핑 기록을 남긴다
Stripe와 PayPal은 provider 측 idempotency를 제공하지만, 우리 서비스가 timeout/partial failure를 겪었을 때 “이 key로 이미 보냈는가, 응답을 받았는가, 재전송해야 하는가”를 판단하려면 내부 기록이 필요하다. 공식 문서들도 provider가 이전 요청의 결과나 최신 상태를 반환한다고 설명하므로, 우리 쪽에서도 그 연관관계를 추적해야 운영이 가능하다.
프로젝트 규칙:
내부적으로 다음을 기록할 수 있어야 한다
- provider
- operation
- provider idempotency key
- 내부 command id
- request fingerprint
- provider request status(시도 중/완료/최종 실패)
- provider response reference
- provider가 idempotency를 제공해도 우리 내부 기록을 완전히 생략하지 않는다
### 7.2 request fingerprint를 함께 저장한다
Stripe는 같은 key 재사용 시 들어온 파라미터를 원래 요청과 비교해 다르면 에러를 낸다고 설명한다. 우리도 내부적으로 같은 key가 다른 요청 의미로 재사용되지 않았는지 확인할 수 있어야 한다.
프로젝트 규칙:
- 내부 저장소에는 key뿐 아니라 request fingerprint도 함께 둔다
- fingerprint는 provider operation 의미를 기준으로 계산한다
- 같은 key + 다른 fingerprint는 버그 또는 오용으로 본다
## 8. 재전송 규칙
### 8.1 timeout/응답 유실 시에는 같은 key로 재전송한다
Stripe는 네트워크 연결 오류로 응답을 못 받아도 같은 key로 재시도하면 안전하다고 설명한다. PayPal도 동일한 PayPal-Request-Id를 다시 보내면 이전 요청의 최신 상태를 반환한다고 설명한다.
프로젝트 규칙:
- provider에 요청을 보냈지만 응답을 못 받았으면 같은 key 재전송을 기본 검토한다
- 새 key로 다시 보내는 것을 기본값으로 두지 않는다
- 이 판단은 retry/timeout 정책과 함께 묶어서 설계한다
### 8.2 provider가 “실행이 시작되지 않았다”고 말한 경우는 새 시도로 볼 수 있다
Stripe는 validation 실패나 concurrent conflict처럼 endpoint 실행이 시작되지 않은 경우에는 결과를 저장하지 않으며, 이런 경우는 다시 시도할 수 있다고 설명한다.
프로젝트 규칙:
- provider가 execution not started에 해당하는 오류를 명시하면 같은 key 재시도 가능성을 검토한다
- validation 자체가 잘못된 요청이라면 재시도보다 요청 수정이 우선이다
- “실행이 시작되지 않았음”과 “응답만 못 받음”을 구분한다
### 8.3 동시 중복 송신을 피한다
PayPal은 같은 PayPal-Request-Id로 동시에 두 요청을 보내면 첫 번째를 처리하고 두 번째는 실패할 수 있다고 설명한다.
프로젝트 규칙:
- 같은 provider key를 가진 outbound 호출은 동시에 두 개 이상 송신하지 않는다
- 내부적으로 키 단위 동시성 제어를 검토한다
- 같은 command를 여러 worker가 동시에 처리하는 구조라면 key-level dedup/lock을 둔다
## 9. provider 응답 해석 규칙
### 9.1 replay 응답은 새 성공과 같은 의미로 취급하되, 출처는 구분 가능해야 한다
Stripe는 같은 key에 대해 첫 결과의 status와 body를 재사용한다고 설명하고, PayPal은 이전 요청의 최신 상태를 반환한다고 설명한다. 즉, provider가 반환한 응답이 “새로 실행된 결과”인지 “기존 실행의 재생/현재 상태”인지 내부적으로는 구분할 수 있는 편이 좋다.
프로젝트 규칙:
- provider replay 응답도 비즈니스적으로는 성공/실패 결과로 받아들인다
- 다만 내부 observability에는
- new execution
- replayed result
- latest known status
- 를 구분할 수 있게 한다
- 외부 API 응답 body를 우리 내부 의미로 무조건 “새로 생성됨”으로 번역하지 않는다
### 9.2 provider의 “latest status”와 “original result” 차이를 이해한다
PayPal은 이전 요청의 “원래 응답”이 아니라 “현재 시점의 최신 상태”를 반환한다고 설명한다. Stripe는 첫 실행 결과를 재사용하는 쪽에 더 가깝다. provider마다 의미가 다르므로, outbound adapter는 이 차이를 내부로 올바르게 번역해야 한다.
프로젝트 규칙:
- provider replay semantics를 문서화한다
- “같은 key면 항상 동일 body 재생”이라고 일반화하지 않는다
- provider별로
- original response replay
- latest status lookup
- concurrent duplicate failure
- 를 구분한다
## 10. TTL 규칙
### 10.1 provider TTL을 존중한다
Stripe는 키를 최소 24시간 이후 정리할 수 있다고 설명한다. PayPal은 일부 API에서 PayPal-Request-Id 보관 기간이 정해져 있고, 그동안 재시도 가능하다고 설명한다.
프로젝트 규칙:
- provider key TTL은 provider 공식 문서 기준을 따른다
- TTL 내 재전송은 같은 key 사용
- TTL 이후는 새 요청으로 처리될 수 있음을 전제로 한다
### 10.2 내부 기록 TTL은 provider TTL보다 짧게 두지 않는다
프로젝트 규칙:
- 내부 key 매핑 기록 TTL은 provider TTL 이상을 기본 검토한다
- provider는 아직 기억하는데 우리는 잊어버리는 상태를 만들지 않는다
- 최소한 “왜 같은 key가 다시 쓰였는지” 추적 가능한 기간을 확보한다
## 11. observability 규칙
### 11.1 outbound idempotency key는 로그에 원문 전체를 남기지 않는다
Stripe는 key에 민감정보를 넣지 말라고 하지만, 그렇다고 로그에 원문 전체를 항상 남겨도 된다는 뜻은 아니다. 외부 키도 운영 식별자일 뿐 민감도 없는 공개값으로 취급하지 않는다.
프로젝트 규칙:
- provider key 원문 전체 로그를 기본 금지
- 필요하면 prefix 또는 내부 correlation id만 남긴다
- 로그에는
- provider
- operation
- outboundCommandId
- providerRequestId
- 정도의 내부 식별자를 우선 사용한다
### 11.2 replay / duplicate / key mismatch는 관측 가능해야 한다
프로젝트 규칙:
outbound idempotency 관련 운영 이벤트는 최소한 다음을 구분 가능해야 한다
- 새 호출
- 같은 key 재전송
- provider replay 응답
- 같은 key 다른 fingerprint 충돌
- 동시 중복 송신 차단
- retry와 idempotency를 함께 분석할 수 있어야 한다
## 12. 다른 문서와의 경계
이 문서는 outbound idempotency만 다룬다.
아래 주제의 source of truth는 별도 문서다.
- retry
- timeout
- fallback
- serialization/deserialization
- exception translation
이 문서는 위 주제들을 다시 반복하지 않고, 외부 provider idempotency를 어떻게 써야 하는지만 정의한다.
## 13. 금지 규칙
다음은 기본 금지다.
- provider 공식 idempotency key 지원이 있는데 무시하고 새 요청처럼 재전송
- 같은 key를 다른 provider operation에 재사용
- 같은 key를 다른 fingerprint 요청에 재사용
- timeout 후 새 key로 같은 side effect 요청 재전송
- provider key와 내부 command 추적 관계를 저장하지 않음
- 같은 key의 동시 중복 송신 허용
- key에 이메일/전화번호/주문자명 같은 의미 있는 PII 사용
- replay semantics가 다른 provider를 같은 규칙으로 단순화
## 14. 체크리스트
다음 질문에 “예”로 답할 수 있어야 한다.
- 이 outbound 호출은 side effect가 있어 idempotency가 필요한가?
- provider가 공식 idempotency key/header를 지원하는가?
- 내부 key와 provider key의 scope가 명확한가?
- timeout/응답 유실 시 같은 key 재전송 전략이 정의되어 있는가?
- 같은 key의 fingerprint 충돌을 감지할 수 있는가?
- provider TTL과 내부 저장 TTL이 정렬되어 있는가?
- replay/new/latest-status semantics를 provider별로 구분하고 있는가?
- retry와 idempotency가 함께 관측 가능한가?
+292
View File
@@ -0,0 +1,292 @@
# Retry 기준
## 1. 목적
이 문서는 외부 API / integration 호출에서 retry를 어떻게 적용할지 정의한다.
이 문서의 목표는 다음과 같다.
- retry 대상을 일시적 실패로 제한한다
- retry가 장애를 증폭시키지 않게 한다
- retry, timeout, idempotency, fallback의 책임을 구분한다
- 외부 HTTP 호출 retry를 adapter 경계 안에서 일관되게 처리한다
## 2. 근거 수준
- Official: Spring Framework / Spring Retry / AWS / Google Cloud 공식 문서에서 직접 확인되는 내용
- Official + Practice: 공식 기능 위에 일반적인 운영 관행을 결합한 내용
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
## 3. 기본 원칙
### 3.1 retry는 기본값이 아니라 제한적 도구다
Spring은 retry 기능을 제공하지만, Spring Framework resilience 문서 기준 기본 retry는 모든 예외를 대상으로 최대 3회 재시도하며 1초 간격을 둡니다. 즉, 프레임워크 기본값만 믿으면 너무 넓게 재시도할 수 있습니다. AWS도 retry는 유용하지만, 부하가 높은 상황에서는 서버에 더 많은 요청을 보내 상황을 악화시킬 수 있다고 설명합니다.
프로젝트 규칙:
- 외부 API retry는 명시적으로 허용한 경우에만 사용한다
- 기본 retry 정책에 의존하지 않는다
- retry를 “일단 켜 두는 안정화 옵션”으로 쓰지 않는다
### 3.2 retry는 timeout, idempotency, fallback과 분리해서 설계한다
AWS는 timeout, retry, backoff를 별도 도구로 설명하고, side effect가 있는 API는 retry 전에 idempotency 가 중요하다고 말합니다. Google Cloud도 non-idempotent operation retry를 anti-pattern으로 경고합니다.
프로젝트 규칙:
- timeout은 “얼마나 기다릴지”
- retry는 “다시 시도할지”
- idempotency는 “다시 시도해도 안전한지”
- fallback은 “재시도 후에도 실패하면 대체 경로가 있는지”
- 를 각각 따로 판단한다
### 3.3 retry는 외부 연동 경계(adapter)에 둔다
Spring이 RestClient, WebClient, HTTP Service Client 같은 외부 호출 도구를 제공하는 만큼, retry도 외부 연동 경계에서 결정하는 것이 자연스럽습니다. application/domain이 provider-specific HTTP 오류나 retry 대상 예외를 직접 다루는 구조는 피하는 것이 좋습니다.
프로젝트 규칙:
- 외부 HTTP retry는 integration adapter/client 경계에서 처리한다
- application/domain이 SocketTimeoutException, WebClientResponseException, ConnectException 분류를 직접 하지 않는다
- retry 이후 최종 실패만 내부 예외로 번역한다
## 4. 언제 retry하는가
### 4.1 retry 대상은 “일시적 실패”다
AWS는 retry가 부분 실패, 일시적 네트워크 문제, 순간적인 과부하 같은 상황에 유용하다고 설명합니다. Google Cloud도 retry는 response criteria와 idempotency criteria를 동시에 만족 하는 요청에만 적용하라고 안내합니다.
프로젝트 규칙:
다음은 retry 후보가 될 수 있다.
- connection timeout / connection reset
- read timeout / response timeout
- 일시적 5xx
- 일시적 429
- 네트워크 단절/짧은 DNS/TLS 실패
- provider가 transient failure로 명시한 에러
### 4.2 retry하지 않는 대상
Google Cloud는 retrying unretryable errors, non-idempotent operation retry를 anti-pattern으로 명시합니다. Spring 기본 retry는 모든 예외를 재시도할 수 있으므로, 이 프로젝트에서는 반드시 대상을 좁혀야 합니다.
프로젝트 규칙:
다음은 기본적으로 retry하지 않는다.
- 4xx business 오류
- 인증 실패(401/403)
- 잘못된 요청 형식(400)
- provider 계약 위반
- validation/parsing 오류
- deterministic failure
- side effect가 있는데 idempotency가 보장되지 않는 호출
### 4.3 404/409 같은 상태는 provider 계약에 따라 해석한다
Spring의 HTTP client는 status를 읽을 수 있지만, 어떤 status를 transient로 볼지는 provider 계약이 결정합니다. 일부 409/404는 정상적인 부재/중복 의미일 수 있고, 일부 429/503은 transient일 수 있습니다.
프로젝트 규칙:
- retry 가능 여부는 HTTP status 숫자만 으로 결정하지 않는다
- provider 계약 문서와 실제 운영 의미를 함께 본다
- 같은 provider 안에서는 status 해석 기준을 문서화한다
## 5. retry 전제조건
### 5.1 retry 전에는 idempotency 가능성을 확인한다
AWS는 side effect가 있는 API는 timeout/partial failure 이후 재시도 시 중복 side effect가 생길 수 있으므로 idempotent API 설계 가 중요하다고 설명합니다. Google Cloud도 non-idempotent operation retry를 anti-pattern으로 지적합니다.
프로젝트 규칙:
- side effect 없는 read 호출은 retry 허용을 더 쉽게 검토한다
- side effect 있는 write 호출은
- provider idempotency support
- 우리 쪽 idempotency key
- 중복 실행 허용 여부
- 를 먼저 확인한다
- 이 검토 없이 자동 retry를 켜지 않는다
### 5.2 retry는 timeout이 먼저 있어야 의미가 있다
timeout이 없으면 호출이 오래 붙잡힌 채 retry까지 가지 못하고, AWS도 timeout을 원격 호출의 기본 안전장치로 설명합니다. retry는 timeout과 함께 설계되어야 합니다.
프로젝트 규칙:
- retry가 있는 외부 API 호출에는 timeout이 먼저 정의되어 있어야 한다
- timeout 없이 retry만 정의하는 것을 금지한다
- retry 문서는 timeout 문서와 함께 읽는 것을 전제로 한다
## 6. backoff 규칙
### 6.1 즉시 반복 retry를 금지한다
Google Cloud는 retry without backoff 를 대표 anti-pattern으로 지적합니다. AWS도 retry는 backoff와 함께 써야 하고, 그렇지 않으면 부하를 폭증시킬 수 있다고 설명합니다.
프로젝트 규칙:
- 자동 retry는 backoff 없이 즉시 연속 재시도하지 않는다
- 최소한 exponential backoff를 기본으로 한다
- “짧게 여러 번 때리면 되겠지”를 금지한다
### 6.2 jitter를 기본으로 한다
AWS는 retries and backoff with jitter를 공식적으로 설명하고, 동시 재시도로 인한 thundering herd를 줄이기 위해 jitter가 중요하다고 강조합니다. Google Cloud도 exponential backoff with jitter를 일반적으로 권장합니다.
프로젝트 규칙:
- backoff에는 jitter를 기본 포함한다
- 같은 장애 시점에 모든 인스턴스가 같은 간격으로 동시에 재시도하지 않게 한다
- jitter 없는 fixed backoff를 기본값으로 두지 않는다
### 6.3 backoff는 무한히 커지지 않게 상한을 둔다
AWS는 exponential backoff를 설명하면서도 상한을 두고, 전체 retry budget 안에서 움직이게 설계해야 한다고 말합니다.
프로젝트 규칙:
- initial interval
- multiplier
- max interval
- max attempts
- 를 모두 명시한다
- 무한 증가형 backoff를 금지한다
## 7. retry 횟수 규칙
### 7.1 짧고 보수적인 max attempts를 기본으로 한다
Spring Framework 기본 retry는 최대 3회 재시도입니다. Google Cloud는 unnecessarily layering retries를 anti-pattern으로 지적합니다. 이 프로젝트도 외부 HTTP 호출은 짧고 보수적인 횟수를 기본으로 둡니다.
프로젝트 규칙:
- 외부 API 자동 retry 기본값은 짧게
- 일반 권장 시작점:
- 총 시도 2~3회 수준
- “10번까지 해보자” 같은 공격적 retry를 기본 금지한다
### 7.2 상위/하위 레이어 retry 중복을 금지한다
Google Cloud는 unnecessarily layering retries 를 anti-pattern으로 지적합니다. client library, gateway, adapter, application service가 모두 retry하면 실제 요청 수가 폭증할 수 있습니다.
프로젝트 규칙:
- 한 호출 경로에서 대표 retry layer를 정한다
- provider SDK가 이미 retry를 하면 우리 adapter retry를 다시 올리지 않는다
- gateway / SDK / client / application retry가 겹치지 않게 한다
## 8. 기술 선택 규칙
### 8.1 선언적 retry와 프로그래밍식 retry를 구분한다
Spring은 @Retryable 애노테이션과 RetryPolicy/RetryTemplate 계열을 제공하고, Spring Framework 7 resilience 기능도 method-level retry를 지원합니다. @Retryable은 간단하지만 기본적으로 프록시 기반이고, 대상/횟수/backoff를 명시적으로 설정해야 합니다.
프로젝트 규칙:
- 단순한 adapter method 재시도에는 선언적 retry 허용
- 복잡한 분기, 일부 코드 블록만 재시도, 동적 정책은 프로그래밍식 retry 우선
- 어떤 방식을 쓰든 retry 대상 예외와 backoff를 명시한다
### 8.2 새 코드의 기본 선택은 provider/client 구조에 맞춰 명시적으로 한다
Spring 자체는 retry 기능을 제공하지만, Boot의 외부 client 구조와 결합할 때는 “어디에 적용할지”가 더 중요합니다. integration adapter 단위의 retry가 가장 기본이며, Spring Cloud CircuitBreaker/Resilience4j 같은 도구가 있다면 공통 정책화도 가능합니다.
프로젝트 규칙:
- retry는 external client adapter 또는 그 바로 위 integration service에 둔다
- controller/application/domain에 retry 애노테이션을 붙이지 않는다
- 공통 라이브러리를 쓰더라도 source of truth는 프로젝트 문서다
## 9. observability 규칙
### 9.1 retry는 관측 가능해야 한다
AWS는 retry가 장애를 완화할 수도 있지만 반대로 악화시킬 수도 있으므로, retry 동작을 이해할 수 있어야 한다고 설명합니다. Spring/Boot의 HTTP client instrumentation과 함께 retry attempt, provider, operation, 최종 결과를 추적 가능하게 해야 합니다.
프로젝트 규칙:
retry 로그/메트릭에는 최소한 다음을 남긴다
- provider/client name
- operation
- attempt number
- final outcome
- error type
- retry가 있었는지, 몇 번 있었는지, 결국 성공/실패했는지 구분 가능해야 한다
### 9.2 최종 실패만 대표 ERROR로 남긴다
AWS/Google Cloud가 경고하는 retry storm와 중복 부하 문제를 고려하면, 중간 실패를 모두 ERROR로 남기면 운영 신호가 오염됩니다. 최종 실패만 대표 ERROR, 중간 실패는 WARN 또는 DEBUG가 기본입니다.
프로젝트 규칙:
- 중간 retry 실패: WARN 또는 DEBUG
- retry 후 성공: WARN 또는 INFO
- retry 후 최종 실패: 대표 ERROR
## 10. provider 계약과의 관계
### 10.1 Retry-After 같은 provider 신호를 존중한다
HTTP/공급자 문서가 retry 간격이나 throttling 신호를 주는 경우, 그 신호를 우선 고려하는 것이 일반적 운영 원칙입니다. Google Cloud도 response criteria를 보고 retry해야 한다고 설명합니다.
프로젝트 규칙:
- provider가 Retry-After 또는 throttling 가이드를 주면 우선 따른다
- 로컬 backoff 정책이 provider 신호와 충돌하지 않게 한다
- provider rate limit 계약을 무시한 retry를 금지한다
### 10.2 provider별 retryable 오류 목록을 문서화한다
프로젝트 규칙:
각 주요 외부 시스템별로
- retryable status
- retryable exception
- non-retryable business error
를 문서화한다
코드 안 산발적인 if status == 503 식 분기를 줄인다
## 11. 이 문서와 다른 문서의 경계
이 문서는 외부 API retry만 다룬다. 아래 주제의 source of truth는 별도 문서다.
- timeout
- idempotency
- fallback
- exception translation
- serialization/deserialization
이 문서는 위 내용을 반복하지 않고, 외부 연동 retry에서 어디까지 함께 고려해야 하는지만 정의한다.
## 12. 금지 규칙
다음은 기본 금지다.
- 모든 예외 retry
- backoff 없는 즉시 재시도
- jitter 없는 고정 간격 재시도 기본값
- side effect API를 idempotency 검토 없이 자동 retry
- SDK + gateway + adapter + application 중복 retry
- retry 대상 아닌 4xx/business error retry
- retry가 있는데 timeout이 없음
- retry 동작이 관측되지 않음
## 13. 체크리스트
다음 질문에 “예”로 답할 수 있어야 한다.
- 이 실패는 정말 transient인가?
- 이 호출은 retry해도 안전한가, 특히 idempotent한가?
- timeout이 먼저 정의되어 있는가?
- retry 대상 예외/status가 명시되어 있는가?
- backoff와 jitter가 있는가?
- max attempts가 짧고 보수적인가?
- 상위/하위 레이어 retry 중복이 없는가?
- retry attempt와 최종 결과가 관측 가능한가?
@@ -0,0 +1,279 @@
# Integration Serialization / Deserialization 기준
## 1. 목적
이 문서는 외부 API / integration 호출에서 request serialization과 response deserialization 기준을 정의한다.
이 문서의 목표는 다음과 같다.
- provider 계약과 내부 모델을 분리한다
- 외부 payload 변화에 대한 내성을 높인다
- media type, 필드명, null/absent, 에러 바디, 날짜/시간 포맷을 일관되게 처리한다
- serialization concern이 application/domain으로 번지지 않게 한다
## 2. 근거 수준
- Official: Spring Framework / Spring Boot / Jackson 공식 문서에서 직접 확인되는 내용
- Official + Practice: 공식 기능 위에 Tolerant Reader 같은 실무 패턴을 결합한 내용
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
## 3. 기본 원칙
### 3.1 외부 serialization은 provider contract가 결정한다
Spring의 REST client는 HTTP 본문을 상위 Java 객체로 읽고 쓰며, JSON 외에도 application/x-www-form-urlencoded, multipart, byte array, XML 같은 형식을 converter로 다룹니다. 따라서 외부 연동 직렬화 기준은 우리 내부 JSON 취향이 아니라 provider가 요구하는 content type, field shape, wire format 이 먼저다.
프로젝트 규칙:
- provider가 JSON을 요구하면 JSON으로
- provider가 form-urlencoded를 요구하면 form으로
- provider가 XML/byte/binary를 요구하면 그 형식으로 보낸다
- “우리 서비스는 JSON 표준이 있으니 외부도 JSON으로 통일”을 금지한다
### 3.2 외부 DTO와 내부 모델은 반드시 분리한다
Spring client가 DTO 변환을 쉽게 해 준다고 해서, provider DTO를 내부 모델처럼 써도 된다는 뜻은 아니다. Tolerant Reader 관점에서도 payload를 읽는 코드는 한 경계에 모아야 나머지 시스템이 변경에 덜 민감해진다.
프로젝트 규칙:
- 외부 request DTO / response DTO는 provider 계약 전용 타입
- application/domain은 외부 DTO를 직접 모른다
- adapter 경계에서 내부 command/result/failure reason으로 변환한다
### 3.3 읽기는 느슨하게, 쓰기는 명시적으로 한다
Jackson의 ignoreUnknown=true는 외부 응답이 필드를 추가해도 파싱을 덜 깨뜨리게 해 준다. 반면 outbound request는 provider가 받지 않는 필드를 보내거나, null과 absent를 헷갈리게 보내면 계약 오류가 생긴다. 따라서 외부 연동에서는 response는 tolerant reader, request는 explicit writer 전략을 기본으로 둔다.
프로젝트 규칙:
- 외부 response DTO는 additive change에 관대할 수 있다
- 외부 request DTO는 보내는 필드를 명시적으로 통제한다
- 내부 객체를 그대로 직렬화해서 provider에 보내지 않는다
## 4. Request serialization 표준
### 4.1 outbound request는 provider 계약에 정확히 맞춘다
Spring REST client는 DTO를 바탕으로 본문을 직렬화하지만, 실제 field name과 media type은 adapter가 정해야 한다. Jackson의 @JsonProperty는 외부 필드명을 DTO 경계에서 맞추는 공식 수단이다.
프로젝트 규칙:
- provider field name mismatch는 외부 request DTO에서 해결한다
- 내부 필드명/도메인 용어를 provider 계약에 맞춰 바꾸지 않는다
- provider-specific enum/string/value shape를 request DTO에 국소화한다
### 4.2 request DTO는 allowlist 방식으로 설계한다
프로젝트 규칙:
- provider에 보낼 필드만 request DTO에 둔다
- 내부 계산값, 디버그 값, 서버 내부 상태를 request DTO에 섞지 않는다
- “언젠가 쓸 수 있으니 같이 보내자”를 금지한다
### 4.3 null과 absent는 provider 계약 기준으로 명시한다
프로젝트 규칙:
- provider가 null과 필드 omission을 다르게 해석하면 반드시 구분한다
- 기본 정책은 “의미가 다르면 DTO와 mapper에서 명시적으로 처리”
- 전역 NON_NULL 같은 설정으로 provider별 의미를 무심코 바꾸지 않는다
### 4.4 media type은 명시적으로 맞춘다
Spring converter는 JSON, form, multipart, byte array, XML 등을 지원한다. 외부 연동에서는 특히 OAuth/token 발급, webhook, 파일 업로드, binary download처럼 JSON이 아닌 형식이 흔하다.
프로젝트 규칙:
- application/json을 기본 추정값으로 두지 않는다
- provider가 요구하는 Content-Type과 Accept를 adapter에서 명시한다
- form 요청은 JSON DTO를 억지로 보내지 않는다
## 5. Response deserialization 표준
### 5.1 external response DTO는 tolerant reader를 기본 검토한다
Jackson의 @JsonIgnoreProperties(ignoreUnknown = true)는 인식하지 못한 필드를 deserialization에서 무시한다. Fowler의 Tolerant Reader도 producer가 필드를 추가해도 consumer가 덜 깨지도록 payload reading을 느슨하게 설계하라고 설명한다.
프로젝트 규칙:
- third-party response DTO는 ignoreUnknown=true를 기본 검토한다
- provider가 필드를 추가해도 우리 파싱이 즉시 깨지지 않게 한다
- 단, first-party API request DTO까지 이 정책을 일반화하지 않는다
### 5.2 success body와 error body를 분리한다
프로젝트 규칙:
- 성공 응답 DTO와 오류 응답 DTO를 따로 둔다
- provider error JSON을 success DTO에 억지로 파싱하지 않는다
- error body는 adapter가 읽고 내부 failure reason/exception으로 번역한다
### 5.3 raw Map/JsonNode는 마지막 수단이다
Spring/Jackson은 상위 객체 매핑과 custom deserializer를 지원한다. 따라서 외부 응답 구조가 완전히 동적이지 않다면 typed DTO가 기본이다. raw map/tree는 계약이 너무 불안정하거나 일부 필드만 읽을 때의 마지막 수단으로 본다.
프로젝트 규칙:
- 기본은 typed response DTO
- 정말 불안정한 payload만 JsonNode/Map 허용
- raw tree를 application/domain까지 들고 가지 않는다
- boundary에서 읽고 안정적인 내부 모델로 바꾼다
### 5.4 외부 enum은 바로 domain enum에 연결하지 않는다
프로젝트 규칙:
- provider enum/string 값은 외부 DTO 또는 mapper 단계에서 해석한다
- domain enum에 provider 값을 직접 박아 넣지 않는다
- provider가 새 enum 값을 추가할 수 있으면 UNKNOWN/기본 처리 전략을 둔다
## 6. 날짜/시간/숫자 규칙
### 6.1 날짜/시간 형식은 provider 계약을 따른다
Jackson의 JavaTimeModule은 java.time 타입을 지원하고, timestamps 기능이 꺼져 있으면 보통 ISO-8601 문자열을 사용한다. 하지만 외부 연동에서는 provider가 epoch millis, string, custom format 중 무엇을 쓰는지가 더 중요하다.
프로젝트 규칙:
- provider가 ISO-8601을 쓰면 Instant/OffsetDateTime 등으로 명시적으로 읽는다
- provider가 epoch number를 쓰면 그 계약을 DTO/커스텀 deserializer에서 처리한다
- 내부 표준 시간 타입을 provider wire format 때문에 오염시키지 않는다
### 6.2 숫자/정밀도는 domain 의미를 잃지 않게 한다
프로젝트 규칙:
- 금액, 환율, 정산 수치처럼 정밀도가 중요한 값은 double을 기본값으로 두지 않는다
- provider가 문자열 금액을 보내면 문자열 → 안전한 내부 수치 타입으로 변환한다
- 숫자 파싱 실패는 provider parsing failure로 다루고 domain 예외와 섞지 않는다
## 7. Jackson / mapper / module 규칙
### 7.1 메서드 안에서 new ObjectMapper()를 만들지 않는다
Spring Boot는 auto-configured JSON mapper와 RestClient.Builder/WebClient.Builder를 제공하고, 그 builder에는 converter/codecs와 적절한 공통 구성이 반영된다. 메서드마다 새 mapper를 만들면 그 구성을 우회하게 된다.
프로젝트 규칙:
- adapter 메서드 안 new ObjectMapper() 금지
- 공통 builder와 공통 mapper를 우선 사용한다
- provider 특수 규칙이 있으면 adapter configuration에서 분리해 구성한다
### 7.2 전역 @JacksonComponent / @JacksonMixin은 진짜 공통 규칙에만 쓴다
Spring Boot는 @JacksonComponent를 자동 등록하고, @JacksonMixin도 auto-configured mapper에 등록한다. 즉, 이 둘은 전역 영향 이 있다. 따라서 provider 하나만을 위한 특수 직렬화 규칙을 전역에 뿌리는 것은 신중해야 한다.
프로젝트 규칙:
- 여러 연동/여러 DTO에 공통인 serializer/deserializer만 전역 등록
- 특정 provider 전용 weird format은 adapter-local configuration 우선
- provider 하나 때문에 전체 애플리케이션 JSON 규칙을 바꾸지 않는다
### 7.3 imperative/reactive client가 쓰는 JSON mapper 경계를 의식한다
Boot는 imperative HTTP clients와 reactive HTTP clients에 대해 각각 pre-configured builder를 제공하고, preferred JSON mapper 설정도 분리해 둔다.
프로젝트 규칙:
- RestClient/WebClient에서 provider-specific codec/mapper를 바꿀 때 범위를 명시한다
- imperative client용 변경이 reactive client 전체에 번지지 않게 한다
- “한 군데 바꾸면 다 되겠지” 식 전역 변경을 지양한다
## 8. 검증 / 번역 규칙
### 8.1 파싱 성공과 비즈니스 성공을 같은 것으로 보지 않는다
프로젝트 규칙:
- JSON/XML/form parsing 성공은 “wire format 해석 성공”일 뿐
- provider가 business failure body를 200으로 줄 수도 있다
- adapter는 파싱 후에 success/error semantics를 다시 해석한다
### 8.2 deserialization 예외는 provider parsing failure로 번역한다
프로젝트 규칙:
- malformed payload, required field missing, unexpected type mismatch는 integration parsing failure로 번역한다
- application/domain이 Jackson 예외 타입을 직접 보지 않게 한다
- provider contract drift 여부를 운영에서 추적 가능하게 한다
## 9. 관측 가능성 규칙
### 9.1 payload 전문 로그를 기본 금지한다
외부 payload는 PII, 토큰, 비밀값, 내부 식별자 등을 포함할 수 있다. 이전 observability 기준과 마찬가지로, serialization/deserialization 문제를 추적한다는 이유로 request/response 전문을 기본 로그에 남기지 않는다. 이 점은 OWASP의 민감정보 로그 금지 원칙과도 맞다.
프로젝트 규칙:
- 기본 로그는 provider, operation, status, contentType, payloadBytes, parse failure type 정도만
- payload 원문은 기본 금지
- 꼭 필요하면 테스트/격리 환경에서 제한적으로 남긴다
### 9.2 parse failure는 contract drift 신호로 남긴다
프로젝트 규칙:
- deserialization 실패는 단순 예외로 묻지 않는다
- provider, operation, content type, failing field/shape 정도를 안전하게 남긴다
- “provider contract가 변했을 수 있음”을 운영에서 추적할 수 있어야 한다
## 10. 테스트 규칙
### 10.1 외부 DTO는 fixture 기반 계약 테스트를 둔다
프로젝트 규칙:
- 대표 성공 응답
- 대표 오류 응답
- provider가 필드를 추가한 응답
- 일부 필드 누락 응답
- 에 대한 parsing 테스트를 둔다
- provider 예시 payload나 실제 캡처 샘플을 fixture로 관리할 수 있다
### 10.2 request serialization도 golden sample로 확인한다
프로젝트 규칙:
- provider에 보내는 JSON/form/XML shape를 golden sample로 검증한다
- field name, null/absent, date/time format, enum value가 계약대로 직렬화되는지 확인한다
- “직렬화는 framework가 알아서 하겠지”에 기대지 않는다
## 11. 다른 문서와의 경계
이 문서는 외부 provider payload의 serialization/deserialization 만 다룬다.
아래 주제의 source of truth는 별도 문서다.
- external API client structure
- timeout
- retry
- outbound idempotency
- fallback
- exception translation
이 문서는 위 문서를 반복하지 않고, payload contract를 읽고 쓰는 경계 규칙 만 정의한다.
## 12. 금지 규칙
다음은 기본 금지다.
- 내부 domain/entity를 외부 request/response DTO로 직접 사용
- provider response DTO를 application/domain 시그니처에 그대로 전달
- 메서드 안 new ObjectMapper() 생성
- 특정 provider 응답 대응을 위해 전역 mapper 규칙을 무심코 변경
- first-party API strict 정책과 external response tolerant 정책을 혼동
- success/error body를 같은 DTO로 억지 파싱
- payload 전문 로그를 기본으로 남김
- 외부 enum/string 값을 바로 domain enum에 박아 넣음
## 13. 체크리스트
다음 질문에 “예”로 답할 수 있어야 한다.
- provider request/response DTO와 내부 모델이 분리되어 있는가?
- outbound request가 provider contract를 정확히 반영하는가?
- external response는 additive change에 대해 필요한 만큼 tolerant한가?
- success body와 error body DTO가 분리되어 있는가?
- null과 absent 의미를 provider 계약 기준으로 다루는가?
- provider-specific weird format이 adapter 경계 안에 갇혀 있는가?
- RestClient/WebClient의 공통 builder/mapper 구성을 우회하지 않는가?
- serialization/deserialization fixture 테스트가 있는가?
+286
View File
@@ -0,0 +1,286 @@
# Timeout 기준
## 1. 목적
이 문서는 외부 API 호출 timeout의 의미, 위치, 기본 정책을 정의한다.
이 문서의 목표는 다음과 같다.
- 외부 연동 호출이 무기한 대기하지 않게 한다
- connect/read/response/pool acquire 같은 timeout 종류를 구분한다
- timeout을 retry, fallback, idempotency와 혼동하지 않게 한다
- provider별/operation별 timeout을 일관되게 설계한다
## 2. 근거 수준
- Official: Spring Boot / Spring Framework / Reactor Netty 공식 문서에서 직접 확인되는 내용
- Official + Practice: 공식 기능 위에 AWS Builders Library 같은 실무 운영 원칙을 결합한 내용
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
## 3. 기본 원칙
### 3.1 모든 원격 호출에는 timeout이 있어야 한다
AWS는 원격 호출에는 timeout을 두는 것이 모범 사례이며, 같은 프로세스 내부가 아닌 프로세스 간 호출 전반에 timeout을 두라고 설명합니다. timeout이 없으면 오래 걸리는 요청이 메모리, 스레드, 연결, ephemeral port 같은 자원을 오래 붙잡아 시스템 전체를 악화시킬 수 있습니다. Reactor Netty도 response timeout을 설정하는 것이 좋은 실천이라고 명시합니다.
프로젝트 규칙:
- 외부 HTTP 호출에 무제한 대기 금지
- 모든 외부 API client는 최소한 timeout 정책을 가져야 한다
- timeout이 설정되지 않은 외부 연동 코드는 승인 후보에서 지양한다
### 3.2 timeout은 retry가 아니다
AWS는 timeout, retry, backoff를 별도 도구로 설명하며, timeout은 대기 시간을 제한하고, retry는 부분 실패/일시 실패를 다시 시도하는 메커니즘이라고 설명합니다. timeout이 발생했다고 해서 side effect가 없었다고 보장되지 않으며, side effect가 있는 API는 idempotency가 있어야 안전하게 재시도할 수 있다고도 말합니다.
프로젝트 규칙:
- timeout은 “언제 포기할지”를 정하는 규칙
- retry는 “포기 전에 다시 시도할지”를 정하는 규칙
- timeout 문서에서 retry를 중복 정의하지 않는다
- side effect가 있는 외부 API는 timeout 이후 재시도 전에 idempotency 가능 여부를 반드시 검토한다
### 3.3 timeout은 외부 시스템별, operation별로 달라질 수 있다
AWS는 timeout 값을 고를 때 downstream latency와 허용 가능한 false timeout 비율을 보고 정해야 하며, 인터넷 구간처럼 네트워크 편차가 큰 경우와 p99.9와 p50이 가까운 tight latency 서비스는 같은 방식으로 잡으면 안 된다고 설명합니다.
프로젝트 규칙:
- 외부 시스템마다 timeout이 다를 수 있다
- 같은 외부 시스템 안에서도 operation별 timeout이 다를 수 있다
- “전 서비스 공통 3초” 같은 일괄값을 기본 전략으로 두지 않는다
- timeout은 실제 latency와 business 중요도에 근거해 결정한다
## 4. timeout 종류 구분
### 4.1 최소한 connect timeout과 read/response timeout을 구분한다
Spring Boot는 전역 HTTP client 설정으로 spring.http.clients.connect-timeout와 spring.http.clients.read-timeout를 제공합니다. HTTP Service group도 connection/read timeout을 그룹별로 설정할 수 있습니다. Reactor Netty는 response timeout과 connection timeout을 별도 개념으로 설명합니다.
프로젝트 규칙:
- 외부 HTTP client는 최소한 다음 둘을 구분한다
- connect timeout
- read/response timeout
- 연결이 안 되는 상황과, 연결은 되었지만 응답이 늦는 상황을 같은 timeout 하나로 퉁치지 않는다
### 4.2 reactive client에서는 pool acquire / TLS / DNS도 별도 고려 대상이다
Reactor Netty는 connection pool acquire timeout, SSL/TLS handshake timeout, proxy timeout, DNS query timeout까지 별도 timeout 옵션으로 설명합니다. connection pool의 pendingAcquireTimeout 기본값은 45초이고, SSL handshake timeout 기본값은 10초이며, DNS query timeout 기본값은 5초입니다.
프로젝트 규칙:
WebClient + Reactor Netty를 쓸 때는 단순 response timeout만 볼 것이 아니라 다음도 검토한다
- connection pool acquire timeout
- SSL handshake timeout
- DNS resolution timeout
- 트래픽이 많거나 TLS/프록시/DNS 영향이 큰 환경에서는 이 고급 timeout을 운영 설계에 포함한다
### 4.3 “전체 호출 deadline”과 client-level timeout을 구분한다
Reactor Netty는 specific timeout 옵션을 두는 편이 Reactor timeout 연산자보다 더 목적에 맞는 제어를 준다고 설명합니다. timeout 연산자는 연결부터 응답 수신까지 전체 동작에 걸리는 시간을 통째로 제한하지만, client-specific timeout은 더 세밀합니다.
프로젝트 규칙:
- client-level timeout은 connect/read/pool/TLS 같은 기술적 단계별 timeout
- business/application deadline은 “이 유스케이스가 전체적으로 몇 초 안에 끝나야 하는가”라는 별도 개념
- 둘을 혼동하지 않는다
- reactive 체인 전체에 무턱대고 timeout()만 거는 것을 기본값으로 두지 않는다
## 5. client 종류별 표준
### 5.1 RestClient 기본값은 Boot 전역 설정 + provider별 override다
Spring Boot는 RestClient.Builder를 자동 구성하고, spring.http.clients.connect-timeout / read-timeout 같은 전역 속성을 제공합니다. 또한 HTTP Service client group별로 connection/read timeout을 다르게 둘 수 있습니다.
프로젝트 규칙:
- imperative 외부 HTTP 호출 기본값은 RestClient
- timeout 기본값은 전역 spring.http.clients.*
- provider별/그룹별 차이는 spring.http.serviceclient.<group> 또는 전용 configuration에서 override
- RestClient.create()를 직접 만들어 timeout 구성을 우회하지 않는다
### 5.2 WebClient는 Reactor Netty timeout까지 함께 본다
Spring Boot는 WebClient.Builder를 자동 구성하고, Boot가 제공하는 builder를 주입해서 쓰는 것을 강하게 권장합니다. Spring Framework는 Reactor Netty HttpClient를 미리 구성해 ReactorClientHttpConnector로 WebClient에 붙일 수 있다고 설명합니다. Reactor Netty는 response timeout, connect timeout, pool timeout, TLS timeout, DNS timeout을 별도로 제공합니다.
프로젝트 규칙:
- reactive/non-blocking 외부 호출은 WebClient
- 단순 Boot 전역 read-timeout만 믿지 않고, 필요하면 Reactor Netty HttpClient를 명시적으로 구성한다
- 대기 원인이 connection인지 response인지 pool acquire인지 구분 가능한 구조를 선호한다
### 5.3 HTTP Service Client는 group 속성으로 timeout을 관리한다
Spring Boot는 @ImportHttpServices와 group 개념을 제공하고, spring.http.serviceclient.<group-name> 아래에서 base URL, default headers, redirect, connection/read timeout, SSL bundle 등을 설정할 수 있다고 설명합니다.
프로젝트 규칙:
- 선언형 HTTP interface client를 쓸 때는 timeout도 group 단위로 관리한다
- interface마다 개별 하드코딩하지 않는다
- 같은 provider 아래 여러 인터페이스가 공통 timeout을 공유하게 한다
## 6. timeout 값 선택 기준
### 6.1 timeout은 downstream latency와 허용 가능한 false timeout 비율로 잡는다
AWS는 intra-region 서비스 호출의 경우 허용 가능한 false timeout 비율(예: 0.1%)을 먼저 정하고, downstream latency percentile(예: p99.9)을 참고해 timeout을 고르는 방식을 권장합니다.
프로젝트 규칙:
- timeout 값은 “감”으로 정하지 않는다
- 가능하면 provider/operation latency 기준을 본다
- 기본 질문은 다음과 같다
- 이 호출이 몇 ms/초 이상 걸리면 사실상 실패로 봐야 하는가?
- false timeout을 얼마나 허용할 것인가?
- timeout 이후 retry/fallback이 가능한가?
### 6.2 인터넷 구간과 내부 구간은 같은 값으로 잡지 않는다
AWS는 인터넷처럼 네트워크 편차가 큰 경우에는 downstream percentile만 보고 timeout을 잡으면 안 되고, reasonable worst-case network latency를 추가로 고려해야 한다고 설명합니다.
프로젝트 규칙:
같은 데이터센터/같은 리전에 있는 내부 서비스 호출과
인터넷을 거치는 SaaS/third-party 호출은
timeout 기준을 다르게 둔다
외부 공개 인터넷 API는 더 큰 네트워크 변동성을 감안한다
### 6.3 너무 낮은 timeout은 배포/콜드 커넥션/TLS 구간에서 오탐을 만든다
AWS는 아주 낮은 timeout(예: 20ms)을 썼을 때 배포 직후 새 secure connection 수립 시간이 timeout에 포함되어 오탐이 생긴 사례를 설명하며, 이후 연결을 미리 준비(prewarm)하는 방식으로 개선했다고 말합니다.
프로젝트 규칙:
- timeout을 지나치게 공격적으로 줄이지 않는다
- 새 연결 수립, TLS handshake, DNS lookup이 포함되는지 확인한다
- 낮은 timeout을 쓰려면 connection reuse/prewarm 전략도 함께 검토한다
### 6.4 “tight latency service”에는 padding을 둔다
AWS는 p99.9와 p50이 가까운 서비스에서는 작은 latency 증가에도 timeout이 급증할 수 있으므로 padding을 두라고 설명합니다.
프로젝트 규칙:
- timeout은 percentile 값에 기계적으로 딱 맞추지 않는다
- 급격한 오탐 증가를 막기 위한 안전 여유를 둔다
- 극단적으로 빡빡한 timeout은 특별한 근거가 있을 때만 허용한다
## 7. operation별 기준
### 7.1 사용자 요청 경로의 외부 호출은 더 엄격한 timeout을 가진다
프로젝트 규칙:
- synchronous request path 안의 외부 호출은 사용자 응답 SLA를 고려해 더 엄격한 timeout을 둔다
- 장시간 대기가 UX와 thread/resource 점유를 악화시키는 경우가 많다
- “느리지만 언젠가 오면 된다”는 기준을 기본값으로 두지 않는다
### 7.2 백그라운드/배치 호출은 더 긴 timeout을 가질 수 있다
프로젝트 규칙:
- 배치/백그라운드 호출은 사용자 직접 응답보다 긴 timeout을 가질 수 있다
- 다만 무기한 대기를 허용하지는 않는다
- 작업 단위 SLA와 재시도/보상 전략을 함께 본다
### 7.3 읽기와 쓰기 호출을 구분한다
AWS는 side effect가 있는 API는 timeout 이후 retry가 중복 side effect를 만들 수 있으므로 idempotency가 중요하다고 설명합니다. timeout 자체도 읽기 호출과 쓰기 호출의 의미가 다를 수 있습니다.
프로젝트 규칙:
- read-only 조회는 상대적으로 더 짧은 timeout을 선호할 수 있다
- side effect가 있는 write 호출은 timeout 후 retry 가능성까지 함께 본다
- “timeout 값만” 정하지 말고, 그 timeout 이후 어떤 동작이 이어질지도 함께 문서화한다
## 8. 설정 위치 규칙
### 8.1 전역 기본값은 공통 설정으로 둔다
Spring Boot는 모든 HTTP client에 적용되는 전역 spring.http.clients.* 속성을 제공합니다.
프로젝트 규칙:
- connect/read timeout의 공통 기본값은 전역 설정으로 둔다
- 서비스 전체 기본값을 문서화한다
- 각 adapter가 제각각 timeout을 하드코딩하지 않는다
### 8.2 provider별 차이는 group 또는 전용 configuration으로 override한다
Spring Boot는 HTTP Service group에 대해 base URL, headers, redirect, connect/read timeout, SSL bundle 등을 그룹별로 둘 수 있다고 설명합니다. 또한 RestClient/WebClient는 injected builder에 좁은 범위 customization을 추가하는 방식을 권장합니다.
프로젝트 규칙:
- provider별 timeout 차이는 group 설정 또는 전용 configuration으로 둔다
- adapter 생성자 안 상수 하드코딩을 기본 금지한다
- 왜 override가 필요한지 근거를 남긴다
### 8.3 operation별 차이는 client 내부에서 명시적으로 표현한다
Reactor Netty는 기본 response timeout 외에 request별 response timeout override도 지원합니다.
프로젝트 규칙:
- 같은 provider 안에서도 operation별로 timeout이 다르면 코드에 의도를 드러낸다
- “특정 operation만 더 길다/짧다”를 숨긴 magic number를 금지한다
- operation별 override는 드물고 명시적이어야 한다
## 9. observability 규칙
### 9.1 timeout은 관측 가능해야 한다
Spring Boot는 auto-configured builders를 통해 HTTP client instrumentation을 함께 적용할 수 있다고 설명합니다. timeout이 일어나도 어떤 provider, 어떤 operation, 어느 단계에서 발생했는지 관측 가능해야 운영이 됩니다.
프로젝트 규칙:
timeout 발생 시 최소한 다음 맥락을 로그/메트릭에서 추적 가능하게 한다
- provider 또는 client name
- operation
- method
- uri template
- timeout type(connect/read/response/pool 등)
- “그냥 timed out” 한 줄 로그로 끝내지 않는다
### 9.2 timeout은 retry/fallback과 함께 해석 가능해야 한다
AWS는 timeout, retry, backoff를 함께 설계해야 하고, retry는 부하를 악화시킬 수 있다고 설명합니다.
프로젝트 규칙:
timeout 로그에는 retry/fallback 결과 맥락이 이어져야 한다
- timeout이 났지만 retry 후 성공했는지
- timeout이 최종 실패인지
- fallback으로 복구됐는지
를 운영자가 구분할 수 있어야 한다
## 10. 금지 규칙
다음은 기본 금지다.
- 외부 HTTP 호출에 무제한 timeout
- connect/read/response timeout을 구분하지 않고 하나의 감각적 숫자로 통일
- RestClient.create() / WebClient.builder() 직접 생성으로 공통 timeout 설정 우회
- base URL, timeout을 adapter 코드 안에 상수로 하드코딩
- reactive 체인 전체에 무턱대고 timeout()만 걸어 세부 원인을 잃어버림
- 매우 낮은 timeout을 두고 TLS/DNS/새 연결 비용을 고려하지 않음
- timeout 이후 retry/idempotency 전략 없이 side effect 호출을 재시도
- timeout 발생 로그에 provider/operation 맥락이 없음
## 11. 체크리스트
다음 질문에 “예”로 답할 수 있어야 한다.
- 이 외부 호출에는 timeout이 명시되어 있는가?
- 최소 connect timeout과 read/response timeout을 구분하고 있는가?
- timeout 값이 downstream latency와 business SLA에 근거하는가?
- 인터넷 구간, TLS handshake, DNS 비용을 고려했는가?
- timeout 기본값과 provider별 override 위치가 일관적인가?
- reactive client라면 pool acquire / TLS / DNS timeout도 필요한지 검토했는가?
- timeout 이후 retry/fallback/idempotency 동작이 함께 설계돼 있는가?
- timeout 발생 시 provider/operation/timeout type이 관측 가능한가?