Symfony Messenger 완벽 가이드: 큐, 워커, 비동기 아키텍처 면접 대비 2026

Symfony Messenger의 메시지 버스 아키텍처, 트랜스포트 설정, 워커 관리, 중복 제거 미들웨어, 재시도 전략, Symfony 7.3+의 스트리밍 AMQP 트랜스포트까지 심층 분석합니다. 2026년 기술 면접에서 자주 출제되는 핵심 개념을 코드와 함께 다룹니다.

Symfony Messenger 비동기 큐와 워커 아키텍처 다이어그램

Symfony Messenger는 PHP에서 비동기 워크로드를 처리하기 위한 핵심 컴포넌트입니다. 구조화된 메시지 버스, 전용 트랜스포트, 관리 대상 워커를 통해 복잡한 비동기 처리를 체계적으로 관리합니다. Symfony 7.3과 8.0에서는 중복 제거 미들웨어, 스트리밍 AMQP 트랜스포트, Doctrine 킵얼라이브가 추가되어 Laravel Horizon이나 Sidekiq에 필적하는 기능 완성도를 갖추게 되었습니다.

Symfony 7.3+ Messenger 핵심 변경 사항

Symfony 7.3에서는 DeduplicateMiddleware(자동 메시지 중복 제거), Doctrine 트랜스포트 킵얼라이브(장시간 실행 태스크의 재배달 방지), #[AsMessage] 속성(선언적 트랜스포트 라우팅)이 도입되었습니다.

Messenger 아키텍처: 버스, 트랜스포트, 워커

Messenger 컴포넌트는 세 가지 관심사를 분리합니다. 디스패치(버스), 전달(트랜스포트), 처리(워커)입니다. 메시지는 일반 PHP 객체이며, 핸들러는 호출 가능한 클래스입니다. 버스가 양쪽을 연결하고, 미들웨어를 통해 라우팅하며, 필요에 따라 메시지를 트랜스포트로 직렬화하여 비동기 처리를 수행합니다.

src/Message/InvoiceGenerated.phpphp
namespace App\Message;

final readonly class InvoiceGenerated
{
    public function __construct(
        public int $orderId,
        public string $customerEmail,
    ) {}
}

이 메시지에는 스칼라 데이터만 포함되어 있으며, Doctrine 엔티티는 전달하지 않습니다. 객체 대신 ID를 전달함으로써 직렬화 문제를 피하고 메시지를 경량으로 유지할 수 있습니다. 핸들러는 처리 시점에 데이터베이스에서 최신 데이터를 조회합니다.

src/MessageHandler/InvoiceGeneratedHandler.phpphp
namespace App\MessageHandler;

use App\Message\InvoiceGenerated;
use App\Service\InvoiceService;
use App\Service\MailerService;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final readonly class InvoiceGeneratedHandler
{
    public function __construct(
        private InvoiceService $invoiceService,
        private MailerService $mailerService,
    ) {}

    public function __invoke(InvoiceGenerated $message): void
    {
        $pdf = $this->invoiceService->generatePdf($message->orderId);
        $this->mailerService->sendInvoice(
            $message->customerEmail,
            $pdf,
        );
    }
}

컨트롤러나 서비스에서 메시지를 디스패치하는 방법은 매우 간단합니다.

src/Controller/OrderController.phpphp
$this->bus->dispatch(new InvoiceGenerated(
    orderId: $order->getId(),
    customerEmail: $order->getCustomer()->getEmail(),
));

동기 처리와 비동기 처리 중 어느 방식으로 실행할지는 트랜스포트 라우팅 설정에 따라 결정됩니다.

트랜스포트 설정과 큐 백엔드

Messenger는 Doctrine DBAL, Redis, Amazon SQS, Beanstalkd, AMQP(RabbitMQ), 그리고 2025년에 도입된 스트리밍 AMQP 트랜스포트를 지원합니다. 각 트랜스포트는 DSN 문자열로 정의합니다.

yaml
# config/packages/messenger.yaml
framework:
    messenger:
        failure_transport: failed

        transports:
            async_priority_high:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: high
                retry_strategy:
                    max_retries: 3
                    delay: 1000
                    multiplier: 3
                    max_delay: 60000

            async_priority_low:
                dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
                options:
                    queue_name: low
                retry_strategy:
                    max_retries: 5
                    delay: 5000
                    multiplier: 2

            failed:
                dsn: 'doctrine://default?queue_name=failed'

        routing:
            'App\Message\InvoiceGenerated': async_priority_high
            'App\Message\CleanupTempFiles': async_priority_low

우선순위별로 트랜스포트를 분리하면, 시간에 민감한 태스크(청구서 생성)가 관리 태스크(임시 파일 정리)보다 먼저 처리되는 것을 보장합니다. 각 트랜스포트에는 메시지의 중요도와 멱등성에 맞게 조정된 별도의 재시도 전략이 설정됩니다.

Doctrine vs. Redis vs. AMQP 비교

Doctrine 트랜스포트는 추가 인프라 없이 사용할 수 있지만 데이터베이스 부하가 증가합니다. Redis는 서브밀리초 지연 시간을 제공합니다. AMQP(RabbitMQ)는 고급 라우팅, 데드 레터 익스체인지, 고처리량 시나리오를 위한 스트리밍 트랜스포트를 갖추고 있습니다. 기존 인프라와 처리량 요구사항에 따라 선택하는 것이 바람직합니다.

Supervisor를 활용한 워커 관리

워커는 트랜스포트에서 메시지를 소비합니다. 프로덕션 환경에서는 messenger:consume 명령을 Supervisor나 systemd 같은 프로세스 매니저 하에서 실행합니다.

bash
# 고우선순위 메시지를 먼저 소비하고, 그다음 저우선순위 처리
php bin/console messenger:consume async_priority_high async_priority_low \
    --memory-limit=128M \
    --time-limit=3600 \
    --limit=500

세 가지 제한 플래그는 메모리 누수를 방지하고 워커가 주기적으로 재시작되도록 보장합니다. Supervisor는 프로세스 종료 후 자동으로 재시작을 수행합니다.

ini
; /etc/supervisor/conf.d/messenger-worker.conf
[program:messenger-consume]
command=php /var/www/app/bin/console messenger:consume async_priority_high async_priority_low --memory-limit=128M --time-limit=3600
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
startsecs=0
stopwaitsecs=30
stdout_logfile=/var/log/messenger-worker.log
stderr_logfile=/var/log/messenger-worker-error.log

numprocs=2로 설정하면 2개의 병렬 워커가 실행되어 처리량이 2배로 증가합니다. 서버의 CPU 코어 수와 메시지 처리 시간에 따라 조정합니다.

미들웨어 파이프라인과 다중 버스를 통한 CQRS

미들웨어는 모든 메시지 디스패치를 래핑하여 횡단 관심사를 추가합니다. 내장 미들웨어 스택은 유효성 검증, Doctrine 트랜잭션, 라우팅을 처리합니다.

yaml
# config/packages/messenger.yaml
framework:
    messenger:
        default_bus: command.bus
        buses:
            command.bus:
                middleware:
                    - validation
                    - doctrine_transaction
            query.bus:
                middleware:
                    - validation
            event.bus:
                default_middleware:
                    allow_no_handlers: true
                middleware:
                    - validation

이 설정은 CQRS(명령 쿼리 책임 분리)를 구현합니다. 커맨드는 Doctrine 트랜잭션 내에서 상태를 변경합니다. 쿼리는 읽기 전용입니다. 이벤트는 핸들러가 없어도 허용되어, 리스너를 독립적으로 추가할 수 있는 Pub/Sub 패턴을 구현합니다.

Symfony 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

Symfony 7.3의 중복 제거 미들웨어

중복 메시지는 리소스를 낭비하고, 고객에게 이중 청구를 하는 등의 부작용을 일으킬 수 있습니다. Symfony 7.3에서는 큐에 이미 존재하는 동일한 메시지를 자동으로 건너뛰는 DeduplicateMiddleware가 도입되었습니다.

src/Message/SendWelcomeEmail.phpphp
namespace App\Message;

use Symfony\Component\Messenger\Stamp\DeduplicateStamp;

final readonly class SendWelcomeEmail
{
    public function __construct(
        public int $userId,
    ) {}
}
php
// 중복 제거를 적용하여 디스패치
use Symfony\Component\Messenger\Stamp\DeduplicateStamp;

$this->bus->dispatch(
    new SendWelcomeEmail(userId: 42),
    [new DeduplicateStamp(id: 'welcome-email-42')],
);

DeduplicateStamp는 잠금 리소스 식별자를 받습니다. 동일한 ID의 메시지가 이미 대기 중이면 새로운 디스패치는 조용히 폐기됩니다. 이 기능을 사용하려면 Lock 컴포넌트와 직렬화 가능한 스토어(Redis, Memcached 또는 데이터베이스)가 필요합니다.

재시도 전략과 실패 트랜스포트

핸들러가 예외를 발생시키면, Messenger는 트랜스포트의 재시도 전략에 따라 메시지를 재시도합니다. 재시도 횟수를 모두 소진하면 메시지는 실패 트랜스포트로 이동합니다.

src/MessageHandler/PaymentHandler.phpphp
namespace App\MessageHandler;

use App\Message\ProcessPayment;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException;

#[AsMessageHandler]
final class PaymentHandler
{
    public function __invoke(ProcessPayment $message): void
    {
        try {
            $this->gateway->charge($message->amount, $message->token);
        } catch (GatewayTimeoutException $e) {
            // 복구 가능: 백오프와 함께 재시도
            throw new RecoverableMessageHandlingException(
                'Payment gateway timeout, retrying',
                previous: $e,
            );
        } catch (InvalidCardException $e) {
            // 복구 불가능: 즉시 실패 트랜스포트로 전송
            throw new UnrecoverableMessageHandlingException(
                'Invalid card, no retry',
                previous: $e,
            );
        }
    }
}

RecoverableMessageHandlingException은 재시도 전략을 트리거합니다. UnrecoverableMessageHandlingException은 재시도를 완전히 우회하고 메시지를 즉시 실패 트랜스포트로 보냅니다. 이러한 구분을 통해 영구적으로 실패하는 메시지에 대한 불필요한 재시도를 방지할 수 있습니다.

bash
# 실패한 메시지 확인 및 관리
php bin/console messenger:failed:show
php bin/console messenger:failed:show 20 --transport=failed

# 특정 메시지 재시도
php bin/console messenger:failed:retry 20 30

# 클래스로 필터링하여 삭제 (Symfony 7.3+)
php bin/console messenger:failed:remove --class-filter="App\Message\CleanupTempFiles"
멱등한 핸들러 설계

재시도가 발생하면 동일한 메시지에 대해 핸들러가 여러 번 실행될 수 있습니다. 모든 핸들러를 멱등하게 설계하는 것이 중요합니다. 작업을 반복하기 전에 해당 작업이 이미 완료되었는지 확인해야 합니다. 데이터베이스의 유니크 제약 조건이나 플래그 확인을 사용하여 이중 처리를 방지합니다.

Doctrine 킵얼라이브와 장시간 실행 메시지

장시간 실행되는 메시지 핸들러는 트랜스포트의 가시성 타임아웃이 만료될 때 메시지가 재배달되는 위험이 있습니다. Symfony 7.2에서는 Redis, SQS, Beanstalkd용 킵얼라이브가 도입되었으며, Symfony 7.3에서는 Doctrine 트랜스포트로 확장되었습니다.

bash
# 장시간 처리 중 재배달을 방지하기 위해 킵얼라이브 활성화
php bin/console messenger:consume async --keepalive

--keepalive 플래그는 Doctrine 트랜스포트 테이블의 delivered_at 타임스탬프를 주기적으로 업데이트하여 워커가 아직 메시지를 처리 중임을 알립니다. 이 플래그 없이 트랜스포트 타임아웃(기본 5분)을 초과하여 처리되는 메시지는 다른 워커에 의해 수집되어 중복 처리가 발생합니다.

#[AsMessage] 속성을 활용한 선언적 라우팅

Symfony 7.2에서는 트랜스포트 라우팅을 YAML에서 메시지 클래스 자체로 옮기는 #[AsMessage] 속성이 도입되었습니다.

src/Message/GenerateReport.phpphp
namespace App\Message;

use Symfony\Component\Messenger\Attribute\AsMessage;

#[AsMessage(transport: 'async_priority_low')]
final readonly class GenerateReport
{
    public function __construct(
        public int $reportId,
        public string $format = 'pdf',
    ) {}
}

이 속성을 사용하면 해당 메시지에 대한 messenger.yamlrouting 섹션이 불필요해집니다. 트랜스포트가 소스 코드에서 선언되므로 코드베이스가 자기 문서화됩니다. YAML 라우팅과 속성 방식은 공존할 수 있으며, 속성이 우선 적용됩니다.

고처리량 큐를 위한 스트리밍 AMQP 트랜스포트

기존 AMQP 트랜스포트는 폴링(get())으로 메시지를 가져오기 때문에 RabbitMQ에 불필요한 부하를 발생시켰습니다. 2025년에 릴리스된 스트리밍 AMQP 트랜스포트는 푸시 모델(consume())로 전환하여 지연 시간과 리소스 사용량을 줄입니다.

yaml
# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            streaming:
                dsn: 'amqp-lib://guest:guest@localhost:5672/%2f/messages'
                options:
                    exchange:
                        name: app_events
                        type: topic
                    queues:
                        order_events:
                            binding_keys: ['order.*']

기본 AMQP 트랜스포트와의 주요 차이점은 다음과 같습니다. C 확장이 필요하지 않고(php-amqplib 사용), 장기 TCP 연결을 통한 스트리밍 전달이 가능하며, 바인딩 키 라우팅을 통한 토픽 익스체인지를 기본 지원합니다. 이 트랜스포트는 최소한의 CPU 오버헤드로 초당 수천 건의 메시지를 처리할 수 있습니다.

결론

  • 메시지에는 Doctrine 엔티티가 아닌 스칼라 ID를 전달하고, 핸들러에서 최신 데이터를 조회하여 직렬화 문제와 오래된 상태를 방지합니다
  • 우선순위와 중요도에 따라 트랜스포트를 분리하고, 각 트랜스포트에 독자적인 재시도 전략과 전용 워커 풀을 설정합니다
  • RecoverableMessageHandlingExceptionUnrecoverableMessageHandlingException을 사용하여 재시도 동작을 명시적으로 제어합니다
  • Doctrine 트랜스포트 워커에서 --keepalive를 활성화하여 장시간 실행 메시지의 재배달을 방지합니다
  • 중복 처리가 부작용을 유발하는 메시지(결제, 이메일, 알림)에는 DeduplicateStamp를 적용합니다
  • 다중 버스로 CQRS를 구현합니다. 커맨드 버스는 Doctrine 트랜잭션을 포함한 변경 작업, 쿼리 버스는 조회, 이벤트 버스는 Pub/Sub에 활용합니다
  • 프로덕션 환경에서는 Supervisor 또는 systemd를 사용하고, --memory-limit, --time-limit, --limit 플래그로 워커 수명 주기를 관리합니다
  • 서브밀리초 지연 시간이 필요한 고처리량 시나리오에서는 스트리밍 AMQP 트랜스포트 도입을 검토합니다

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

태그

#symfony
#messenger
#php
#async
#message queue
#workers

공유

관련 기사