1. 옆집 불이 우리 집까지 번지지 않으려면
여러분이 사는 아파트 옆집에서 누전으로 불이 났다고 상상해봤다. 만약 아파트 전체 전선이 하나로 이어져 있고 아무런 안전장치가 없다면, 그 불꽃은 전선을 타고 우리 집 냉장고와 TV까지 태워버릴 것입니다. 이것을 막기 위해 집집마다 두꺼비집(Circuit Breaker, 누전 차단기)이 있습니다. 어느 한 곳에서 과전류가 흐르면, 딱 그 부분의 연결만 "탁!" 하고 끊어버립니다. 덕분에 아파트 전체가 정전되거나 불타는 것을 막을 수 있습니다.
마이크로서비스 아키텍처(MSA)에서도 똑같은 일이 벌어집니다. 서비스 A가 서비스 B를 호출하는데, B가 장애로 응답을 못 하는 상황입니다. 이때 A가 "야, 대답 좀 해봐!" 하고 계속 요청을 보내면(Retry), A도 응답을 기다리느라 스레드(Thread)가 고갈되어 같이 죽어버립니다. A가 죽으면 A를 호출하던 C도 죽고, 결국 시스템 전체가 셧다운되는 연쇄 장애(Cascading Failure)가 일어납니다.
이때 필요한 것이 소프트웨어 버전의 서킷 브레이커 패턴입니다.
2. 서킷 브레이커의 3가지 상태 (State Machine)
서킷 브레이커는 단순한 On/Off 스위치가 아닙니다. 상황에 따라 3단계 상태를 오가는 스마트한 문지기입니다.
2.1. Closed (닫힘 - 정상 운행)
- 의미: 회로가 닫혀 있어 전기가 잘 흐르는 상태입니다. 평소처럼 API 요청을 정상적으로 보냅니다.
- 동작: 요청이 실패하면 내부적으로 실패 횟수를 카운트합니다.
- 임계치(Threshold): 만약 "최근 10초 동안 에러율 50% 초과" 같은 조건이 충족되면, 즉시 Open 상태로 전환합니다.
- 슬라이딩 윈도우: 최근 N개의 요청(Count-based) 또는 최근 N초(Time-based) 동안의 기록만 저장하여 판단합니다.
2.2. Open (열림 - 차단됨)
- 의미: 회로가 끊어졌습니다. 전기가 안 통합니다.
- 동작: API 요청을 아예 보내지 않습니다. 외부 서비스로 나가는 요청을 가로채서 즉시 에러를 리턴(Fail Fast)하거나, 미리 준비된 기본값(Fallback)을 리턴합니다.
- 목적: 죽어가는 서비스 B에게 쉴 시간(Recovery Time)을 줍니다. 무리하게 요청을 보내봤자 어차피 실패할 것이 뻔하고, B를 더 힘들게 할 뿐이니까요. 일정 시간(예: 5초)이 지나면 Half-Open 상태로 전환합니다.
2.3. Half-Open (반 열림 - 간 보기)
- 의미: "이제 좀 괜찮아졌나?" 하고 조심스럽게 확인하는 상태입니다.
- 동작: 딱 한 번(또는 정해진 소수)의 요청만 허용해 봅니다. 일종의 '정찰병'을 보내는 것입니다.
- 성공하면: "오, 살아났네!" 하고 Closed 상태로 돌아가 정상 영업을 재개합니다. 실패 카운터도 초기화됩니다.
- 실패하면: "아직 글렀네." 하고 다시 Open 상태로 돌아가서 더 기다립니다.
3. 재시도(Retry) 패턴의 위험성: Thundering Herd
많은 개발자들이 에러가 나면 무지성으로 try-catch 안에 while(retry < 3)를 넣습니다.
하지만 장애 상황에서 재시도는 기름을 붓는 격입니다.
시나리오
- 결제 서비스 DB에 락(Lock)이 걸려서 처리가 아주 느려졌습니다.
- 주문 서비스가 요청을 보냈는데 3초 타임아웃이 났습니다.
- 주문 서비스는 "어? 실패했네? 다시 시도!" 하고 또 보냅니다. (트래픽 2배)
- 사용자도 앱에서 "어? 반응이 없네?" 하고 '구매하기' 버튼을 연타합니다. (트래픽 3~4배)
- 결제 서비스는 가뜩이나 힘든데 요청이 평소의 몇 배로 폭주합니다. 이를 Thundering Herd Problem (우르르 몰려가는 떼)이라고 합니다.
- 결국 결제 서비스 DB는 회복 불능 상태로 사망합니다.
서킷 브레이커는 이 악순환을 끊습니다. "지금 쟤 상태 안 좋아. 건드리지 마." 하고 아예 요청을 차단을 시켜버려서, 결제 서비스가 숨을 돌리고 DB 락을 풀 시간을 벌어주는 것입니다.
4. 벌크헤드 및 기타 탄력적 패턴
서킷 브레이커와 절친인 친구들이 있습니다.
4.1. 벌크헤드 패턴 (Bulkhead Pattern)
배(Ship)는 바닥에 구멍이 뚫려도 전체가 가라앉지 않도록 여러 개의 격벽(Bulkhead)으로 나뉘어 있습니다.
소프트웨어에서도 자원을 격리해야 합니다.
Tomcat 스레드 풀을 하나만 쓰면, API A가 느려져서 스레드를 다 잡아먹을 때 API B도 덩달아 응답을 못 줍니다.
Resilience4j 같은 라이브러리는 서비스별로 스레드 풀이나 세마포어를 분리할 수 있습니다. 결제 서비스용 스레드가 꽉 차도, 검색 서비스용 스레드는 남아있게 하는 것이죠.
4.2. 속도 제한 (Rate Limiting)
특정 클라이언트는 1분에 100회만 요청 가능하도록 제한합니다. 이는 DDoS 공격을 방어하거나 리소스를 공평하게 분배하는 데 쓰입니다.
5. 구현 예시 (Resilience4j, Java)
Java 진영에서는 Netflix Hystrix(현재는 유지보수 중단) 대신 Resilience4j를 표준처럼 사용합니다. 가볍고, 함수형 프로그래밍 스타일을 지원합니다.
# application.yml 설정 예시
resilience4j.circuitbreaker:
instances:
paymentService:
failureRateThreshold: 50 # 에러율 50% 넘으면 차단
minimumNumberOfCalls: 10 # 최소 10번은 호출해보고 판단
slidingWindowType: COUNT_BASED # 시간(TIME) 또는 횟수(COUNT) 기준
slidingWindowSize: 10 # 최근 10개 기록만 봄
waitDurationInOpenState: 5000ms # 차단 후 5초 대기
automaticTransitionFromOpenToHalfOpenEnabled: true
// 코드 적용
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallback")
public String processPayment(PaymentRequest req) {
return paymentApi.pay(req); // 외부 API 호출
}
// Fallback 메서드 (차단되었을 때 대신 실행됨)
public String fallback(PaymentRequest req, Throwable t) {
// 1. "죄송합니다. 결제 시스템 점검 중입니다." 메시지 리턴
// 2. 또는 로컬 캐시에 있는 예전 데이터 리턴 (Graceful Degradation)
log.error("Payment failed", t);
return "Pending";
}
이 코드가 적용되면, paymentApi 장애 시 processPayment는 즉시 fallback을 실행하여 시스템 안정성을 지킵니다.
6. 서비스 메시 (Service Mesh)와 미래
최근에는 애플리케이션 코드에 서킷 브레이커 로직을 넣는 것보다, 인프라 레벨에서 사이드카(Sidecar)로 처리하는 것이 추세입니다. Istio나 Linkerd 같은 서비스 메시 도구들은 네트워크 트래픽을 가로채고, 자동으로 서킷 브레이커 기능을 수행합니다.
장점 (Pros)
- 언어 중립적: Java, Python, Go, Node.js 서비스가 섞여 있어도 동일한 정책을 적용할 수 있습니다.
- 코드 분리: 비즈니스 로직과 인프라 관심사가 깔끔하게 분리됩니다.
단점 (Cons)
- 복잡도 증가: 쿠버네티스 설정이 복잡해집니다.
- 섬세함 부족: "DB 에러일 때는 차단하고, 비즈니스 에러일 때는 차단하지 마라" 같은 정교한 로직은 애플리케이션 레벨(Resilience4j)이 더 유리합니다.
7. 마무리 - 실패를 전제로 설계하라
분산 시스템에서 "절대 죽지 않는 서버"는 없습니다. 네트워크는 끊기고, 하드웨어는 고장 나고, 배포하다가 버그는 생깁니다. 우리의 목표는 "장애가 없는 시스템"이 아니라 "장애를 견디는(Resilient) 시스템"이어야 합니다.
"한 놈이 죽었을 때 다 같이 죽지 않는 것"이 핵심입니다. 서킷 브레이커는 시스템이 부분적으로 고장 나더라도, 전체 기능이 마비되는 것을 막아주는 가장 강력한 안전벨트입니다. 오늘 여러분의 서비스 대시보드를 켜고 확인해 보세요. 혹시 재시도 로직 때문에 당신의 서버가 고통받고 있지는 않나요?