마이크로서비스 아키텍처 · 회복탄력성(타임아웃·재시도·서킷브레이커) · 이론
서킷 브레이커의 상태 기계
한 줄 요약
서킷 브레이커는 장애를 고치지 않는다. 장애가 확실할 때 빠르게 실패해서, 호출자의 자원이 마르지 않게 지킨다.
왜 이게 필요했나
재시도만으로는 부족합니다. 다운스트림이 정말로 죽어 있을 때 재시도는 상황을 악화시키기만 합니다. 그리고 호출자 입장에서도, 어차피 실패할 호출을 3초씩 기다리는 것은 커넥션과 스레드의 낭비입니다.
서킷 브레이커는 "최근 결과를 보니 이 서비스는 지금 안 된다"고 판단하면 호출을 아예 시도하지 않고 즉시 실패를 돌려줍니다. 마이크로초 단위로 실패하므로 호출자의 자원은 마르지 않고, 다운스트림은 회복할 숨을 얻습니다.
어떻게 동작하나
세 상태입니다.
CLOSED 는 정상입니다. 모든 호출이 통과하고, 결과를 슬라이딩 윈도우에 기록합니다. 윈도우 안의 실패율이 임계값을 넘으면 OPEN 으로 갑니다.
OPEN 은 차단입니다. 모든 호출이 즉시 거부됩니다. waitDurationInOpenState 가 지나면 HALF_OPEN 으로 갑니다.
HALF_OPEN 은 시험입니다. 정해진 수의 시험 호출만 허용하고, 그 결과로 CLOSED 로 복귀하거나 OPEN 으로 돌아갑니다.
설정값에는 정답이 없지만 출발점은 있습니다. 저자가 운영에서 쓰는 값은 이렇습니다. slidingWindowSize: 10, minimumNumberOfCalls: 5, failureRateThreshold: 50, slowCallDurationThreshold: 3s, waitDurationInOpenState: 30s, permittedNumberOfCallsInHalfOpenState: 3. 서비스 성격에 따라 임계값을 조정합니다 — 결제처럼 실패가 치명적인 곳은 30~40%, 알림처럼 관대해도 되는 곳은 60~70% 입니다.
여기서 가장 중요한 설계 결정 하나. 무엇을 실패로 셀 것인가입니다. 4xx 는 실패가 아닙니다. 클라이언트가 잘못 보낸 것이므로 서킷이 개입할 사안이 아닙니다. 실제 사고 사례가 있습니다. 재고 서비스가 특정 상품에 400 을 돌려주기 시작했는데, 그 400 이 실패율에 집계되면서 서킷이 OPEN 이 됐고, 정상 상품 조회까지 전부 차단됐습니다. 서킷은 5xx, 타임아웃, 연결 실패에만 반응해야 합니다.
현장에서 만나는 모습
두 번째로 흔한 사고는 HALF_OPEN 플래핑입니다. permittedNumberOfCallsInHalfOpenState 를 1 로 두면 시험 호출 한 건이 우연히 실패할 때마다 다시 OPEN 으로 떨어집니다. 다운스트림이 간헐적으로만 응답하는 상황에서 영영 CLOSED 로 못 돌아옵니다. 최소 3, 보통 5~10 을 씁니다.
세 번째. minimumNumberOfCalls 의 기본값이 100 인 구현이 많습니다. 호출 빈도가 낮은 서비스에서는 윈도우가 채워지기 전에 장애가 끝나 버려서 서킷이 한 번도 안 열립니다. "서킷을 넣었는데 안 열려요"의 절반은 이 이유입니다.
그리고 서킷은 반드시 폴백과 함께 씁니다. 폴백이 return null 이면 서킷은 장애를 NullPointerException 으로 바꾼 것에 불과합니다. 캐시된 값, 축소된 응답, 또는 명확한 안내 중 하나여야 합니다.
다음 실습에서 할 것
서킷 브레이커를 직접 구현합니다. 상태 전이를 눈으로 보고, OPEN 에서 다운스트림에 호출이 정말 가지 않는지 카운터로 증명하고, 4xx 를 실패로 세지 않게 만들고, 마지막에 전체 전이 이력을 파일로 남깁니다.