
서킷 브레이커(Circuit Breaker): 분산 시스템의 두꺼비집
집에 누전이 발생하면 두꺼비집이 내려가 전체 정전을 막습니다. 마이크로서비스에서도 한 서비스의 장애가 전체로 전파되는 것을 막기 위해 서킷 브레이커 패턴과 벌크헤드(Bulkhead) 패턴이 필수적입니다.

집에 누전이 발생하면 두꺼비집이 내려가 전체 정전을 막습니다. 마이크로서비스에서도 한 서비스의 장애가 전체로 전파되는 것을 막기 위해 서킷 브레이커 패턴과 벌크헤드(Bulkhead) 패턴이 필수적입니다.
로버트 C. 마틴(Uncle Bob)이 제안한 클린 아키텍처의 핵심은 무엇일까요? 양파 껍질 같은 계층 구조와 의존성 규칙(Dependency Rule)을 통해 프레임워크와 UI로부터 독립적인 소프트웨어를 만드는 방법을 정리합니다.

프링글스 통(Stack)과 맛집 대기 줄(Queue). 가장 기초적인 자료구조지만, 이걸 모르면 재귀 함수도 메시지 큐도 이해할 수 없습니다.

결제 API 연동이 끝이 아니었다. 중복 결제 방지, 환불 처리, 멱등성까지 고려하니 결제 시스템이 왜 어려운지 뼈저리게 느꼈다.

페이스북은 왜 REST API를 버렸을까? 원하는 데이터만 쏙쏙 골라 담는 GraphQL의 매력과 치명적인 단점 (캐싱, N+1 문제) 분석.

여러분이 사는 아파트 옆집에서 누전으로 불이 났다고 상상해봤다. 만약 아파트 전체 전선이 하나로 이어져 있고 아무런 안전장치가 없다면, 그 불꽃은 전선을 타고 우리 집 냉장고와 TV까지 태워버릴 것입니다. 이것을 막기 위해 집집마다 두꺼비집(Circuit Breaker, 누전 차단기)이 있습니다. 어느 한 곳에서 과전류가 흐르면, 딱 그 부분의 연결만 "탁!" 하고 끊어버립니다. 덕분에 아파트 전체가 정전되거나 불타는 것을 막을 수 있습니다.
마이크로서비스 아키텍처(MSA)에서도 똑같은 일이 벌어집니다. 서비스 A가 서비스 B를 호출하는데, B가 장애로 응답을 못 하는 상황입니다. 이때 A가 "야, 대답 좀 해봐!" 하고 계속 요청을 보내면(Retry), A도 응답을 기다리느라 스레드(Thread)가 고갈되어 같이 죽어버립니다. A가 죽으면 A를 호출하던 C도 죽고, 결국 시스템 전체가 셧다운되는 연쇄 장애(Cascading Failure)가 일어납니다.
이때 필요한 것이 소프트웨어 버전의 서킷 브레이커 패턴입니다.
서킷 브레이커는 단순한 On/Off 스위치가 아닙니다. 상황에 따라 3단계 상태를 오가는 스마트한 문지기입니다.
많은 개발자들이 에러가 나면 무지성으로 try-catch 안에 while(retry < 3)를 넣습니다.
하지만 장애 상황에서 재시도는 기름을 붓는 격입니다.
서킷 브레이커는 이 악순환을 끊습니다. "지금 쟤 상태 안 좋아. 건드리지 마." 하고 아예 요청을 차단을 시켜버려서, 결제 서비스가 숨을 돌리고 DB 락을 풀 시간을 벌어주는 것입니다.
서킷 브레이커와 절친인 친구들이 있습니다.
배(Ship)는 바닥에 구멍이 뚫려도 전체가 가라앉지 않도록 여러 개의 격벽(Bulkhead)으로 나뉘어 있습니다.
소프트웨어에서도 자원을 격리해야 합니다.
Tomcat 스레드 풀을 하나만 쓰면, API A가 느려져서 스레드를 다 잡아먹을 때 API B도 덩달아 응답을 못 줍니다.
Resilience4j 같은 라이브러리는 서비스별로 스레드 풀이나 세마포어를 분리할 수 있습니다. 결제 서비스용 스레드가 꽉 차도, 검색 서비스용 스레드는 남아있게 하는 것이죠.
특정 클라이언트는 1분에 100회만 요청 가능하도록 제한합니다. 이는 DDoS 공격을 방어하거나 리소스를 공평하게 분배하는 데 쓰입니다.
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을 실행하여 시스템 안정성을 지킵니다.
최근에는 애플리케이션 코드에 서킷 브레이커 로직을 넣는 것보다, 인프라 레벨에서 사이드카(Sidecar)로 처리하는 것이 추세입니다. Istio나 Linkerd 같은 서비스 메시 도구들은 네트워크 트래픽을 가로채고, 자동으로 서킷 브레이커 기능을 수행합니다.
분산 시스템에서 "절대 죽지 않는 서버"는 없습니다. 네트워크는 끊기고, 하드웨어는 고장 나고, 배포하다가 버그는 생깁니다. 우리의 목표는 "장애가 없는 시스템"이 아니라 "장애를 견디는(Resilient) 시스템"이어야 합니다.
"한 놈이 죽었을 때 다 같이 죽지 않는 것"이 핵심입니다. 서킷 브레이커는 시스템이 부분적으로 고장 나더라도, 전체 기능이 마비되는 것을 막아주는 가장 강력한 안전벨트입니다. 오늘 여러분의 서비스 대시보드를 켜고 확인해 보세요. 혹시 재시도 로직 때문에 당신의 서버가 고통받고 있지는 않나요?