Circuit Breakers: 탄력적인 마이크로서비스의 숨은 영웅들

발행: (2026년 5월 29일 AM 10:02 GMT+9)
8 분 소요
원문: Dev.to

I’m happy to translate the article for you, but I need the full text of the post (the part you’d like translated). Could you please paste the content you want translated here? Once I have it, I’ll keep the source line unchanged and provide a Korean translation while preserving all formatting, markdown, and technical terms.

왜 서킷 브레이커가 중요한가

프로덕션 환경에서 여러 서비스를 운영하면 실패는 피할 수 없습니다. 하위 서비스가 지연 시간이 급증하거나 500 오류를 반환하거나 완전히 사라질 수 있습니다. 보호 장치가 없으면 단일 오류가 시스템 전체에 연쇄적으로 퍼져 스레드를 낭비하고, 연결 풀을 고갈시키며, 결국 의존 서비스까지 다운시킬 수 있습니다. 바로 이때 서킷 브레이커가 빛을 발합니다—실패를 증폭시키는 대신 우아하게 감소시킵니다.

아마도 타임아웃과 재시도를 사용해 보셨겠지만, 그것만으로는 충분하지 않습니다. 재시도는 과부하를 악화시키고, 타임아웃은 여전히 리소스를 낭비하게 합니다. 서킷 브레이커는 실패를 모니터링하고, 일정 임계값을 초과하면 호출을 차단해 미리 정의된 폴백을 즉시 반환합니다. 이를 통해 서비스가 실패가 확정된 요청에 CPU를 소모하는 것을 방지하고, 하위 서비스가 부하를 줄인 상태에서 회복할 수 있도록 합니다.

Source:

회로 차단기 상태 머신

상태 머신은 간단합니다:

상태동작
닫힘정상 작동; 모든 호출이 그대로 전달됩니다. 실패가 발생하면 카운터가 증가합니다.
열림원격 서비스에 도달하지 않고 호출이 빠르게 거부됩니다.
반‑열림설정 가능한 타임아웃이 지난 후, 몇 개의 탐색 요청이 허용됩니다. 성공하면 차단기가 닫힘 상태로 재설정되고, 그렇지 않으면 열림 상태로 돌아갑니다.

일반적인 임계값:

  • 실패 비율이 설정된 한도를 초과할 경우 (예: 최근 10회 호출 중 50 % 이상) → 열림 상태로 전환.
  • 타임아웃이 만료될 경우 → 반‑열림 상태로 전환.
  • 탐색이 성공하면 → 닫힘 상태로 복귀.

구현 예시 (Go)

gobreaker(Go)나 resilience4j(Java)와 같은 라이브러리는 보일러플레이트 코드를 추상화합니다. 아래는 gobreaker를 사용한 간결한 예시입니다:

package main

import (
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/sony/gobreaker"
)

var cb *gobreaker.CircuitBreaker

func init() {
	cb = gobreaker.NewCircuitBreaker(gobreaker.Settings{
		Name:        "user-svc",
		MaxRequests: 3,                // half‑open 상태에서 허용되는 요청 수
		Interval:    30 * time.Second, // 통계 초기화 간격
		Timeout:     10 * time.Second, // open 상태 유지 시간
		ReadyToTrip: func(c gobreaker.Counts) bool {
			// 요청이 5회 이상이고 실패 비율이 50 % 초과이면 트립
			return c.Requests >= 5 && float64(c.TotalFailures)/float64(c.Requests) > 0.5
		},
	})
}

// FetchUser는 회로 차단기를 적용해 사용자를 조회합니다.
func FetchUser(id string) (string, error) {
	result, err := cb.Execute(func() (interface{}, error) {
		resp, err := http.Get("http://user-service/" + id)
		if err != nil {
			return nil, err
		}
		defer resp.Body.Close()
		if resp.StatusCode >= 500 {
			return nil, fmt.Errorf("upstream error: %d", resp.StatusCode)
		}
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return nil, err
		}
		return string(body), nil
	})
	if err != nil {
		return "", err // 호출자는 대체 로직을 선택할 수 있습니다
	}
	return result.(string), nil
}

이 스니펫은 30초 윈도우 동안 실패를 추적합니다. 5회 요청 중 50 % 이상이 실패하면 10초 동안 회로가 열립니다. 그 기간 동안 Execute는 즉시 반환되어 자원을 절약합니다. half‑open 상태에서는 3개의 요청을 허용해 복구 여부를 확인합니다.

고급 사용법

  1. Bulkheads – 브레이커당 동시 호출 수를 제한하여 문제가 있는 서비스가 전체 스레드 풀을 고갈시키지 않도록 합니다.
  2. Selective Retries – 일시적인 오류(예: 429 또는 503) 에만 재시도를 적용하고, 브레이커의 실패 카운트에 포함되지 않도록 하여 조기 트립을 방지합니다.
  3. Per‑dependency Settings – 중요한 서비스는 낮은 영향의 엔드포인트보다 더 많은 실패를 허용할 수 있으므로, 임계값을 그에 맞게 조정합니다.

모니터링 및 가시성

  • 상태가 변경될 때마다 로그와 메트릭을 발생시킵니다.
  • 추적:
    • 트립 비율
    • 작업 지연 시간
    • 폴백 호출

데이터를 사용하여 임계값을 조정하거나 업스트림 상태를 조사합니다. 브레이커가 너무 자주 트립한다면, 임계값이 너무 낮거나 업스트림 서비스가 실제로 장애가 발생한 것입니다.

Common Pitfalls

  • 반열린 상태의 실패 무시 – 이를 정상적인 실패로 처리하십시오; 그렇지 않으면 차단기가 무한히 열려 있을 수 있습니다.
  • 재시도와 차단기의 실패 카운트 결합 – 재시도로 인한 오류에서 차단기가 열릴 수 있습니다.
  • 타임아웃을 너무 짧게 설정 – 정상적인 지연 스파이크를 실패로 분류할 수 있습니다.
  • 통계 리셋 안 함 – 적절한 Interval이 없으면 오래된 실패가 차단기를 필요 이상으로 오래 열어두게 합니다.
0 조회
Back to Blog

관련 글

더 보기 »