인사이트

인사이트

타임아웃·재시도·서킷 브레이커: 분산 시스템이 장애를 견디는 3종 기본기

타임아웃·재시도·서킷 브레이커 분산 시스템이 장애를 견디는 3종 기본기

🤖 AI Summary

서비스를 여러 조각으로 나누면 조각들 사이의 원격 호출이 늘어납니다. 그리고 원격 호출은 언젠가 느려지고 실패합니다. AWS 엔지니어가 정리한 글은 회복력 있는 시스템의 세 가지 필수 도구로 타임아웃·재시도·백오프를 꼽습니다. 타임아웃이 없으면 응답을 기다리는 쪽이 자원을 붙잡은 채 멈춰 있고, 재시도는 "이기적"이어서 이미 힘든 서비스에 부하를 더합니다. 그래서 지수 백오프로 간격을 늘리고 지터로 시점을 흩뿌리며, 재시도 횟수 자체를 토큰 버킷으로 제한합니다. 재시도가 안전하려면 멱등성도 필요합니다. 여기에 실패가 계속될 때 아예 호출을 끊는 서킷 브레이커가 더해집니다. Closed·Open·Half-Open 세 상태로 동작하며, 회복 중인 서비스가 다시 폭주에 노출되지 않게 지켜 줍니다. 다만 서킷 브레이커는 두 출처의 평가가 갈립니다. Azure는 재시도로 충분한 경우 불필요한 복잡도가 될 수 있다고 하고, AWS 글은 복구 시간을 늘릴 수 있다며 토큰 버킷 기반 재시도 제한을 먼저 권합니다. 이 글은 AWS 기고글과 Azure 공식 문서로 이 도구들을 정리합니다.

블로그 목차

원격 호출은 언젠가 느려집니다

모놀리식이든 마이크로서비스든, 서비스는 결국 다른 무언가를 부릅니다. 데이터베이스, 외부 API, 옆 팀의 서비스. 문제는 그 호출이 실패하는 게 아니라 대답을 안 하는 순간입니다. 실패는 처리하면 되지만, 무응답은 기다리게 만듭니다.

기다림의 대가는 자원입니다. Azure 아키텍처 패턴 문서의 설명이 정확합니다. 응답을 기다리며 막혀 있는 요청들은 "메모리, 스레드, 데이터베이스 커넥션 같은 핵심 시스템 자원을 붙잡고 있을 수 있다"는 것. 그 자원이 고갈되면 해당 호출과 무관한 다른 기능까지 함께 무너질 수 있습니다. 하나의 느린 의존성이 전체 장애가 되는 경로가 여기서 열립니다.

그래서 AWS 엔지니어가 쓴 글은 회복력 있는 시스템을 만드는 "세 가지 필수 도구: 타임아웃, 재시도, 백오프"를 이야기합니다. 백오프는 재시도를 안전하게 만드는 장치이므로, 이 글은 타임아웃·재시도·서킷 브레이커 셋을 축으로 두고 백오프와 지터, 멱등성은 재시도 안에서 다루겠습니다.




타임아웃: 기다림에 끝을 정한다

첫 번째 도구는 단순합니다. 언제까지 기다릴지 정하는 것. AWS 글은 아마존의 모범 사례를 이렇게 밝힙니다. "모든 원격 호출에 타임아웃을 설정하고, 일반적으로는 같은 머신 안에서 일어나는 프로세스 간 호출에도 설정한다." 네트워크를 건너가지 않는 호출까지 포함한다는 점이 인상적입니다.

어려운 부분은 값입니다. 너무 길면 자원을 오래 붙잡고, 너무 짧으면 정상 요청까지 실패로 처리됩니다. 짧은 쪽의 대가는 생각보다 큽니다. AWS 글은 백엔드 지연이 조금만 늘어도 모든 요청이 재시도로 돌아서며 전면 장애로 이어질 수 있다고 경고합니다. Azure 문서도 같은 딜레마를 짚습니다. 더 짧은 타임아웃을 설정하되 "대부분의 경우 작업이 성공할 만큼은 충분히 길어야 한다"는 것.

AWS 글이 제시하는 절차는 이렇습니다. 같은 리전 안의 호출을 전제로 한 방법입니다. 먼저 허용 가능한 오탐(정상인데 타임아웃) 비율을 정하고(예: 0.1%), 그다음 다운스트림 서비스의 응답 지연 시간 백분위수(이 예시에서는 p99.9)를 확인합니다. 다만 이 방법이 늘 맞지는 않습니다. 인터넷을 거치느라 클라이언트 쪽 네트워크 지연이 큰 경우가 하나고, p99.9가 p50과 거의 차이 나지 않을 만큼 지연이 고른 서비스가 다른 하나입니다. 앞의 경우에는 클라이언트가 전 세계에 퍼져 있을 수 있다는 점을 감안해 합리적인 최악의 네트워크 지연을 반영하고, 뒤의 경우에는 여유를 더 두라고 AWS 글은 덧붙입니다.




재시도: 가장 위험한 도구

타임아웃이 걸렸다면 다시 시도해 볼 수 있습니다. 일시적인 오류라면 두 번째 시도가 성공하니까요. 그런데 재시도는 셋 중 가장 조심해야 할 도구입니다. AWS 글의 표현이 인상적입니다. "재시도는 이기적이라는 점을 기억하는 데서 출발하는 것이 좋다." 클라이언트 입장에서는 성공 확률을 높이는 행동이지만, 서버 입장에서는 이미 힘든 상황에 요청이 하나 더 들어오는 일입니다.

특히 위험한 경우가 있습니다. AWS 글은 "오류가 부하 때문에 발생한 것이라면, 모든 클라이언트가 동시에 재시도할 경우 재시도는 효과가 없을 수 있다"고 지적합니다. 과부하로 실패 → 모두 재시도 → 부하 가중 → 다시 실패. 이 고리를 끊기 위해 세 가지 장치를 씁니다.

  • 지수 백오프: AWS 글 기준 가장 흔한 패턴으로, "시도할 때마다 대기 시간을 지수적으로 늘리는" 방식입니다. 무한정 늘릴 수는 없으니 상한을 두는 capped exponential backoff를 함께 씁니다. 단 상한을 두면 모든 클라이언트가 그 상한 간격으로 계속 재시도하게 되는 새 문제가 생겨, AWS 글은 상한값 조정보다 재시도 횟수 자체를 제한하는 쪽을 택합니다.

  • 지터(jitter): 백오프만으로는 부족합니다. 같은 계산식을 쓰는 클라이언트들은 여전히 같은 시점에 몰리기 때문입니다. AWS 글의 해법은 간명합니다. "이 문제를 피하기 위해 우리는 지터를 사용한다." 대기 시간에 무작위성을 섞어 재시도 시점을 흩뿌리는 것이죠. 한 가지 반전도 있습니다. 정기 실행되는 예약 작업에 지터를 넣을 때는 호스트마다 무작위로 뽑지 않고, 같은 호스트에서는 매번 같은 값이 나오는 일관된 방식을 씁니다. 무작위로 흩뿌리면 문제가 생겼을 때 사람이 패턴을 읽어 원인을 찾기가 훨씬 어려워지기 때문입니다.

  • 재시도 총량 제한: AWS 글은 "로컬에서 토큰 버킷을 사용해 재시도를 제한함으로써 이 위험을 완화할 수 있다"고 설명합니다. 재시도에도 예산을 두는 셈입니다. AWS는 이 동작을 2016년에 자사 SDK에 넣었으므로, SDK를 쓰는 쪽은 이 제한이 이미 적용된 상태입니다. 토큰 버킷 개념 자체는 레이트 리미팅 글에서 다뤘습니다.

계층마다 독립적으로 재시도하면 증폭은 곱셈으로 커집니다. AWS 글의 예시는 강렬합니다. 5단 깊이의 호출 스택에서 각 계층이 3회씩 따로 재시도할 때 "데이터베이스에 걸리는 부하가 243배로 늘어, 회복할 가능성이 희박해진다"는 것. 그래서 "저비용 컨트롤 플레인·데이터 플레인 작업에서는 일반적으로 스택의 한 지점에서만 재시도하는 것"이 모범 사례로 제시됩니다. 물론 상위 계층에서만 재시도하면 앞선 호출의 작업이 버려져 효율이 떨어지는 반대급부도 함께 언급됩니다.

무엇을 재시도할지도 가려야 합니다. HTTP는 클라이언트 오류와 서버 오류를 구분하는데, AWS 글은 "클라이언트 오류는 같은 요청으로 재시도해도 나중에 성공하지 않으므로 재시도해서는 안 되고, 서버 오류는 이후 시도에서 성공할 수 있다"고 정리합니다. 다만 결과적 일관성을 가진 시스템에서는 그 경계가 흐려진다는 단서도 붙습니다.

재시도 대상을 골랐다면 다음은 안전성입니다. 같은 요청을 두 번 보내도 되는지를 따져야 합니다. AWS 글은 "타임아웃이나 실패가 곧 부수 효과가 일어나지 않았다는 뜻은 아니다"라고 짚습니다. 결제 요청이 처리됐는데 응답만 못 받았을 수도 있다는 뜻이죠. 그래서 부수 효과가 여러 번 일어나면 곤란한 경우, "API를 멱등하게 설계해 안전하게 재시도할 수 있게 만드는 것이 모범 사례"입니다. 참고로 읽기 전용 API는 대체로 멱등하지만, 자원 생성 API는 그렇지 않을 수 있습니다.

누가 무엇을 막아 주는가




서킷 브레이커: 아예 시도하지 않기, 그리고 그 대가

재시도는 "곧 성공하겠지"라는 기대 위에 있습니다. 그런데 상대가 완전히 무너졌다면 그 기대는 틀렸고, 계속 두드리는 건 서로에게 손해입니다. 여기서 서킷 브레이커가 등장합니다. Azure 문서는 이 패턴이 "실패할 가능성이 높은 작업을 애플리케이션이 반복해서 실행하려 드는 것을 막아 준다"고 설명합니다.

다만 짚어 둘 것이 있습니다. 이 지점에서 두 출처의 온도차가 드러납니다. Azure는 서킷 브레이커를 정식 패턴으로 정리하면서도 "잘 알려진 재시도 알고리즘으로 충분하고 의존성이 재시도를 감당하도록 설계돼 있다면, 애플리케이션의 서킷 브레이커는 불필요한 복잡도를 더할 수 있다"고 적용하지 않을 경우를 함께 제시합니다. AWS 쪽 글은 유보가 더 강합니다. 서킷 브레이커가 "널리 권장된다"고 언급한 뒤 곧바로 "불행히도 서킷 브레이커는 시스템에 테스트하기 어려운 모드 동작(modal behavior)을 들여오고, 복구 시간을 크게 늘릴 수 있다"고 유보를 답니다. 그래서 AWS 쪽이 먼저 제시하는 대안이 앞서 본 토큰 버킷 기반 재시도 제한입니다.

AWS 글이 결론에서 제시하는 방향도 같은 맥락입니다. "의존성이 건강하다고 관측될 때만 재시도하고, 재시도가 가용성 개선에 도움이 되지 않으면 재시도를 멈춘다"는 것.

유보를 감안하더라도 어떻게 동작하는지는 알아 둘 값어치가 있습니다. 세 가지 상태를 오가는 상태 머신입니다.

  • Closed(닫힘, 평상시): 요청이 정상적으로 흘러갑니다. 프록시는 최근 실패 횟수를 세고, "주어진 기간 안에 최근 실패 횟수가 지정된 임계값을 넘으면 Open 상태로 전환하고 타임아웃 타이머를 시작"합니다. 이 실패 카운터는 시간 기반이라 주기적으로 자동 초기화되는데, 가끔 발생하는 실패만으로 회로가 열리지 않게 하려는 설계입니다.

  • Open(열림, 차단): 호출을 시도조차 하지 않습니다. "애플리케이션의 요청은 즉시 실패하고 예외가 반환"됩니다. 상대에게 부하를 주지 않고, 우리 쪽도 기다리느라 자원을 낭비하지 않습니다.

  • Half-Open(반열림, 탐색): 타이머가 끝나면 "제한된 수의 요청만 통과시켜" 상대의 상태를 살핍니다. 지정된 횟수만큼 연속으로 성공하면 결함이 해결됐다고 보고 Closed로 돌아가며 실패 카운터를 초기화합니다. 반대로 "어느 하나라도 실패하면 결함이 여전하다고 보고 Open 상태로 되돌아갑니다."

Azure 문서는 사각지대도 짚습니다. 외부 서비스의 타임아웃이 지나치게 길면 서킷 브레이커를 실행하는 스레드 자체가 오래 묶일 수 있고, 여러 인스턴스가 동시에 그 호출을 시도하면 스레드가 무더기로 잡힌 채 실패한다는 것입니다. 서킷 브레이커도 타임아웃 값이 합리적일 때 제 역할을 합니다.

Half-Open이 왜 필요한지가 이 패턴의 핵심입니다. Azure 문서는 이렇게 설명합니다. "Half-Open 상태는 회복 중인 서비스가 갑자기 요청 폭주에 노출되는 것을 막아 준다." 이제 막 일어서는 서비스에 밀린 요청이 한꺼번에 몰리면 다시 쓰러지니까요. 조심스럽게 문을 조금만 열어 보는 단계인 셈입니다.

Half-Open은 회복 중인 서비스가 요청 폭주에 노출되는 것을 막습니다 (출처 Azure 아키텍처 패턴)




셋은 함께 쓰되, 역할을 헷갈리지 않기

마지막으로 정리할 것은 재시도와 서킷 브레이커의 관계입니다. 비슷해 보이지만 목적이 정반대입니다. Azure 문서가 명확하게 구분합니다. "재시도 패턴은 결국 성공할 것이라는 기대를 갖고 작업을 다시 시도하게 해 준다. 서킷 브레이커 패턴은 실패할 가능성이 높은 작업을 애플리케이션이 수행하지 못하게 막는다."

둘은 함께 쓸 수 있습니다. 다만 조건이 있습니다. 문서는 재시도 로직이 서킷 브레이커가 반환하는 예외에 민감해야 하며, 결함이 일시적이지 않다고 서킷이 알리면 재시도를 멈춰야 한다고 안내합니다. 서킷이 열렸는데도 재시도가 계속 두드리면, 두 패턴을 다 쓰고도 효과는 사라집니다.

한 가지 함정도 함께 기억할 만합니다. 재시도와 백오프에 쓰는 총 시간이 상위 호출자의 타임아웃을 넘지 않아야 합니다. AWS 글의 지적처럼 "대부분의 경우 호출자는 자기 타임아웃 때문에 어차피 그 호출을 포기하게" 되므로, 넘어선 재시도는 성공률을 올리지 못한 채 자원만 씁니다.

정리하면 순서는 이렇습니다. 타임아웃으로 기다림에 끝을 정하고, 재시도는 백오프·지터·총량 제한과 멱등성을 갖춘 뒤에만 켜고, 반복 실패가 이어지면 서킷 브레이커까지 검토합니다. 서버 쪽에서 들어오는 요청을 제한하는 레이트 리미팅과 짝을 이루면, 나가는 호출과 들어오는 요청 양쪽에 방어선이 생깁니다.




이것만 기억하세요

회복력의 기본은 타임아웃·재시도·백오프이고(AWS), 여기에 서킷 브레이커가 더해집니다. 핵심 주의점은 재시도는 이기적이라는 것입니다. 지수 백오프와 지터로 시점을 흩뿌리고, 토큰 버킷으로 총량을 제한하며, 멱등한 요청만 재시도해야 합니다. 서킷 브레이커는 재시도의 반대편에서 실패할 가능성이 높은 호출을 아예 막는 역할이지만, 도입 여부는 재시도 제한만으로 충분한지와 함께 따져야 합니다.




자주 묻는 질문 (FAQ)

Q. 타임아웃은 어디에 걸어야 하나요?

AWS 글은 아마존의 모범 사례로 모든 원격 호출에 타임아웃을 설정하고, 일반적으로 같은 머신 안의 프로세스 간 호출에도 설정한다고 밝힙니다. 타임아웃이 없으면 기다리는 쪽이 스레드·커넥션 같은 자원을 붙잡고 있게 되고, 그 고갈이 무관한 다른 기능까지 무너뜨릴 수 있어요.

Q. 재시도를 하면 오히려 상황이 나빠질 수도 있나요?

그렇습니다. AWS 글은 재시도가 이기적이라는 점을 기억하라고 조언하며, 오류의 원인이 부하일 때 모든 클라이언트가 동시에 재시도하면 효과가 없을 수 있다고 설명해요. 그래서 지수 백오프로 간격을 늘리고, 지터로 시점을 흩뿌리며, 재시도 횟수를 로컬 토큰 버킷으로 제한하는 방법을 함께 씁니다.

Q. 지터는 왜 필요한가요?

지수 백오프만 쓰면 실패한 클라이언트들이 같은 계산식으로 같은 시점에 다시 몰릴 수 있습니다. AWS 글은 이 문제를 피하려고 지터를 사용한다고 밝혀요. 대기 시간에 무작위성을 섞어 재시도 시점을 흩뿌리면, 과부하로 흔들리던 서비스가 같은 순간 다시 요청 더미를 맞는 상황을 줄일 수 있습니다.

Q. 모든 요청을 재시도해도 되나요?

아니에요. AWS 글은 타임아웃이나 실패가 곧 부수 효과가 일어나지 않았다는 뜻은 아니라고 지적합니다. 요청이 이미 처리됐는데 응답만 못 받았을 수 있다는 뜻이죠. 그래서 부수 효과가 여러 번 일어나면 곤란하다면 API를 멱등하게 설계하는 것이 모범 사례입니다. 읽기 전용 API는 대체로 멱등하지만 자원 생성 API는 그렇지 않을 수 있어요.

Q. 서킷 브레이커와 재시도는 무엇이 다른가요?

Azure 문서는 목적이 다르다고 명시합니다. 재시도는 결국 성공할 것이라는 기대로 다시 시도하게 해 주고, 서킷 브레이커는 실패할 가능성이 높은 작업을 아예 수행하지 못하게 막아요. 함께 쓸 수 있지만, 서킷이 일시적 결함이 아니라고 알리면 재시도 로직은 시도를 멈춰야 합니다.

비용 절감부터 차별화된 속도와 안정적 운영까지
기업에 최적화된 IT 환경을 지원합니다

비용 절감부터 차별화된 속도와
안정적 운영까지 기업에 최적화된 IT 환경을 지원합니다

비용 절감부터
차별화된 속도와 안정적 운영까지
기업에 최적화된 IT 환경을 지원합니다

(주)스피디

경기도 성남시 수정구 위례서일로 18, 1101호 (위례 더존메디컬타워)

TEL 031-697-8413

FAX 02-6455-4743

E.mail sales@speedykorea.com

© SPEEDY. All rights reserved

(주)스피디

경기도 성남시 수정구 위례서일로 18, 1101호
(위례 더존메디컬타워)


TEL 031-697-8413

FAX 02-6455-4743

E.mail sales@speedykorea.com

© SPEEDY. All rights reserved

(주)스피디

경기도 성남시 수정구 위례서일로 18, 1101호
(위례 더존메디컬타워)


TEL 031-697-8413

FAX 02-6455-4743

E.mail sales@speedykorea.com

© SPEEDY. All rights reserved