공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 11 / 22

외부 API 연동

타임아웃·재시도·멱등성·서킷 브레이커·동시 한도·폴백
섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

6. 연습 문제

이 레슨의 네 파일에 직접 코드를 덧붙이는 문제 5개입니다. 정답을 보기 전에 먼저 힌트만 보고 스스로 작성해 보세요. 다섯 문제 모두 Main 에 시나리오를 추가해 실행 결과로 직접 확인할 수 있습니다.

문제 1. ApiClient 에 Retry-After 헤더 존중 추가

429 응답을 받으면 우리가 정한 백오프 대신, 상대가 Retry-After 헤더로 알려준 초만큼 대기하도록 once 와 call 을 고치세요. 힌트: HttpResponse.headers().firstValue("Retry-After") 로 값을 꺼내 ApiException 에 실어 전달합니다.

java
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 는 상대가 자기 상태를 가장 잘 아는 정보이므로, 있으면 우리 추정 백오프보다 우선해야 합니다.

문제 2. CircuitBreaker 를 슬라이딩 윈도우 방식으로 개선

"연속 실패 N회" 대신 "최근 10회 중 실패율 50% 이상" 이면 OPEN 되도록 CircuitBreaker 를 고치세요. 힌트: boolean[] window 와 순환 인덱스로 최근 10회 결과를 저장합니다.

java
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번" 만으로 리셋되어 과민합니다. 슬라이딩 윈도우는 간헐적 성공이 섞여도 최근 흐름을 반영해 더 안정적으로 판단합니다.

문제 3. 재시도 포함 총 소요 시간 상한 추가

재시도를 포함한 전체 호출이 5초를 넘으면, 남은 재시도를 하지 않고 즉시 실패하도록 call 을 고치세요. 힌트: 루프 시작 전 long deadline = System.currentTimeMillis() + 5000 을 두고 매 재시도 전에 확인합니다.

java
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초를 넘게 기다릴 수 있습니다. 상한을 따로 둬야 사용자 대기 시간을 예측 가능하게 만듭니다.

문제 4. 전용 스레드 풀로 외부 호출 Bulkhead 만들기

외부 호출을 크기 10 스레드 풀에서만 실행하고, 큐가 가득 차면 대기 대신 즉시 거절하도록 만드세요. 힌트: ThreadPoolExecutor 의 큐 크기와 RejectedExecutionHandler 를 직접 지정합니다.

java
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 가 아무리 느려져도 다른 화면을 처리할 톰캣 스레드는 줄지 않습니다.

문제 5. FakePartner /rate 엔드포인트와 토큰 버킷 통과시키기

FakePartner 에 초당 3건을 넘으면 429 와 Retry-After: 1 을 응답하는 /rate 를 추가하고, 4.3 절 TokenBucket 으로 20건을 초당 3건 이하로 눌러 호출하세요. 힌트: 서버 쪽은 초 단위로 카운터를 리셋합니다.

java
// 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 를 막아내는지 눈으로 확인할 수 있습니다.

연습 문제
    이전 섹션5 자주 하는 실수 (Tip)6 / 7다음 섹션7 정리