이 레슨의 네 파일에 직접 코드를 덧붙이는 문제 5개입니다. 정답을 보기 전에 먼저 힌트만 보고 스스로 작성해 보세요. 다섯 문제 모두 Main 에 시나리오를 추가해 실행 결과로 직접 확인할 수 있습니다.
429 응답을 받으면 우리가 정한 백오프 대신, 상대가 Retry-After 헤더로 알려준 초만큼 대기하도록 once 와 call 을 고치세요. 힌트: HttpResponse.headers().firstValue("Retry-After") 로 값을 꺼내 ApiException 에 실어 전달합니다.
public static class ApiException extends RuntimeException {
public final int status;
public final long retryAfterMillis; // 0 이면 헤더 없음
public ApiException(int status, String message, long retryAfterMillis) {
super(message); this.status = status; this.retryAfterMillis = retryAfterMillis;
}
}
// once() 에서 429 를 던질 때
long ra = r.headers().firstValue("Retry-After")
.map(s -> Long.parseLong(s) * 1000).orElse(0L);
throw new ApiException(r.statusCode(), "HTTP 429", ra);
// call() 의 대기 부분
long wait = e.retryAfterMillis > 0 ? e.retryAfterMillis : backoffMillis(attempt);Retry-After 는 상대가 자기 상태를 가장 잘 아는 정보이므로, 있으면 우리 추정 백오프보다 우선해야 합니다.
"연속 실패 N회" 대신 "최근 10회 중 실패율 50% 이상" 이면 OPEN 되도록 CircuitBreaker 를 고치세요. 힌트: boolean[] window 와 순환 인덱스로 최근 10회 결과를 저장합니다.
private final boolean[] window = new boolean[10]; // true = 실패
private int idx = 0, filled = 0;
private synchronized void record(boolean failed) {
window[idx] = failed;
idx = (idx + 1) % window.length;
filled = Math.min(filled + 1, window.length);
int fails = 0;
for (int i = 0; i < filled; i++) if (window[i]) fails++;
if (filled == window.length && fails * 100.0 / filled >= 50) {
state = State.OPEN; openedAt = Instant.now();
}
}
// onSuccess()는 record(false), onFailure()는 record(true) 를 호출연속 실패 카운터는 "성공 1번" 만으로 리셋되어 과민합니다. 슬라이딩 윈도우는 간헐적 성공이 섞여도 최근 흐름을 반영해 더 안정적으로 판단합니다.
재시도를 포함한 전체 호출이 5초를 넘으면, 남은 재시도를 하지 않고 즉시 실패하도록 call 을 고치세요. 힌트: 루프 시작 전 long deadline = System.currentTimeMillis() + 5000 을 두고 매 재시도 전에 확인합니다.
long deadline = System.currentTimeMillis() + 5000;
for (int attempt = 1; attempt <= attempts; attempt++) {
if (System.currentTimeMillis() > deadline)
throw new ApiException(0, "총 소요 시간 5초 초과로 중단");
// 기존 호출·재시도 로직
}개별 타임아웃만 있으면 재시도 3회가 각각 1.5초씩 걸릴 때 총 4.5초를 넘게 기다릴 수 있습니다. 상한을 따로 둬야 사용자 대기 시간을 예측 가능하게 만듭니다.
외부 호출을 크기 10 스레드 풀에서만 실행하고, 큐가 가득 차면 대기 대신 즉시 거절하도록 만드세요. 힌트: ThreadPoolExecutor 의 큐 크기와 RejectedExecutionHandler 를 직접 지정합니다.
ExecutorService bulkhead = new ThreadPoolExecutor(
10, 10, 0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(20), // 큐 20개까지만 대기
(r, ex) -> { throw new RejectedExecutionException("외부 호출 포화"); });
try {
return bulkhead.submit(() -> client.get(url)).get(3, TimeUnit.SECONDS);
} catch (RejectedExecutionException e) {
throw new ApiClient.ApiException(0, "외부 호출 풀 포화, 잠시 후 재시도");
}이 풀이 톰캣 스레드 풀과 분리돼 있으므로, 이 API 가 아무리 느려져도 다른 화면을 처리할 톰캣 스레드는 줄지 않습니다.
FakePartner 에 초당 3건을 넘으면 429 와 Retry-After: 1 을 응답하는 /rate 를 추가하고, 4.3 절 TokenBucket 으로 20건을 초당 3건 이하로 눌러 호출하세요. 힌트: 서버 쪽은 초 단위로 카운터를 리셋합니다.
// FakePartner
AtomicInteger sec = new AtomicInteger(-1), inSec = new AtomicInteger();
server.createContext("/rate", ex -> {
int now = (int) (System.currentTimeMillis() / 1000);
if (sec.getAndSet(now) != now) inSec.set(0);
if (inSec.incrementAndGet() > 3) {
ex.getResponseHeaders().set("Retry-After", "1");
send(ex, 429, "{\"error\":\"rate limit\"}");
} else send(ex, 200, "{\"status\":\"ok\"}");
});
// 호출 쪽: 초당 3개로 맞춘 버킷을 통과해야 보낸다
TokenBucket bucket = new TokenBucket(3, 1000);
for (int i = 0; i < 20; i++) {
while (!bucket.tryAcquire()) FakePartner.sleep(50);
client.get(p.url("/rate"));
}서버 쪽 한도를 재현해 두면, 우리 쪽 토큰 버킷 설정값이 실제로 429 를 막아내는지 눈으로 확인할 수 있습니다.