PHP Academy · 강의

복원력: 회로 차단기와 재시도

부분적인 장애 상황에서도 시스템을 안정적으로 유지합니다.

레슨 4/413개 단계

복원력: 회로 차단기와 재시도은(는) CoddyKit의 무료 PHP Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 PHP Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

복원력 패턴

분산 시스템에서는 부분 장애가 정상적인 상태입니다. 어떤 의존성은 항상 느리거나, 재시작 중이거나, 과부하 상태입니다. 복원력이란 문제가 있는 하나의 의존성 때문에 서비스가 중단되고, 이어서 전체 시스템까지 함께 중단되지 않도록 각 서비스를 설계하는 것입니다.

이 레슨에서는 시간 초과, 백오프를 적용한 재시도, 서킷 브레이커, 벌크헤드, 점진적 성능 저하라는 핵심 도구를 모두 PHP로 다룹니다.

먼저 시간 초과 설정

복원력 설정 중 가장 중요한 하나는 시간 초과입니다. 시간 초과가 없으면 느린 다운스트림을 기다리느라 PHP-FPM 작업 프로세스가 묶이고, 요청이 쌓이며, 작업 프로세스가 부족해집니다. 결국 다른 시스템이 느렸다는 이유로 서비스가 중단됩니다. 이것이 전형적인 연쇄 장애입니다.

모든 외부 호출에 연결 시간 초과와 전체 요청 시간 초과를 모두 설정해야 합니다. 항상 설정하십시오.

<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;

$http = new Client([
    'connect_timeout' => 0.5, // fail fast if we can't even connect
    'timeout'         => 2.0, // hard cap on the whole call
]);
// A hung dependency now fails in 2s instead of holding a worker forever.

신중한 재시도

일시적인 장애(순간적인 오류, 짧은 네트워크 끊김, 503)는 두 번째 시도에서 성공하는 경우가 많습니다. 하지만 재시도는 위험합니다. 너무 성급하게 재시도하면 이미 어려움을 겪고 있는 서비스의 부하를 증폭시킵니다. 규칙은 다음과 같습니다.

  • 멱등 작업과 재시도 가능한 오류(시간 초과, 5xx, 429)에 대해서만 재시도합니다.
  • 시도 횟수에 상한을 둡니다.
  • 4xx 클라이언트 오류는 절대 재시도하지 않습니다. 다시 실패할 뿐입니다.

지수 백오프와 무작위 변동

많은 클라이언트가 일정한 간격으로 재시도하면 타이밍이 동기화되어, 복구 중인 서비스에 파도처럼 부하를 가하는 동시 폭주가 발생합니다. 이를 해결하려면 지수 백오프(시도할 때마다 지연 시간을 두 배로 늘림)에 무작위 변동(무작위화)을 더해 부하를 분산해야 합니다.

<?php
function backoffDelay(int $attempt, float $base = 0.1, float $cap = 5.0): float {
    $exp = min($cap, $base * (2 ** $attempt));   // 0.1, 0.2, 0.4, ...
    return mt_rand(0, (int)($exp * 1000)) / 1000; // full jitter: 0..exp
}
for ($i = 0; $i < 5; $i++) {
    printf("attempt %d -> wait %.3fs\n", $i, backoffDelay($i));
}

재시도 반복문

이제 각 요소를 결합해 보겠습니다. 시도 횟수를 제한하고, 재시도 가능한 오류에 대해서만 재시도하며, 시도 사이에는 무작위 변동이 적용된 백오프만큼 대기하고, 모든 시도가 실패하면 마지막 실패를 전달합니다.

<?php
function withRetry(callable $op, int $maxAttempts = 4): mixed {
    $attempt = 0;
    while (true) {
        try {
            return $op();
        } catch (\Throwable $e) {
            $attempt++;
            if ($attempt >= $maxAttempts || !isRetryable($e)) {
                throw $e; // give up
            }
            usleep((int)(backoffDelay($attempt) * 1_000_000));
        }
    }
}
function isRetryable(\Throwable $e): bool { return $e->getCode() === 0 || $e->getCode() >= 500; }
function backoffDelay(int $a): float { return min(5.0, 0.1 * (2 ** $a)) * (mt_rand(0, 100) / 100); }

서킷 브레이커

재시도는 순간적인 장애에는 도움이 되지만, 의존성이 완전히 중단된 경우 모든 요청을 재시도하는 것은 시간과 자원만 낭비합니다. 서킷 브레이커는 실패를 추적하다가 실패가 임계값을 넘으면 열립니다. 호출을 즉시 차단하고, 중단된 서비스를 기다리는 대신 바로 실패를 반환합니다.

서킷 브레이커에는 세 가지 상태가 있습니다. 닫힘(호출이 통과하고 실패를 집계함), 열림(호출을 즉시 거부함), 반개방(복구 여부를 확인하기 위해 몇 번의 시험 호출을 수행함)입니다.

서킷 브레이커 상태 머신

상태 전환은 다음과 같습니다. 실패가 임계값을 넘으면 닫힘에서 열림으로 전환됩니다. 냉각 기간이 지나면 열림에서 반개방으로 전환됩니다. 시험 호출이 성공하면 반개방에서 닫힘으로, 실패하면 다시 열림으로 전환됩니다. 각 요청이 새로운 프로세스에서 실행되므로 상태는 PHP 프로세스 간에 공유되어야 합니다(Redis/APCu).

<?php
final class CircuitBreaker {
    public function __construct(
        private int $threshold = 5,
        private int $coolDown = 30, // seconds
    ) {}

    public function call(callable $op, array &$state): mixed {
        if ($state['status'] === 'open') {
            if (time() - $state['openedAt'] < $this->coolDown) {
                throw new \RuntimeException('Circuit open - failing fast');
            }
            $state['status'] = 'half-open'; // time to probe
        }
        try {
            $result = $op();
            $state = ['status' => 'closed', 'failures' => 0]; // recovered
            return $result;
        } catch (\Throwable $e) {
            $state['failures'] = ($state['failures'] ?? 0) + 1;
            if ($state['failures'] >= $this->threshold || $state['status'] === 'half-open') {
                $state['status'] = 'open';
                $state['openedAt'] = time();
            }
            throw $e;
        }
    }
}

벌크헤드

선박의 격실에서 이름을 따온 벌크헤드 패턴은 자원을 격리하여 한 영역의 장애가 배 전체를 침몰시키지 못하게 합니다. 모든 작업 프로세스가 느린 Reports 서비스에 호출할 수 있다면, Reports 장애가 모든 작업 프로세스를 차지하여 Checkout을 굶길 수 있습니다.

자원을 분할하십시오. 의존성마다 연결 풀과 작업 프로세스 풀 또는 큐를 분리하거나, 다운스트림별 동시성 제한을 두어야 합니다. 그러면 하나의 실패한 의존성이 자신에게 할당된 부분만 소진하게 됩니다.

점진적 성능 저하와 대체 경로

중요하지 않은 의존성을 사용할 수 없을 때는 오류를 반환하는 대신 기능을 낮춰 제공하십시오. 캐시된 값이나 기본값을 제공하거나, 기능을 축소한 경험을 제공할 수 있습니다. 서킷 브레이커의 열림 상태는 대체 경로를 실행하는 자연스러운 계기입니다.

<?php
function getRecommendations(callable $remoteCall, Redis $cache, string $userId): array {
    try {
        $recs = $remoteCall($userId);
        $cache->setex("recs:$userId", 3600, json_encode($recs));
        return $recs;
    } catch (\Throwable $e) {
        // Fallback 1: last-known-good from cache
        if ($cached = $cache->get("recs:$userId")) {
            return json_decode($cached, true);
        }
        // Fallback 2: generic popular items - never block the page
        return ['popular-1', 'popular-2'];
    }
}

안전한 재시도를 위한 멱등성 키

재시도는 멱등 작업에서만 안전합니다. "카드 결제"처럼 멱등적이지 않은 작업에는 서버가 중복을 제거할 수 있도록 멱등성 키를 함께 보내십시오. 첫 번째 시도가 이미 성공했지만 응답이 유실된 후 재시도가 도착하더라도, 서버는 카드를 두 번 결제하는 대신 원래 결과를 반환합니다.

<?php
function chargeWithRetry(callable $http, string $orderId, int $cents): array {
    // Same key across all retries of THIS logical charge
    $key = 'charge-' . $orderId;
    return withRetry(fn() => $http('POST', '/charges', [
        'headers' => ['Idempotency-Key' => $key],
        'json'    => ['order' => $orderId, 'amount' => $cents],
    ]));
}
function withRetry(callable $op) { return $op(); } // see earlier scene

함께 적용하기

패턴은 서로 조합되며, 순서가 중요합니다. 일반적으로 견고한 외부 호출은 다음과 같이 중첩됩니다.

  • 각 개별 시도에 적용하는 시간 초과(가장 안쪽).
  • 일시적인 장애를 처리하기 위해 시간 초과가 적용된 호출을 감싸는 백오프를 적용한 재시도.
  • 지속적인 장애가 발생하면 빠르게 작동하도록 재시도를 감싸는 서킷 브레이커.
  • 이 의존성이 사용할 수 있는 용량을 제한하는 벌크헤드.
  • 위로 전파되는 모든 오류를 처리하는 가장 바깥쪽의 대체 경로.

Resilience4PHP 방식의 래퍼 같은 라이브러리도 있지만, 특정 패키지보다 계층 구조를 이해하는 것이 더 중요합니다.

빠른 확인

알맞은 패턴 선택하기

요약

부분 장애를 견디기:

  • 모든 호출에 시간 초과를 설정하면 작업 프로세스 고갈과 연쇄 장애를 방지할 수 있습니다.
  • 재시도는 멱등 작업과 재시도 가능한 오류에만 적용하고, 지수 백오프와 무작위 변동을 사용합니다.
  • 의존성이 중단되면 서킷 브레이커가 빠르게 실패합니다(닫힘 → 열림 → 반개방).
  • 벌크헤드는 자원을 격리하여 하나의 장애가 모든 자원을 고갈시키지 못하게 합니다.
  • 점진적 성능 저하와 대체 경로는 핵심 흐름을 계속 작동하게 합니다.

이 패턴들을 계층적으로 결합하면 피할 수 없는 장애를 통제되고 복구 가능한 사건으로 바꿀 수 있습니다.

무료로 시작

AI 튜터와 함께 PHP을(를) 배우세요 — 무료

브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.

코스
49
레슨
195

자주 묻는 질문

“복원력: 회로 차단기와 재시도” 강의는 무료인가요?

네 — “복원력: 회로 차단기와 재시도” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 PHP Academy 강의 전체를 잠금 해제할 수 있습니다. PHP Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“복원력: 회로 차단기와 재시도”에서 뭘 배우나요?

부분적인 장애 상황에서도 시스템을 안정적으로 유지합니다. 브라우저에서 직접 실행하는 실습 코드로 PHP Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

PHP Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 PHP Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“복원력: 회로 차단기와 재시도” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 PHP Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 PHP Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 모놀리스에서 마이크로서비스로
  2. 서비스 통신: REST와 gRPC
  3. API 게이트웨이와 서비스 검색
  4. 복원력: 회로 차단기와 재시도
← PHP Academy(으)로 돌아가기