0Pricing
PHP Academy · 강의

이벤트 기반 워크플로 구축

이벤트와 멱등성을 통해 서비스를 조정합니다.

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

이벤트 기반 작업 흐름

단일 메시지는 간단합니다. 여러 서비스에 걸쳐 주문 접수 → 재고 예약 → 카드 결제 → 배송 → 알림을 수행하는 작업 흐름에서는 이벤트 기반 설계가 진가를 발휘하지만, 순진하게 구현하면 문제가 발생합니다.

이 단원에서는 이벤트 연쇄 방식과 중앙 조정 방식의 차이, 원자적 발행을 위한 아웃박스 패턴, 분산 롤백을 위한 사가, 그리고 이 모든 것을 지탱하는 멱등성을 다룹니다.

이벤트 연쇄 방식과 중앙 조정 방식 비교

여러 단계의 흐름을 조정하는 방법은 두 가지입니다.

  • 이벤트 연쇄 방식 — 각 서비스가 이벤트에 반응하고 자체 이벤트를 내보내며, 중앙 두뇌는 없습니다. 결합도는 낮지만 전체 흐름이 명시적으로 드러나지 않아 추적하기 어렵습니다.
  • 중앙 조정 방식 — 중앙 조정자가 각 서비스에 다음 작업을 지시합니다. 명시적이고 관찰하기 쉽지만, 조정자가 결합 지점이 됩니다.

경험칙은 다음과 같습니다. 단순한 다중 분배에는 이벤트 연쇄 방식을 사용하고, 순서가 정해진 단계가 많으며 상태를 명확히 보여 줘야 할 때는 중앙 조정 방식을 사용합니다.

이벤트와 명령 비교

메시지 이름을 신중하게 정하십시오.

  • 이벤트는 과거에 발생한 사실을 나타냅니다. 예를 들어 OrderPlaced입니다. 발행자는 누가 수신하는지 신경 쓰지 않습니다.
  • 명령은 특정 처리기에게 앞으로 수행할 작업을 요청합니다. 예를 들어 ChargeCard입니다.

이벤트는 이벤트 연쇄 방식을 움직이고, 명령은 중앙 조정 방식을 움직입니다. 용어를 섞으면, 겉으로는 이벤트이지만 실제로는 하나의 처리기를 은밀히 요구하는 경우처럼 숨은 결합이 발생하기 쉽습니다.

<?php
final class OrderPlaced {
    public function __construct(
        public readonly string $orderId,
        public readonly string $customerId,
        public readonly int $amountCents,
        public readonly string $occurredAt,
    ) {}
}

$e = new OrderPlaced('o-42', 'c-7', 1990, gmdate('c'));
echo json_encode($e), "\n";

이중 기록 문제

전형적인 오류는 처리기가 데이터베이스를 업데이트하는 작업과 메시지를 발행하는 작업을 서로 별개의 작업으로 수행하는 것입니다. 두 작업 사이에 프로세스가 종료되면 불일치가 발생합니다. 행은 변경되었지만 이벤트가 전송되지 않거나, 그 반대가 될 수 있습니다.

<?php
// BROKEN: not atomic. A crash between the two lines corrupts state.
function placeOrder(PDO $db, $broker, array $o): void {
    $db->prepare('INSERT INTO orders ...')->execute($o);
    // <-- crash here = row exists but no event ever published
    $broker->publish('OrderPlaced', json_encode($o));
}

트랜잭션 아웃박스

해결책은 아웃박스 패턴입니다. 데이터를 변경하는 동일한 데이터베이스 트랜잭션 안에서 이벤트를 outbox 테이블에 삽입합니다. 별도의 릴레이 프로세스가 발행되지 않은 행을 읽어 브로커로 전송합니다. 하나의 원자적 커밋으로 처리하므로 이중 기록이 발생하지 않습니다.

<?php
function placeOrder(PDO $db, array $o): void {
    $db->beginTransaction();
    $db->prepare('INSERT INTO orders (id, total) VALUES (?, ?)')
       ->execute([$o['id'], $o['total']]);
    // Same transaction -> atomic with the business write
    $db->prepare('INSERT INTO outbox (id, type, payload) VALUES (?, ?, ?)')
       ->execute([bin2hex(random_bytes(8)), 'OrderPlaced', json_encode($o)]);
    $db->commit();
}

릴레이(발행자)

작업자가 아웃박스를 조회하거나 CDC를 통해 데이터베이스 변경 로그를 따라가며 각 행을 발행한 다음 전송 완료로 표시합니다. 릴레이는 발행한 후 표시하기 전에 충돌할 수 있으므로 자체적으로 최소 한 번 전달 방식을 사용합니다. 소비자가 멱등적으로 동작한다면 이는 문제가 되지 않습니다.

<?php
function relayOutbox(PDO $db, $broker): void {
    $rows = $db->query(
        'SELECT id, type, payload FROM outbox
         WHERE published_at IS NULL ORDER BY created_at LIMIT 100'
    )->fetchAll(PDO::FETCH_ASSOC);

    foreach ($rows as $r) {
        $broker->publish($r['type'], $r['payload'], messageId: $r['id']);
        $db->prepare('UPDATE outbox SET published_at = now() WHERE id = ?')
           ->execute([$r['id']]);
    }
}

멱등 소비자(다시 보기)

릴레이와 브로커 모두 최소 한 번 전달 방식을 사용하므로 하위 처리기는 중복 메시지를 받게 됩니다. 각 소비자는 처리한 메시지 식별자를 기록하고 중복 메시지를 즉시 건너뜁니다. 이는 앞서 배운 중복 제거 관문을 서비스별로 적용한 것입니다.

<?php
function onOrderPlaced(PDO $db, string $messageId, array $data): void {
    $db->beginTransaction();
    try {
        $db->prepare('INSERT INTO inbox (message_id) VALUES (?)')
           ->execute([$messageId]); // unique index = dedup
    } catch (PDOException $e) {
        $db->rollBack();
        return; // already handled this message
    }
    reserveStock($data['orderId']);
    $db->commit();
}
function reserveStock(string $id): void {}

사가: 분산 롤백

서비스 전체에 걸쳐 하나의 ACID 트랜잭션을 열 수는 없습니다. 사가는 장시간 실행되는 흐름을 로컬 트랜잭션의 연속으로 모델링하며, 각 트랜잭션에는 해당 작업을 되돌리는 보상 동작이 있습니다. 3단계가 실패하면 2단계와 1단계의 보상 동작을 역순으로 실행합니다.

예를 들어 재고를 예약한 후 결제가 실패하면 보상을 위해 ReleaseStock을 발행합니다. 자동 롤백은 없으므로 모든 단계에 대한 되돌리기 동작을 직접 설계해야 합니다.

중앙 조정 사가

중앙 조정자가 사가를 진행합니다. 성공하면 다음 단계로 넘어가고, 실패하면 보상 동작을 전달합니다. 충돌이 발생해도 재개할 수 있도록 사가의 상태를 저장하십시오.

<?php
function handleStepResult(array $saga, string $step, bool $ok, $bus): array {
    if ($ok) {
        $next = ['reserveStock' => 'chargeCard', 'chargeCard' => 'ship'][$step] ?? null;
        if ($next) { $bus->send($next, $saga['orderId']); $saga['state'] = $next; }
        else { $saga['state'] = 'completed'; }
    } else {
        // Run compensations in reverse for whatever already succeeded
        foreach (array_reverse($saga['done']) as $s) {
            $bus->send('compensate.' . $s, $saga['orderId']);
        }
        $saga['state'] = 'compensating';
    }
    return $saga;
}

긴 작업 흐름의 시간 초과

사가의 한 단계가 응답을 전혀 보내지 않을 수 있습니다. 결제 서비스가 중단되거나 사람의 승인이 이루어지지 않는 경우입니다. 시간 초과가 없으면 예약을 계속 보유한 채 사가가 영원히 멈춥니다. 단계마다 마감 시각을 저장하고, 스케줄러가 기한을 넘긴 사가를 검색하여 실패 및 보상 경로를 실행하도록 하십시오.

<?php
function reapTimedOutSagas(PDO $db, $bus): void {
    $rows = $db->query(
        "SELECT order_id, state FROM sagas
         WHERE state NOT IN ('completed','compensating')
           AND deadline_at < now()"
    )->fetchAll(PDO::FETCH_ASSOC);

    foreach ($rows as $r) {
        echo "Saga {$r['order_id']} timed out at step {$r['state']}\n";
        $bus->send('saga.compensate', $r['order_id']); // trigger rollback
    }
}

버전 관리 및 관측성

워크플로는 수년간 유지되므로 이벤트는 안전하게 진화해야 합니다:

  • 모든 이벤트에 version(또는 스키마)을 추가하고, 소비자는 알 수 없는 새 필드를 허용하며 필드가 항상 존재한다고 가정하지 않아야 합니다.
  • 추가 방식의 변경을 우선하고, 기존 필드의 의미를 다른 용도로 재사용하지 마십시오.
  • 모든 메시지에 상관관계 식별자를 전달하여 로그와 추적 정보에서 하나의 비즈니스 트랜잭션을 모든 서비스에 걸쳐 추적할 수 있게 하십시오.

상관관계 식별자가 없으면 다섯 개 서비스에 걸친 코레오그래피 흐름을 디버깅하기가 거의 불가능합니다.

<?php
$envelope = [
    'type'          => 'OrderPlaced',
    'version'       => 2,
    'correlationId' => $incoming['correlationId'] ?? bin2hex(random_bytes(8)),
    'occurredAt'    => gmdate('c'),
    'data'          => ['orderId' => 'o-42'],
];
echo json_encode($envelope, JSON_PRETTY_PRINT), "\n";

빠른 확인

이중 쓰기 문제를 피하는 방법입니다.

복습

이제 신뢰할 수 있는 이벤트 기반 워크플로를 설계할 수 있습니다:

  • 흐름의 복잡도에 따라 코레오그래피(이벤트) 또는 오케스트레이션(명령)을 선택합니다.
  • 트랜잭션 아웃박스와 릴레이를 사용하여 이중 쓰기 문제를 해결합니다.
  • 수신함/중복 제거 키를 통해 모든 소비자를 멱등적으로 만듭니다.
  • 분산 롤백에는 보상 작업을 포함하는 사가를 사용합니다.
  • 이벤트는 추가 방식으로 버전 관리하고, 추적 가능성을 위해 상관관계 식별자를 모든 흐름에 전달합니다.

이러한 패턴은 느슨하게 연결된 메시지를 신뢰할 수 있고 관측 가능한 비즈니스 프로세스로 바꿉니다.

자주 묻는 질문

“이벤트 기반 워크플로 구축” 강의는 무료인가요?

네 — “이벤트 기반 워크플로 구축” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 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. PHP에서 RabbitMQ 다루기
  3. PHP용 Apache Kafka
  4. 이벤트 기반 워크플로 구축
← PHP Academy(으)로 돌아가기