# Circuit Breaker 패턴: 마이크로서비스가 도미노처럼 무너지는 것을 막는 법
Table of Contents
옆 동네 불이 우리 집까지 번진다면?
마이크로서비스 아키텍처(MSA)를 도입하면 서비스가 수십 개로 쪼개집니다. “주문 서비스”, “결제 서비스”, “재고 서비스”, “배송 서비스”…
멋져 보이지만 치명적인 단점이 있습니다. 바로 **장애의 전파(Cascading Failure)**입니다. 만약 “결제 서비스” DB에 락(Lock)이 걸려 응답이 30초씩 걸린다고 해봅시다.
- 사용자가 주문 버튼을 누릅니다.
- “주문 서비스”는 “결제 서비스”를 호출하고 응답을 기다립니다. (스레드 블로킹)
- “결제 서비스”가 답이 없으니, “주문 서비스”의 스레드 풀이 대기 상태인 요청들로 가득 찹니다.
- 결국 “주문 서비스”도 뻗어버립니다.
- “주문 서비스”를 호출하는 프론트엔드 서버도 같이 느려집니다.
결제 하나 안 되는데 전체 쇼핑몰이 마비되는, 전형적인 도미노 현상입니다. 이때 필요한 것이 바로 **Circuit Breaker(회로 차단기)**입니다.
Circuit Breaker의 원리: “안 될 놈은 빨리 자른다”
우리 집 두꺼비집(Circuit Breaker)과 똑같습니다. 과전류가 흐르면(에러가 폭주하면) 퓨즈가 딱 끊어져서 가전제품을 보호하죠.
소프트웨어의 서킷 브레이커도 3가지 상태를 가집니다.
1. CLOSED (정상)
평소 상태입니다. 전기가 잘 흐르듯, 요청이 정상적으로 외부 서비스로 전달됩니다. 이때 에러 횟수나 비율을 모니터링합니다. “최근 100번 호출 중에 50번 실패했네?” -> 기준치를 넘으면 회로를 엽니다.
2. OPEN (차단)
장애가 감지되어 회로가 열린 상태입니다.
이제 외부 서비스로 요청을 아예 보내지 않습니다.
대신 CallNotPermittedException 같은 에러를 즉시 뱉거나, 미리 준비된 기본값(Fallback)을 리턴합니다.
“어차피 보내봤자 실패할 거잖아? 기다리지 말고 빨리 실패하자(Fail Fast)“는 전략입니다. 덕분에 호출하는 쪽의 스레드가 보호됩니다.
3. HALF-OPEN (간 보기)
OPEN 상태로 일정 시간(예: 30초)이 지나면, “이제 좀 살았나?” 하고 테스트 요청을 몇 개만 보내봅니다.
- 성공하면? -> “오 복구됐구나!” -> CLOSED로 복귀
- 또 실패하면? -> “아직 멀었네” -> 다시 OPEN 유지
Resilience4j로 구현하는 실전 예제 (Java/Spring)
과거엔 Netflix Hystrix를 많이 썼지만, 지금은 Resilience4j가 대세입니다. 훨씬 가볍고 함수형 인터페이스를 지원하거든요.
설정 (application.yml)
resilience4j:
circuitbreaker:
instances:
paymentService:
baseConfig: default
failureRateThreshold: 50 # 실패율 50% 넘으면 차단
minimumNumberOfCalls: 10 # 최소 10번은 호출해보고 판단
waitDurationInOpenState: 30s # 차단 후 30초 뒤에 재시도(Half-Open)해봄
slidingWindowType: COUNT_BASED
slidingWindowSize: 20 # 최근 20건의 결과를 보고 판단
코드 적용
@Service
public class OrderService {
private final PaymentClient paymentClient;
// paymentService라는 이름의 서킷 브레이커 적용
// 실패 시 fallbackMethod 실행
@CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackPayment")
public String order(String orderId) {
return paymentClient.processPayment(orderId);
}
// Fallback 메서드: 장애 발생 시 실행될 안전장치
// 파라미터 시그니처가 원본 메서드와 같아야 하며, 마지막에 Exception을 받습니다.
private String fallbackPayment(String orderId, Throwable t) {
log.error("Payment service is down: {}", t.getMessage());
return "결제 서비스 지연으로 나중에 처리됩니다. (임시 주문 접수)";
}
}
이렇게 하면 결제 서비스가 죽었을 때, 사용자는 무한 로딩 대신 “지연 처리됩니다”라는 안내를 받고 쇼핑을 계속할 수 있습니다. 시스템도 안전하게 보호되고요.
실전 운영 팁: 타임아웃과 벌크헤드
서킷 브레이커만으로는 부족할 때가 있습니다.
1. 타임아웃(Timeout)은 짧게
서킷 브레이커가 열리기 전(CLOSED 상태)까지는 여전히 느린 응답을 기다려야 합니다. 그래서 HTTP 클라이언트(RestTemplate, WebClient 등)의 타임아웃 설정이 필수입니다. 보통 1~3초 내외로 짧게 잡는 것이 좋습니다. 30초를 기다려주는 고객은 없습니다.
2. 벌크헤드(Bulkhead) 패턴
선박의 격벽(Bulkhead)처럼 자원을 격리하는 패턴입니다. “결제 서비스 호출용 스레드 풀”과 “재고 서비스 호출용 스레드 풀”을 나누는 겁니다. 결제 쪽이 터져서 스레드를 다 잡아먹어도, 재고 조회 기능은 멀쩡하게 돌아가게 할 수 있습니다.
마치며: 실패를 받아들이는 자세
완벽한 시스템은 없습니다. 구글도 아마존도 장애는 납니다. 시니어 개발자의 역량은 “장애를 안 내는 것”뿐만 아니라, **“장애가 나도 서비스가 우아하게(Gracefully) 동작하게 만드는 것”**에 있습니다.
Circuit Breaker는 그 우아함을 위한 가장 강력한 도구입니다.
지금 여러분의 프로젝트에서 외부 API를 호출하는 코드를 살펴보세요. try-catch만 덩그러니 있다면, 내일의 장애를 막기 위해 서킷 브레이커를 달아보는 건 어떨까요?