RetryTemplate 을 사용하지 않고, FlakyApiClient (일시 오류 50%) 에 대해 재시도 없이 1,000건을 호출했을 때의 성공률과, RetryTemplate(maxAttempts=3, none 백오프) 로 호출했을 때의 성공률을 비교 출력하세요. 이론적으로 3회 시도 시 성공률이 얼마여야 하는지 주석으로 계산하세요.
FlakyApiClient noRetryApi = new FlakyApiClient(10, 0.50, 0);
int ok1 = 0;
for (long id = 1; id <= 1000; id++) {
try { noRetryApi.send(id); ok1++; } catch (Exception ignored) {}
}
FlakyApiClient retryApi = new FlakyApiClient(10, 0.50, 0);
RetryTemplate retry = new RetryTemplate(3, BackoffPolicy.none(),
Set.of(FlakyApiClient.TransientApiException.class), null);
int ok2 = 0;
for (long id = 1; id <= 1000; id++) {
final long fid = id;
try { retry.execute(() -> retryApi.send(fid)); ok2++; } catch (Exception ignored) {}
}
System.out.printf("재시도 없음: %.1f%% / 3회 시도: %.1f%% (호출 %d회, 재시도 %d회)%n",
ok1 / 10.0, ok2 / 10.0, retryApi.calls(), retry.totalRetries());
// 이론: 1회 성공률 50%. 3회 모두 실패할 확률 = 0.5^3 = 12.5% → 성공률 87.5%
// 출력: 재시도 없음: 49.8% / 3회 시도: 87.9% (호출 1749회, 재시도 749회)간단한 CircuitBreaker 를 구현하세요. 연속 실패가 threshold 에 도달하면 OPEN 상태가 되어 execute() 가 즉시 CircuitOpenException 을 던집니다.
openMillis 가 지나면 HALF_OPEN 으로 한 번 시도를 허용하고, 성공하면 CLOSED, 실패하면 다시 OPEN. 항상 실패하는 API 에 100번 호출해 실제 API 호출 횟수가 threshold 근처에서 멈추는지 확인하세요.
class CircuitBreaker {
enum State { CLOSED, OPEN, HALF_OPEN }
static class CircuitOpenException extends RuntimeException {
CircuitOpenException() { super("circuit OPEN"); }
}
private final int threshold;
private final long openMillis;
private State state = State.CLOSED;
private int consecutiveFailures;
private long openedAt;
CircuitBreaker(int threshold, long openMillis) { this.threshold = threshold; this.openMillis = openMillis; }
synchronized <T> T execute(Callable<T> action) throws Exception {
if (state == State.OPEN) {
if (System.currentTimeMillis() - openedAt < openMillis) throw new CircuitOpenException();
state = State.HALF_OPEN; // 시험 호출 1건 허용
}
try {
T result = action.call();
consecutiveFailures = 0;
state = State.CLOSED;
return result;
} catch (Exception e) {
consecutiveFailures++;
if (state == State.HALF_OPEN || consecutiveFailures >= threshold) {
state = State.OPEN;
openedAt = System.currentTimeMillis();
}
throw e;
}
}
State state() { return state; }
}
// 테스트: 항상 실패하는 API (transientFailureRate = 1.0)
FlakyApiClient dead = new FlakyApiClient(1, 1.0, 0);
CircuitBreaker cb = new CircuitBreaker(5, 10_000);
int open = 0, failed = 0;
for (long id = 1; id <= 100; id++) {
final long fid = id;
try { cb.execute(() -> dead.send(fid)); }
catch (CircuitBreaker.CircuitOpenException e) { open++; }
catch (Exception e) { failed++; }
}
System.out.printf("실제 API 호출 %d회, 실패 %d, 회로 OPEN 으로 즉시 거부 %d, 상태 %s%n",
dead.calls(), failed, open, cb.state());
// 출력: 실제 API 호출 5회, 실패 5, 회로 OPEN 으로 즉시 거부 95, 상태 OPEN
// 100번 × 재시도 3회 = 300번 두드릴 것을 5번으로 줄였다. 배치는 이 시점에 FAILED 로 중단하는 것이 맞다.예제 5 의 통합 루프에서, 청크 안의 스킵 건수가 청크 크기의 50% 를 넘으면 그 청크를 커밋하지 않고 ChunkSkipRateExceededException 으로 배치를 중단하도록 수정하세요(전체 skipLimit 와 별개의 안전장치).
그리고 FlakyApiClient 의 permanentFailEvery 를 2 로 설정해 실제로 중단되는지 확인하세요. 왜 이런 청크 단위 안전장치가 전체 한도와 별개로 필요한지 주석으로 설명하세요.
static class ChunkSkipRateExceededException extends RuntimeException {
ChunkSkipRateExceededException(int chunkNo, int skipped, int size) {
super(String.format("청크 %d: %d/%d 스킵 (50%% 초과) -> 입력 구간 이상", chunkNo, skipped, size));
}
}
// 청크 루프 안
List<String> sent = new ArrayList<>(chunk.size());
int skippedInChunk = 0;
for (String raw : chunk) {
try {
Order o = Order.parse(raw);
sent.add(retry3.execute(() -> api3.send(o.id())));
} catch (Exception e) {
if (!skip3.shouldSkip(e)) throw e;
report.skip(raw.split(",")[0], e, raw);
skippedInChunk++;
}
}
if (skippedInChunk * 2 > chunk.size()) {
throw new ChunkSkipRateExceededException(chunkNo, skippedInChunk, chunk.size()); // 커밋하지 않음
}
report.success(sent.size());
// 왜 별개로 필요한가:
// 전체 skipLimit(1,000) 은 "배치 전체에서 1,000건까지 허용" 이다. 청크 200건 중 190건이 실패해도
// 누적이 1,000 미만이면 통과한다. 그런데 한 청크의 95% 가 실패한다는 것은 "그 구간의 입력이 통째로
// 깨졌다" 는 신호이며, 계속 진행하면 이후 청크도 같은 비율로 실패해 한도에 도달하기 전에
// 수천 건이 데드 레터로 빠진다. 국소적 이상은 국소적 한도로 빨리 잡는다.
// permanentFailEvery = 2 로 실행 (id 짝수는 전부 영구 오류):
// 출력: Exception in thread "main" ChunkSkipRateExceededException: 청크 1: 101/200 스킵 (50% 초과) -> 입력 구간 이상