Files
project-auth-server/docs/examples/integration/retry.md
T

160 lines
3.8 KiB
Markdown

# Retry 예시
## 좋은 예시
### 예시 1. retry 대상 예외와 backoff를 명시한다
```java
@Component
@RequiredArgsConstructor
public class ExternalTokenGateway {
private final ExternalTokenClient externalTokenClient;
@Retryable(
retryFor = {
SocketTimeoutException.class,
ConnectException.class,
ResourceAccessException.class
},
noRetryFor = {
IllegalArgumentException.class,
ExternalAuthenticationRejectedException.class
},
maxAttempts = 3,
backoff = @Backoff(delay = 300, maxDelay = 2_000, multiplier = 2.0, random = true)
)
public TokenResult issueToken(TokenCommand command) {
return externalTokenClient.issueToken(command);
}
}
```
**좋은 이유:**
- retry 대상을 좁혔다
- business rejection은 제외했다
- backoff + jitter 성격(random = true)을 명시했다.
### 예시 2. 최종 실패만 내부 예외로 번역한다
```java
@Component
@RequiredArgsConstructor
public class KeycloakTokenClient {
private final RestClient restClient;
public TokenResult issue(TokenCommand command) {
try {
return doIssue(command);
} catch (HttpServerErrorException | ResourceAccessException ex) {
throw new ExternalAuthTemporaryFailureException(ex);
} catch (HttpClientErrorException.Unauthorized ex) {
throw new ExternalAuthRejectedException(ex);
}
}
private TokenResult doIssue(TokenCommand command) {
return restClient.post()
.uri("/protocol/openid-connect/token")
.body(command)
.retrieve()
.body(TokenResult.class);
}
}
```
**좋은 이유:**
- provider-specific HTTP 오류를 내부 의미로 번역한다
- application이 raw HTTP client 예외를 직접 보지 않는다
### 예시 3. retry 후 성공은 WARN으로 남긴다
```java
log.warn("External auth request succeeded after retry. provider={} operation={} attempts={}",
"keycloak", "issue-token", attemptCount);
```
**좋은 이유:**
- 중간 장애 징후를 추적 가능하게 남긴다
- 최종 성공을 장애처럼 ERROR로 과장하지 않는다
### 예시 4. provider rate limit 신호를 존중한다
```java
if (response.getStatusCode().value() == 429) {
Duration retryAfter = parseRetryAfter(response.getHeaders());
throw new RetryableRateLimitedException(retryAfter);
}
```
**좋은 이유:**
- provider가 주는 throttling 신호를 반영할 수 있다
- 무작정 같은 간격으로 재시도하지 않는다
## 나쁜 예시
### 예시 1. 모든 예외를 그대로 retry한다
```java
@Retryable
public void callExternalApi() {
// ...
}
```
**나쁜 이유:**
- Spring 기본값은 모든 예외를 재시도할 수 있다
- deterministic failure와 business rejection까지 재시도될 수 있다.
### 예시 2. backoff 없이 즉시 재시도한다
```java
for (int i = 0; i < 3; i++) {
try {
return call();
} catch (Exception ignored) {
}
}
```
**나쁜 이유:**
- retry without backoff anti-pattern이다
- 순간 장애 시 부하를 더 키운다.
### 예시 3. side effect API를 idempotency 검토 없이 다시 호출한다
```java
try {
paymentClient.capture(request);
} catch (TimeoutException ex) {
paymentClient.capture(request);
}
```
**나쁜 이유:**
- timeout은 side effect 미발생을 보장하지 않는다
- non-idempotent retry anti-pattern에 가깝다.
### 예시 4. SDK retry와 adapter retry를 동시에 켠다
```java
public void send() {
sdkClient.send(); // SDK 내부 retry 있음
}
```
그리고 바깥에서 다시 @Retryable 적용
**나쁜 이유:**
- retry layering anti-pattern이다
- 실제 요청 수와 부하가 폭증할 수 있다.