본문 바로가기
Framework/Spring Boot

[Spring Boot] Resilience4j 서킷 브레이커: 장애의 도미노를 끊어라

반응형

마이크로서비스 아키텍처(MSA)는 시스템을 여러 개로 쪼개어 하나가 죽어도 다른 놈들은 살려두자는 철학에서 출발했습니다. 그런데 현실은 정반대입니다. 사용자가 상품을 주문할 때, [주문 서버] -> [결제 서버] -> [재고 서버] 순으로 서로 꼬리를 물고 API를 호출(RestTemplate, FeignClient)하는 구조를 생각해 봅시다. 만약 막내인 [재고 서버]의 DB에 락(Lock)이 걸려 응답이 10초씩 지연된다면? 결제 서버도 응답을 기다리다 쓰레드(Thread)가 고갈되어 멈추고, 주문 서버도 멈추고, 결국 맨 앞의 클라이언트(앱)도 하얀 화면에서 멈춰버립니다. 제일 밑바닥에 있는 마이크로서비스 하나가 아프면, 그를 호출하는 모든 윗단 서비스들이 도미노처럼 쓰러져(Cascading Failure) 회사 전체가 마비되는 MSA 최악의 악몽이 벌어지는 것입니다. 이 끔찍한 연쇄 붕괴를 막기 위해 "상대방 서버가 아파 보이면, 아예 호출(전기) 자체를 끊어버려서 내 몸이라도 살리자!"라는 두꺼비집(누전 차단기) 개념이 도입되었습니다. 이것이 바로 서킷 브레이커(Circuit Breaker) 패턴입니다.

이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "MSA 최후의 방어막!" Resilience4j를 활용한 서킷 브레이커와 Fallback(대체) 설계에 대하여 다뤄보겠습니다.

⚡ 1. 서킷 브레이커의 3가지 상태 (두꺼비집 원리)

Resilience4j(과거 넷플릭스 Hystrix의 모던 대체제)는 호출 대상(재고 서버)의 상태를 모니터링하며 3가지 스위치 모드를 작동시킵니다.

Circuit Breaker 개념도
  • CLOSED (닫힘 = 정상 통전): 전기가 잘 통하는 정상 상태입니다. 주문 서버가 재고 서버로 API 요청을 자유롭게 보냅니다.
  • OPEN (열림 = 회로 차단!): 10번 호출했는데 5번이 에러(500)나 타임아웃이 났습니다. Resilience4j가 "재고 서버 맛탱이 갔네! 당분간 호출 금지!"라며 회로를 끊어버립니다(OPEN). 이후의 요청은 재고 서버로 아예 날아가지도 않고, 내 안에서 즉각 에러(CallNotPermittedException)를 뱉어버립니다. 이를 '빠른 실패(Fail-Fast)'라고 하며, 쓸데없이 기다리다 같이 죽는 것을 방지합니다!
  • HALF_OPEN (반열림 = 간 보기): OPEN 상태로 1분이 지났습니다. 재고 서버가 살아났는지 궁금해진 Resilience4j는 회로를 살짝 닫고(HALF_OPEN) 테스트용 요청 몇 개만 흘려보냅니다. 성공하면 다시 CLOSED로 복구하고, 실패하면 다시 얄짤없이 OPEN(차단)으로 돌려버립니다.

🛡️ 2. Resilience4j 실전 적용과 Fallback (플랜 B)

회로가 끊어져서(OPEN) 즉각 에러가 터졌습니다. 하지만 고객 화면에 '500 서버 에러' 창을 띄울 수는 없습니다. "추천 상품 서버가 죽었어? 에러 띄우지 말고, 그냥 DB에 캐싱해 둔 '기본 추천 상품 리스트'라도 보여줘서 화면을 살려!" 이것을 Fallback(대체 플랜)이라고 부릅니다.

@Service
@Slf4j
public class ProductRecommendationService {

    // 🌟 이 메서드를 호출할 때 "recommend-api"라는 이름의 서킷 브레이커 룰을 적용!
    // 만약 타겟 서버가 죽어서 회로가 차단(OPEN)되거나 타임아웃이 나면, "fallbackRecommend" 메서드를 대신 실행해라!
    @CircuitBreaker(name = "recommend-api", fallbackMethod = "fallbackRecommend")
    public List<Product> getRecommendations(String userId) {
        // (불안정한 타겟 서버) 추천 시스템 API를 HTTP(Feign, RestTemplate 등)로 호출합니다.
        return feignClient.getRecommendationsFromAI(userId);
    }

    // 🛡️ 플랜 B (Fallback 메서드): 원래 메서드와 파라미터/리턴 타입이 완벽히 똑같아야 하고, 끝에 예외(Throwable)를 받습니다.
    public List<Product> fallbackRecommend(String userId, Throwable t) {
        log.warn("추천 서버 장애 발생! 회로 차단됨. 기본 상품으로 대체 응답. 원인: {}", t.getMessage());
        
        // AI 추천 서버가 죽었으니, 그냥 하드코딩된 인기 상품 TOP 3을 던져서 화면 에러를 막습니다!
        return Arrays.asList(
            new Product("신라면"), new Product("삼다수"), new Product("초코파이")
        );
    }
}

🎯 3. 마무리 및 다음 단계

지금까지 분산 시스템에서 피할 수 없는 '하나의 장애가 전체로 번지는(Cascading Failure)' 최악의 상황을 원천 차단하고, 빠른 실패(Fail-Fast)와 Fallback(플랜 B)을 통해 사용자 경험(UX)을 우아하게 방어해 내는 현대 MSA의 필수 방패, Resilience4j 서킷 브레이커 패턴에 대해 상세히 다루어 보았습니다. 이제 여러분의 마이크로서비스는 어떤 험난한 트래픽 폭풍 속에서도 스스로를 지켜낼 것입니다.

서버 아키텍처(MSA, 배포, 모니터링, 보호망)는 거의 완벽해졌습니다. 이제 다시 코드 레벨의 치명적인 비즈니스 문제로 돌아와 봅시다. 쇼핑몰에 한정판 나이키 신발이 딱 1개 남았습니다. 철수와 영희가 동시에 0.001초의 오차도 없이 [결제하기] 버튼을 눌렀습니다. 분명 재고는 1개인데, DB 트랜잭션이 겹치면서 철수도 1개 성공, 영희도 1개 성공, 총 2개가 팔려버리는 기적(초과 판매 버그)이 발생합니다. 일반적인 싱글 서버라면 synchronized 키워드로 막겠지만, 우리는 서버가 여러 대(MSA)로 찢어져 있어서 자바의 synchronized가 전혀 통하지 않습니다! 이어지는 40단계 포스팅에서는 "수십 대의 서버가 하나의 재고를 두고 싸울 때, 완벽한 줄 세우기를 구현하는 절대 반지!" Redis(레디스)를 활용한 분산 락(Distributed Lock)과 Redisson 동시성 제어 실무 비법에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!

반응형