Circuit Breaker, Fallback 그리고 Graceful Degradation

Circuit Breaker

앞에서는 재시도와 타임아웃으로 장애를 대응했습니다. 하지만 장애가 일시적인 것이 아니라, 시스템이 감당할 수 없는 수준의 부하(Traffic Spike)로 인해 서버가 다운될 위기라면 어떻게 해야 할까요?

계속 재시도를 통한 응답을 대기하면서 서버 리소스를 사용해야 할까요? 이때는 과감하게 트래픽을 차단하여 시스템이 회복할 수 있는 시간을 주는 것이 장애를 전파시키지 않고 서비스 지속할 수 있습니다.

서킷 브레이커의 3가지 상태

  1. Closed (정상): 회로가 닫혀있어 트래픽이 정상적으로 흐릅니다.

  2. Open (차단): 오류율이나 응답 지연이 임계치(Threshold)를 넘으면 회로가 열립니다. 모든 요청을 즉시 차단하고 에러를 반환하거나 폴백(Fallback) 로직을 실행합니다.

  3. Half-Open (반 열림): 차단 후 일정 시간이 지나면 회복하기 위한 상태입니다. 소량의 트래픽만 흘려보내 성공하면 Closed로, 실패하면 다시 Open으로 돌아갑니다.

왜 Half-Open이 필요한가요?

Open에서 바로 Closed로 가면, 아직 시스템이 회복되지 않았는데 갑자기 대량의 트래픽이 쏟아져 다시 죽을 수 있습니다. 소량의 테스트 요청으로 안전을 확인하는 단계가 필수적입니다.

예시: 트래픽으로부터 메일 서비스 보호하기

[상황 설정]

  • Main Service: 결제 후 메일 발송을 요청합니다.

  • Mail Service: 메일 발송을 담당합니다. (성능이 매우 낮게 설정됨: Thread 3개, Queue 30개)

  • 장애 유발: 10초 후 트래픽을 1 RPS에서 20 RPS로 급격히 증가시킵니다.

이 상황에서 메일 서비스가 부하를 견디지 못하고 Connection RefusedTimeout을 발생하면, 서킷 브레이커를 발동시켜 Main Service에서 요청을 미리 차단합니다.

[테스트 결과]

  1. 정상 구간 (0~10초): 1 RPS. 요청이 잘 처리됨. (Closed)

  2. 부하 발생 (10~20초): 20 RPS로 증가. 메일 서비스의 스레드와 큐가 꽉 차서 TimeoutConnection Refused 발생.

  3. 서킷 발동 (Open): 실패율이 30%를 넘자 서킷이 열림. 이후 요청은 아예 메일 서비스로 가지 않고 "CallNotPermittedException"이 발생하며 즉시 차단됨.

  4. 회복 시도 (Half-Open): 10초 후 상태가 Half-Open으로 변경. 테스트 요청 3개가 성공함.

  5. 정상화 (Closed): 다시 Closed 상태로 돌아와 정상 서비스 재개.

주의 사항

1. 멱등성(Idempotency) 문제

서킷 브레이커나 타임아웃으로 인해 클라이언트(Main Service) 입장에서는 "실패"로 처리되었지만, 서버(Mail Service)에서는 큐에 쌓여있던 요청이 뒤늦게 "성공"할 수 있습니다. 이때 클라이언트가 재시도 혹은 폴백 로직을 통해 메일이 두 번 발송될 수 있습니다. 따라서 API는 반드시 멱등성을 보장하도록 설계해야 합니다.

  • 서킷이 closed 상태일 때, 메인 요청이 정상적으로 메일 서버로 전송되더라도, timeout 시간 안에 응답이 오지 않는다면 이를 실패로 간주하고 폴백을 실행

    • closed에서 전송된 요청은 메일 서버 대기 큐에서 대기중

    • 폴백 로직에서 메일 서버로 다시 요청을 전송 → 중복 요청

2. 예외 선별

모든 예외에 대해 서킷을 열면 안 됩니다.

  • 유효성 검사 실패(400 Bad Request): 이건 클라이언트 잘못이지 시스템 부하 문제가 아닙니다. 서킷을 열 필요가 없습니다.

  • 연결 거부, 타임아웃(500 Server Error): 이런 시스템 장애 관련 예외만 등록해야 합니다.

Fallback과 Graceful Degradation

서킷 브레이커는 상태에 따라 트래픽을 제어해주는 역할을 했었습니다.

서킷이 열려서 요청이 차단되면, 사용자는 그냥 하얀 에러 페이지만 봐야 할까요?

멀티 인스턴스 환경에서의 서킷 브레이커 활용법

우리가 로컬에서 테스트할 때는 서버가 딱 한 대였습니다. 하지만 실제 운영 환경(Production)에서는 트래픽 분산을 위해 동일한 애플리케이션이 여러 대(Instance A, B, C...)가 동작하고 있습니다.

여기서 문제가 발생합니다. 서킷 브레이커의 상태(Open/Closed)는 기본적으로 인스턴스 메모리 내에서 관리되기 때문입니다.

[문제 상황]

  1. 상황: 외부의 '메일 서비스'가 느려지기 시작합니다.

  2. Instance A:

    • 오류 임계치를 넘어 서킷이 열렸습니다(Open). 메일 서비스로의 요청을 차단합니다.

  3. Instance B:

    • 여러 복합적인 요소로 아직 임계치를 넘지 않아 서킷이 닫혀있습니다(Closed).

    • 계속해서 불안정한 메일 서비스로 요청을 보냅니다.

[무엇이 문제인가요?]

  • 회복 지연:

    • 서킷 브레이커의 주 목적은 장애가 난 서비스에 '쉴 시간'을 주는 것입니다.

    • 하지만 다른 인스턴스들이 계속 요청을 보내 괴롭힌다면, 장애 서비스는 회복하지 못하고 요청이 계속 들어가서 회복이 지연될 수 있습니다.

  • 사용자 경험의 불일치:

    • 어떤 사용자는 "잠시 후 시도해주세요.(Instance A)"라는 빠른 응답을 받지만, 어떤 사용자는 무한 로딩(Instance B)을 겪게 됩니다.

    • 주식 매매나 티켓팅 같은 민감한 서비스 혹은 실시간 응답이 중요한 서비스라면 이는 공정성 이슈로 번질 수 있습니다.

[해결 방향]

이 문제를 해결하려면 서킷의 상태를 인스턴스끼리 공유해야 합니다. 보통 Redis나 Kafka 같은 외부 저장소를 활용하여 상태를 동기화하지만, 배보다 배꼽이 커질 수 있으므로 현재 인프라 상황에 맞춰 신중하게 도입해야 합니다.

서킷 브레이커를 활용한 Fault Tolerance System(FTS) 구성chevron-right

요청 실패 시의 대안: Fallback

요청이 실패하는 경우. 즉, 서킷이 열리거나 로직에서 예외가 발생했을 때 실행되는 대체 로직을 Fallback이라고 합니다. 이를 통해 주요 기능이 실패했을 때, 대체 동작을 제공하여 해당 기능 수행 실패를 수습하고 대체합니다.

  • 코드에서는 @CircuitBreaker(fallbackMethod = "...")와 같은 형식으로 설정할 수 있습니다.

폴백은 단순히 에러를 뱉는 것 외에, 비즈니스적으로 적용 가능한 대안들을 알아봤습니다.

  1. 기본값 반환 (Return Default)

  • 가장 단순한 방법입니다. null이나 빈 리스트, 혹은 미리 정의된 기본 객체를 반환합니다.

  • 사용자가 에러를 인지하지 못하게 자연스럽게 넘어갈 수 있지만, null과 같은 반환은 주의가 필요합니다.

  1. 재시도 큐에 저장 (Async Retry)

  • 지금은 당장 실패했지만, 데이터가 유실되면 안 되는 경우(예: 주문 관련, 결제 알림 등)에 사용할 수 있습니다.

  • 실패한 요청 데이터를 DB나 메시지/이벤트 브로커에 저장해 두고, 나중에 배치 프로그램이나 별도의 컨슈머가 재처리하도록 설정할 수 있습니다.

  1. 다른 수단으로 대체

  • “이메일 발송”이 실패했다면? SMS 또는 SNS 서비스의 알림 API를 호출하도록 하여 경로를 우회할 수 있습니다.

  • 매체가 바뀌었을 뿐, 사용자가 “메시지를 전달받는다.”라는 목적은 달성할 수 있습니다.

  1. 래핑 된 예외 던지기

  • 다시 예외를 던지는 것이 큰 의미는 없을 수 있지만, 폴백으로도 복구가 불가능한 경우에 사용될 수 있습니다.

  • 이때는 단순한 RuntimeException을 던지기보다는, 시스템에서 정의한 커스텀 예외로 감싸서 던지는 것이 모니터링 측면에서 유리합니다.

사용자 경험을 위한 Graceful Degradation

Graceful Degradation은 폴백을 포함하여 더 넓은 시스템 설계 철학입니다.

시스템의 일부 기능(부가 기능)이 동작하지 않더라도, 핵심 기능은 계속 유지하여 전체 서비스가 완전히 중단되지 않도록 만드는 것을 의미합니다.

  • 서비스를 사용하는 사용자들의 사용자 경험을 어떻게 하면 덜 해칠지에 대해 집중합니다.

[예시]

  • 수강 신청 사이트: '강의 리뷰' 서버가 터졌습니다. 그렇다고 수강 신청 자체를 막아야 할까요?

    • Graceful Degradation: 리뷰 영역은 "불러올 수 없음"으로 표시하거나 아예 숨기고, 핵심 기능인 '강의 목록 조회'와 '수강 신청'은 정상적으로 제공합니다.

  • 포털 메인: '실시간 주식' 위젯 서버가 응답하지 않습니다.

    • Graceful Degradation: 주식 영역만 빈칸으로 두고, 뉴스나 날씨 등 나머지 정보는 정상적으로 보여줍니다.

Last updated