공공부하자개발 · 영어 학습 노트
자바
실무 확장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정리‹ 이전다음 ›

4. 응용 변형 예제

4.1 Spring Boot RestClient + Resilience4j

java
@Bean
RestClient pgClient() {
    var factory = new JdkClientHttpRequestFactory(HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(2)).build());
    factory.setReadTimeout(Duration.ofSeconds(3));
    return RestClient.builder().baseUrl("https://pg.example.com")
            .requestFactory(factory).build();
}

@Service
class PgService {
    @Retry(name = "pg")
    @CircuitBreaker(name = "pg", fallbackMethod = "payFallback")
    String pay(String key, String body) {
        return pgClient().post().uri("/pay")
                .header("Idempotency-Key", key).body(body)
                .retrieve().body(String.class);
    }
    String payFallback(String key, String body, Exception e) {
        return "{\"status\":\"PENDING\"}";   // 미확인 상태로 응답, 4.2 절 대사 대상
    }
}
yaml
resilience4j:
  retry:
    instances.pg: { max-attempts: 3, wait-duration: 200ms, retry-exceptions: [java.io.IOException] }
  circuitbreaker:
    instances.pg: { sliding-window-size: 10, failure-rate-threshold: 50, wait-duration-in-open-state: 10s }

애노테이션 순서가 실행 순서입니다. @Retry 가 @CircuitBreaker 를 감싸므로, 재시도 3회가 모두 실패해야 서킷의 실패 횟수가 1 올라갑니다. 순서를 반대로 두면 서킷이 재시도 한 번마다 실패를 세어 지나치게 빨리 OPEN 됩니다.

4.2 미확인 결제 대사

타임아웃 난 결제는 성공도 실패도 아닌 UNKNOWN 으로 저장하고, 스케줄러가 나중에 상대 조회 API 로 확정합니다.

sql
CREATE TABLE payment (
    id            BIGINT PRIMARY KEY,
    idempotency_key VARCHAR(64) NOT NULL UNIQUE,
    status        VARCHAR(10)  NOT NULL,   -- PENDING, UNKNOWN, PAID, FAILED
    amount        BIGINT       NOT NULL
);
java
try {
    String res = pgClient.post(url, body, key);
    mapper.updateStatus(key, "PAID");
} catch (ApiClient.ApiException e) {
    mapper.updateStatus(key, "UNKNOWN");   // 처리 여부를 모름, 재시도 금지
}

// @Scheduled(fixedDelay = 60_000), 09 레슨 SafeTask 로 감싼다
for (String key : mapper.findUnknownKeys()) {
    String status = pgClient.get("/pay/status/" + key);   // 상대의 조회 API 로 확정
    mapper.updateStatus(key, status);
}

UNKNOWN 상태로 남은 결제는 사람이 개입하기 전까지 자동으로 재시도하면 안 됩니다. 조회 API 로 확정되기 전에는 성공인지 실패인지 우리도 상대도 알 수 없는 상태이기 때문입니다.

4.3 토큰 버킷 속도 제한기

세마포어는 동시 개수를, 토큰 버킷은 시간당 개수를 제한합니다. 분당 100건을 예로 듭니다.

java
class TokenBucket {
    private final long capacity = 100, refillMillis = 60_000 / 100;   // 0.6초마다 1개
    private final AtomicLong tokens = new AtomicLong(capacity);
    private final AtomicLong lastRefill = new AtomicLong(System.currentTimeMillis());

    boolean tryAcquire() {
        long now = System.currentTimeMillis();
        long elapsed = now - lastRefill.get();
        long refill = elapsed / refillMillis;
        if (refill > 0 && lastRefill.compareAndSet(lastRefill.get(), now)) {
            tokens.set(Math.min(capacity, tokens.get() + refill));
        }
        return tokens.get() > 0 && tokens.decrementAndGet() >= 0;
    }
}

세마포어는 "지금 몇 개가 동시에 진행 중인가" 만 봅니다. 토큰 버킷은 "1분 동안 몇 개를 보냈는가" 를 보므로, 순간적으로 몰아 보내는 버스트를 막는 데 더 맞습니다.

4.4 응답 검증과 계약 방어

상대 응답의 필드가 누락되거나 타입이 바뀌면, 조용히 null 로 흘려보내지 말고 즉시 예외로 막아야 합니다. 07 레슨의 MiniJson 을 그대로 씁니다.

java
Map<String, Object> json = MiniJson.parse(body);
Object paymentId = json.get("paymentId");
if (paymentId == null) {
    throw new ApiClient.ApiException(0, "계약 위반: paymentId 누락");
}
if (!(paymentId instanceof String)) {
    throw new ApiClient.ApiException(0, "계약 위반: paymentId 타입 변경 (" + paymentId.getClass() + ")");
}

계약 위반을 여기서 잡지 않으면, null 이 결제 서비스 안쪽까지 흘러가 훨씬 찾기 어려운 오류로 나타납니다. 상대 API 가 문서와 다르게 응답을 바꾼 사고는 이 검증이 있어야 배포 당일에 바로 드러납니다.

이 검증은 ApiException(0, ...) 을 던지므로 재시도 대상이 아니라 즉시 실패로 처리됩니다. 상대가 응답 형식을 바꾼 것은 재시도로 고쳐지는 문제가 아니라 사람이 확인해야 하는 문제이기 때문입니다.

응용 변형 예제
  • 4.1 Spring Boot RestClient + Resilience4j
  • 4.2 미확인 결제 대사
  • 4.3 토큰 버킷 속도 제한기
  • 4.4 응답 검증과 계약 방어
이전 섹션3 코드 예제4 / 7다음 섹션5 자주 하는 실수 (Tip)