3.8 KiB
3.8 KiB
Retry 예시
좋은 예시
예시 1. retry 대상 예외와 backoff를 명시한다
@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. 최종 실패만 내부 예외로 번역한다
@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으로 남긴다
log.warn("External auth request succeeded after retry. provider={} operation={} attempts={}",
"keycloak", "issue-token", attemptCount);
좋은 이유:
- 중간 장애 징후를 추적 가능하게 남긴다
- 최종 성공을 장애처럼 ERROR로 과장하지 않는다
예시 4. provider rate limit 신호를 존중한다
if (response.getStatusCode().value() == 429) {
Duration retryAfter = parseRetryAfter(response.getHeaders());
throw new RetryableRateLimitedException(retryAfter);
}
좋은 이유:
- provider가 주는 throttling 신호를 반영할 수 있다
- 무작정 같은 간격으로 재시도하지 않는다
나쁜 예시
예시 1. 모든 예외를 그대로 retry한다
@Retryable
public void callExternalApi() {
// ...
}
나쁜 이유:
- Spring 기본값은 모든 예외를 재시도할 수 있다
- deterministic failure와 business rejection까지 재시도될 수 있다.
예시 2. backoff 없이 즉시 재시도한다
for (int i = 0; i < 3; i++) {
try {
return call();
} catch (Exception ignored) {
}
}
나쁜 이유:
- retry without backoff anti-pattern이다
- 순간 장애 시 부하를 더 키운다.
예시 3. side effect API를 idempotency 검토 없이 다시 호출한다
try {
paymentClient.capture(request);
} catch (TimeoutException ex) {
paymentClient.capture(request);
}
나쁜 이유:
- timeout은 side effect 미발생을 보장하지 않는다
- non-idempotent retry anti-pattern에 가깝다.
예시 4. SDK retry와 adapter retry를 동시에 켠다
public void send() {
sdkClient.send(); // SDK 내부 retry 있음
}
그리고 바깥에서 다시 @Retryable 적용
나쁜 이유:
- retry layering anti-pattern이다
- 실제 요청 수와 부하가 폭증할 수 있다.