🔌 1. 왜 “두꺼비집"이라고 부를까?
집에 누전이 되면 전체 정전을 막기 위해 두꺼비집(배선 차단기)이 내려갑니다. MSA에서도 마찬가지입니다. B 서비스가 죽었을 때, 이를 호출하는 A 서비스까지 같이 느려지다 죽는 것(Cascade Failure) 을 막기 위해 회로를 끊어버립니다.
1-1. 장애 전파 시나리오
sequenceDiagram
participant Client
participant CB as CircuitBreaker
participant Service
Client->>CB: Request 1
CB->>Service: Call
Note right of Service: Timeout! (5s)
Service--xCB: Failure
CB--xClient: TimeoutException
Client->>CB: Request 2 ... N
CB->>Service: Call
Service--xCB: Failure (x N times)
Note over CB: Failure Rate > Threshold
Note over CB: State: CLOSED -> OPEN
Client->>CB: Request N+1
CB--xClient: CallNotPermittedException (Fail Fast)
Note right of Client: No waiting, instant fail
🚦 2. 상태 기계 (State Machine)
서킷 브레이커는 3가지 상태를 오가며 시스템을 보호합니다.
stateDiagram-v2
[*] --> CLOSED: 초기 상태 (정상)
CLOSED --> OPEN: 실패율 임계치 초과 (차단)
note right of OPEN: 즉시 에러 반환 (Fail Fast)
OPEN --> HALF_OPEN: 대기 시간 경과 (간 보기)
HALF_OPEN --> CLOSED: 시험 호출 성공
HALF_OPEN --> OPEN: 시험 호출 실패
- CLOSED (닫힘): 정상. 전기가 잘 통함. (트래픽 통과)
- OPEN (열림): 차단됨. 전기가 안 통함. (호출 즉시 차단 예외 발생)
- HALF_OPEN (반 열림): “이제 좀 괜찮나?” 하고 몇 개만 살짝 보내봄. 성공하면 닫고, 실패하면 다시 엽니다.
🛡️ 3. Resilience4j 실전 설정
“실패가 몇 번 나면 끊을래?“를 결정하는 것이 핵심입니다.
resilience4j:
circuitbreaker:
instances:
myService:
failureRateThreshold: 50 # 50% 실패하면 Open
slidingWindowSize: 100 # 최근 100개 요청 기준
minimumNumberOfCalls: 10 # 최소 10개는 표본이 쌓여야 함
waitDurationInOpenState: 10s # 10초 동안 차단 유지 후 Half-Open
waitDurationInOpenState: 10s # 10초 동안 차단 유지 후 Half-Open
permittedNumberOfCallsInHalfOpenState: 3 # Half-Open 때 3개만 보내봄
3-2. Sliding Window (집계 방식)
최근 N개의 요청을 저장하고, 그 중 실패 비율을 계산합니다.
graph LR
subgraph "Sliding Window (Size 10)"
direction LR
R1[OK] --- R2[OK] --- R3[Fail] --- R4[OK] --- R5[Fail] --- R6[Fail] --- R7[OK] -.- R10[New Request]
end
style R3 fill:#ffcdd2,stroke:#c62828
style R5 fill:#ffcdd2,stroke:#c62828
style R6 fill:#ffcdd2,stroke:#c62828
style R10 fill:#fff9c4,stroke:#fbc02d
Fallback (대안)
차단되었을 때 클라이언트에게 “에러"만 던지면 안 되겠죠? Fallback 메소드를 통해 “기본값"이라도 줘야 합니다.
@CircuitBreaker(name = "myService", fallbackMethod = "fallbackHello")
public String callExternalServer() {
return restTemplate.getForObject("/api/hello", String.class);
}
// 🚧 장애 시 실행될 메소드
public String fallbackHello(Throwable t) {
log.error("외부 서버 죽음: {}", t.getMessage());
return "잠시 점검 중입니다. (기본 응답)";
}
⚠️ 4. 주의사항: “Thread Pool Hell”
서킷 브레이커 없이 Timeout만 걸면 어떻게 될까요?
응답이 30초 걸리는 장애 서버에 요청이 몰리면, 내 서버의 스레드 풀(Thread Pool)이 대기하느라 꽉 차버립니다. (Bulkhead 패턴이 필요한 이유)
서킷 브레이커는 “아예 요청을 안 보내고(Fail Fast)” 스레드를 즉시 반환하게 하여 내 서버를 살립니다.
요약
- 목적: 장애 전파 방지 (나라도 살자).
- 상태: Normal(Closed) -> Error(Open) -> Test(Half-Open).
- Fallback: 안 될 때 줄 수 있는 ‘차선책’을 준비해라.
- 설정: 너무 빨리 열리면 민감하고, 너무 늦게 열리면 장애가 전파된다.
💬 댓글