공공부하자개발 · 영어 학습 노트
자바
실무 배치대용량 데이터 처리0/6 완료
  • 01배치 프로세스 개념과 아키텍처
  • 02대용량 파일 I/O (NIO, Buffered)
  • 03Chunk 단위 처리와 OOM 방지
  • 04트랜잭션: 커밋과 롤백 시뮬레이션
  • 05Skip과 Retry 로직 구현
  • 06메서드 활용 패턴 (배치 유틸)
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 배치 › 05 / 6

Skip과 Retry 로직 구현

섹션 7진행 0 / 6
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

6. 연습 문제

문제 1

RetryTemplate 을 사용하지 않고, FlakyApiClient (일시 오류 50%) 에 대해 재시도 없이 1,000건을 호출했을 때의 성공률과, RetryTemplate(maxAttempts=3, none 백오프) 로 호출했을 때의 성공률을 비교 출력하세요. 이론적으로 3회 시도 시 성공률이 얼마여야 하는지 주석으로 계산하세요.

정답 보기
java
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회)

문제 2

간단한 CircuitBreaker 를 구현하세요. 연속 실패가 threshold 에 도달하면 OPEN 상태가 되어 execute() 가 즉시 CircuitOpenException 을 던집니다.

openMillis 가 지나면 HALF_OPEN 으로 한 번 시도를 허용하고, 성공하면 CLOSED, 실패하면 다시 OPEN. 항상 실패하는 API 에 100번 호출해 실제 API 호출 횟수가 threshold 근처에서 멈추는지 확인하세요.

정답 보기
java
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 로 중단하는 것이 맞다.

문제 3

예제 5 의 통합 루프에서, 청크 안의 스킵 건수가 청크 크기의 50% 를 넘으면 그 청크를 커밋하지 않고 ChunkSkipRateExceededException 으로 배치를 중단하도록 수정하세요(전체 skipLimit 와 별개의 안전장치).

그리고 FlakyApiClient 의 permanentFailEvery 를 2 로 설정해 실제로 중단되는지 확인하세요. 왜 이런 청크 단위 안전장치가 전체 한도와 별개로 필요한지 주석으로 설명하세요.

정답 보기
java
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% 초과) -> 입력 구간 이상
연습 문제
  • 문제 1
  • 문제 2
  • 문제 3
이전 섹션5 자주 하는 실수 (Tip)6 / 7다음 섹션7 정리