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

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이다
  • 실제 요청 수와 부하가 폭증할 수 있다.