타임아웃과 재시도, 서킷 브레이커까지

2026. 10. 20. 13:00·IT
728x90
반응형

외부 API를 호출하는 코드에 타임아웃을 안 걸어두면, 상대가 느려지는 순간 내 서비스가 함께 멈춘다. 재시도를 잘못 넣으면 이미 힘든 상대를 더 때려서 장애를 키운다. 이 글은 타임아웃, 재시도, 서킷 브레이커 세 가지를 어떤 순서로 어떻게 설정해야 하는지 정리한다.

타임아웃이 없으면 스레드가 말라붙는다

가장 먼저 할 일은 타임아웃 설정이다. 이게 없으면 나머지는 의미가 없다.

외부 API가 응답하지 않으면 내 서버의 요청 처리 스레드(또는 커넥션)가 그 자리에서 기다린다. 요청이 계속 들어오면 대기하는 스레드가 쌓이고, 결국 풀이 고갈된다. 그러면 그 외부 API와 무관한 요청까지 처리하지 못한다. 장애가 전파되는 전형적인 경로다.

타임아웃은 두 종류를 따로 잡아야 한다. 연결 타임아웃은 TCP 연결을 맺는 데 걸리는 시간이고, 읽기 타임아웃은 응답을 기다리는 시간이다.

requests.get(url, timeout=(3, 10))   # 연결 3초, 읽기 10초

값은 상대의 정상 응답 시간 분포에서 정한다. p99가 800밀리초라면 읽기 타임아웃을 2~3초로 잡는다. 여유를 너무 많이 주면 타임아웃의 의미가 없고, 너무 빠듯하면 정상 요청이 잘린다.

라이브러리의 기본값을 믿으면 안 된다. 많은 HTTP 클라이언트가 기본 타임아웃이 무한이다. 명시적으로 주지 않으면 안 걸린다.

재시도는 조건을 가려서 한다

타임아웃이 나면 다시 시도하고 싶어진다. 그런데 무엇을, 몇 번, 어떤 간격으로 할지가 중요하다.

재시도해도 되는 것은 멱등한 요청이다. GET, PUT, DELETE는 같은 요청을 여러 번 해도 결과가 같다. 그리고 실패 원인이 일시적이어야 한다. 연결 실패, 타임아웃, 503, 429가 여기 해당한다.

재시도하면 안 되는 것은 POST처럼 부작용이 있는 요청이다. 결제 요청을 재시도하면 이중 결제가 날 수 있다. 그리고 400, 401, 404처럼 다시 해도 같은 결과가 나올 오류는 재시도할 이유가 없다.

POST를 꼭 재시도해야 한다면 멱등 키(idempotency key)를 쓴다. 클라이언트가 만든 고유 키를 헤더에 실어 보내고, 서버는 같은 키로 온 요청을 한 번만 처리한다. 결제 API들이 이 방식을 쓴다.

간격에 지수 백오프와 지터를 넣는다

재시도를 즉시, 고정 간격으로 하면 상황을 악화시킨다. 상대가 과부하로 느려진 것이라면, 재시도가 부하를 더한다.

지수 백오프는 시도할 때마다 대기 시간을 늘린다. 1초, 2초, 4초, 8초 식이다. 상대에게 회복할 시간을 준다.

여기에 지터(jitter)를 더해야 한다. 백오프만 쓰면 동시에 실패한 클라이언트들이 동시에 재시도한다. 1초 뒤에 전부, 2초 뒤에 전부 몰린다. 파도가 반복되는 셈이다. 무작위 값을 섞으면 이 동기화가 깨진다.

import random, time

def call_with_retry(fn, attempts=4, base=1.0, cap=10.0):
    for i in range(attempts):
        try:
            return fn()
        except TransientError:
            if i == attempts - 1:
                raise
            delay = min(cap, base * (2 ** i))
            time.sleep(random.uniform(0, delay))   # full jitter

재시도 횟수는 3회 정도가 적당하다. 그 이상 해도 될 것은 대개 처음 몇 번에 된다. 그리고 전체 대기 시간이 상위 호출자의 타임아웃을 넘지 않아야 한다. 넘으면 상위에서 이미 포기한 요청을 위해 계속 재시도하는 꼴이 된다.

계층마다 재시도를 넣지 않는 것도 중요하다. 3계층이 각각 3회 재시도하면 최악의 경우 27번이다. 재시도는 한 곳에서만 한다.

서킷 브레이커는 포기를 자동화한다

상대가 완전히 죽었다면 재시도는 낭비다. 매 요청이 타임아웃까지 기다렸다가 실패한다. 사용자는 응답을 몇 초씩 기다린 끝에 오류를 본다.

서킷 브레이커는 실패가 일정 비율을 넘으면 아예 호출을 멈춘다. 즉시 실패를 반환하므로 사용자는 빨리 답을 받고, 상대는 부하에서 벗어난다.

핵심은 Half-Open 상태다. 무작정 열어두면 상대가 복구돼도 모른다. 일정 시간이 지나면 소수의 요청만 통과시켜 보고, 성공하면 정상으로 돌아간다.

서킷이 열렸을 때 무엇을 반환할지를 정해두는 것이 실제 설계다. 캐시된 이전 값을 주거나, 기본값을 주거나, 해당 기능만 비활성화한 화면을 준다. 추천 목록이 없어도 상품 페이지는 보여줄 수 있다. 부분 실패를 전체 실패로 만들지 않는 것이 목적이다.

설정보다 우선순위가 중요하다

도구를 다 나열했으니 순서를 정리한다. 나는 이 순서로 적용한다.

1단계: 모든 외부 호출에 타임아웃. 예외 없다. 이것만 해도 연쇄 장애의 상당수가 막힌다. 데이터베이스 쿼리, 캐시 조회, 내부 서비스 호출도 포함이다.

2단계: 멱등한 호출에만 백오프+지터 재시도. 3회 이내로 제한하고, 한 계층에서만 한다.

3단계: 실패해도 되는 의존성에 서킷 브레이커와 폴백. 없어도 핵심 기능이 동작하는 것부터 적용한다.

4단계: 관측. 타임아웃 발생 수, 재시도 수, 서킷 상태를 지표로 내보낸다. 이게 없으면 설정이 맞는지 알 수 없다. 재시도가 급증하는 것은 상대가 나빠지고 있다는 조기 신호다.

한 가지 덧붙이면, 이 장치들은 상대가 나쁠 때 내가 버티는 도구이지 상대를 고치는 도구가 아니다. 재시도가 계속 늘어난다면 근본 원인을 봐야 한다. 서킷 브레이커가 자주 열리는 의존성이 있다면, 그 호출이 정말 동기적으로 필요한지 다시 생각해 볼 만하다. 큐를 거쳐 비동기로 처리할 수 있다면 애초에 이 문제를 겪지 않는다.

참고 자료

  • Timeouts, retries and backoff with jitter — Amazon Builders' Library — 지터가 필요한 이유와 백오프 전략 비교
  • Exponential Backoff And Jitter — AWS Architecture Blog — 지터 방식별 시뮬레이션 결과
  • Circuit Breaker — martinfowler.com — 서킷 브레이커의 상태 전이와 적용 기준
728x90
반응형

'IT' 카테고리의 다른 글

트랜잭션 격리 수준과 데드락 줄이기  (0) 2026.10.22
API 버저닝, 언제 올리고 언제 안 올리나  (0) 2026.10.21
헬스체크와 무중단 배포, 프로브 제대로 쓰기  (0) 2026.10.19
페이지네이션, 오프셋 대신 커서를 쓸 때  (0) 2026.10.18
CORS 오류가 나는 진짜 이유와 해결법  (0) 2026.10.17
'IT' 카테고리의 다른 글
  • 트랜잭션 격리 수준과 데드락 줄이기
  • API 버저닝, 언제 올리고 언제 안 올리나
  • 헬스체크와 무중단 배포, 프로브 제대로 쓰기
  • 페이지네이션, 오프셋 대신 커서를 쓸 때
밍글링글링
밍글링글링
mingling - 밍글링, 밍글밍글링. 코드와 어우러지다. IT/ 프로그래밍/소스
    반응형
    250x250
  • 밍글링글링
    mingling
    밍글링글링
  • 전체
    오늘
    어제
    • 밍글링글링 (463)
      • Flutter (2)
      • 일상생활 (8)
        • 리뷰 (1)
        • 생활정보 (4)
        • 맛집 (0)
        • 여행 (0)
        • 모든정보 (3)
      • JAVA (126)
        • 개념 (6)
        • 예제 (115)
        • Exception (2)
      • C (1)
        • C (1)
        • C++ (0)
        • C# (0)
      • JS (29)
        • JavaScript (18)
        • JQuery (5)
        • AJax (0)
        • NODE.JS (6)
        • Angular.JS 2.0 (0)
      • WEB (87)
        • HTML (6)
        • CSS (61)
        • JSP (20)
        • JSTL (0)
      • FrameWork (8)
        • Spring (8)
        • BootStrap (0)
        • MyBATIS (0)
        • JUnit (0)
      • 외부 라이브러리 (5)
      • 공유 소스 관리 (5)
        • Git (5)
        • SVN (0)
      • 빅데이터 프로그래밍 (37)
        • Python (37)
        • R Programming (0)
      • DB (7)
        • ORACLE (0)
        • MySql (6)
      • Development Tools (7)
        • StarUML (0)
        • eXERD (0)
        • Eclipse (4)
      • SKILL (6)
        • Migration (0)
        • Security (6)
      • MicroSoft (0)
        • Excel (0)
        • Word (0)
      • Android (0)
      • Server (21)
        • Ubuntu (5)
        • Linux (15)
      • IOS (0)
      • XML (0)
      • 미디어 (0)
      • 공지사항 (3)
      • NETWORK (1)
      • 게임 (4)
        • 피파 (1)
        • 리니지M (0)
        • 배틀그라운드 (1)
        • 듀랑고 (2)
      • 세상 이슈 (6)
      • 일렉트론 (0)
      • 대회 소식 (4)
      • 업무 (2)
      • Express, Vue (6)
      • docker (11)
      • svelte (3)
      • 블록체인 (1)
      • IT (66)
      • Rust (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    jsp include
    nginx
    자바 클래스
    proxy pass
    자바 배열
    클론코딩
    servlet class
    AI 반도체
    vscode
    vue cli
    css table
    ubuntu
    SSL 인증서
    jsp parameter
    React Compiler
    에디터 팁
    mysql db
    제미나이
    브라우저 자동화
    css tb
    nginx ssl 적용
    ssl 인증서 발급
    rust linux
    nginx ssl 설정
    VS Code 팁
    자바 exception
    java casting
    svelte
    프런트엔드
    Rust lang
    docker
    자바 for문
    AI 코딩 에이전트
    css perspective
    오류
    Extension Bisect
    spring java
    티스토리 자동화
    css float
    자바 객체 지향
    Java Array
    css transition
    API 설계
    vue 설치
    러스트
    자바 생성자
    Node
    리눅스 설치
    gitlab 설치
    lang rust
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
밍글링글링
타임아웃과 재시도, 서킷 브레이커까지
상단으로

티스토리툴바