Laravel 큐와 잡: 비동기 아키텍처와 면접 대비 2026

Laravel 큐 시스템의 내부 동작, 잡 배치 처리, 체이닝, 재시도 전략을 상세히 분석합니다. 2026년 기술 면접에서 출제되는 큐 아키텍처 핵심 개념을 체계적으로 정리합니다.

Laravel Queues and Jobs Architecture

Laravel 큐는 이메일 발송, 파일 업로드 처리, 보고서 생성 등 시간이 오래 걸리는 작업을 백그라운드 워커에 위임하기 위한 통합 API를 제공합니다. 사용자를 기다리게 하는 대신, 애플리케이션은 잡을 큐에 푸시하고 즉시 다음 처리로 넘어갑니다. 이 메커니즘은 확장 가능한 Laravel 애플리케이션의 핵심 기반입니다.

큐 아키텍처 개요

Laravel은 드라이버에 독립적인 단일 API를 통해 여러 큐 백엔드(Redis, Amazon SQS, 데이터베이스, Beanstalkd)를 지원합니다. 잡은 ShouldQueue 인터페이스를 구현한 직렬화 가능한 PHP 클래스입니다. 워커가 큐에서 잡을 가져와 역직렬화한 후 handle() 메서드를 실행합니다. 실패한 잡은 전용 failed_jobs 테이블에 저장되어 재시도 또는 조사가 가능합니다.

Laravel 잡 디스패치의 내부 동작 원리

잡 클래스에서 dispatch()가 호출되면, Laravel은 잡 인스턴스(공개 프로퍼티 포함)를 직렬화하고 설정된 큐 연결에 페이로드를 푸시합니다. 직렬화된 페이로드에는 정규화된 클래스명, 직렬화된 프로퍼티, 대상 큐 이름, 허용된 시도 횟수 및 타임아웃 등의 메타데이터가 포함됩니다.

큐 워커 프로세스(php artisan queue:work)는 큐 백엔드에서 새로운 잡을 폴링하는 장시간 실행 데몬으로 동작합니다. 페이로드를 수신하면 워커는 잡을 역직렬화하고, 서비스 컨테이너를 통해 의존성을 해결한 후 handle()을 호출합니다.

App/Jobs/ProcessInvoice.phpphp
namespace App\Jobs;

use App\Models\Order;
use App\Services\PdfGenerator;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class ProcessInvoice implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $backoff = 60;
    public int $timeout = 120;

    public function __construct(
        public readonly Order $order
    ) {}

    public function handle(PdfGenerator $pdf): void
    {
        // Generate PDF invoice for the order
        $invoice = $pdf->generate($this->order);

        // Store the generated file
        $this->order->update([
            'invoice_path' => $invoice->path(),
            'invoiced_at' => now(),
        ]);
    }

    public function failed(\Throwable $e): void
    {
        // Notify the ops team when invoice generation fails
        logger()->error('Invoice generation failed', [
            'order_id' => $this->order->id,
            'error' => $e->getMessage(),
        ]);
    }
}

SerializesModels 트레이트는 Eloquent 모델 전체가 아닌 모델의 기본 키와 클래스명만 저장합니다. 워커가 잡을 처리할 때 데이터베이스에서 최신 모델을 가져옵니다. 이를 통해 오래된 데이터 사용을 방지하고 페이로드 크기를 작게 유지합니다.

병렬 워크로드를 위한 잡 배치 처리

잡 배치 처리는 여러 잡을 단일 배치로 그룹화하고, 전체 진행 상황을 추적하며, 모든 잡이 완료되면 콜백을 실행합니다. 이 패턴은 데이터 임포트, 대량 알림, 보고서 생성 등 최종 단계 실행 전에 여러 독립적인 작업 단위가 완료되어야 하는 상황에 적합합니다.

App/Http/Controllers/ImportController.phpphp
use App\Jobs\ImportRow;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

public function import(Request $request)
{
    $rows = $this->parseCSV($request->file('data'));

    // Create a batch of import jobs, one per CSV row
    $batch = Bus::batch(
        collect($rows)->map(fn ($row) => new ImportRow($row))
    )
    ->then(function (Batch $batch) {
        // All jobs completed successfully
        Notification::send(
            auth()->user(),
            new ImportComplete($batch->totalJobs)
        );
    })
    ->catch(function (Batch $batch, \Throwable $e) {
        // First failure in the batch
        logger()->warning('Batch import partial failure', [
            'batch_id' => $batch->id,
            'failed' => $batch->failedJobs,
        ]);
    })
    ->finally(function (Batch $batch) {
        // Runs after all jobs finish (success or failure)
        Cache::forget("import_lock_{$batch->id}");
    })
    ->allowFailures()
    ->dispatch();

    return response()->json(['batch_id' => $batch->id]);
}

Laravel 12에서는 배치 페이로드에 큐 대기 시간과 워커 식별 정보를 포함하는 메타데이터가 추가되었습니다. allowFailures() 메서드는 단일 잡 실패가 전체 배치를 취소하는 것을 방지합니다. 부분적 성공이 허용되는 대규모 임포트에서 매우 중요한 기능입니다.

순차 워크플로우를 위한 잡 체이닝

배치 처리가 병렬 워크로드를 담당하는 반면, 체이닝은 순차적 실행을 보장합니다. 체인 내의 각 잡은 이전 잡이 성공한 후에만 실행됩니다. 어떤 잡이라도 실패하면 나머지 체인은 중단되고 catch 콜백이 실행됩니다.

App/Services/OrderWorkflow.phpphp
use App\Jobs\ValidatePayment;
use App\Jobs\ReserveInventory;
use App\Jobs\SendConfirmation;
use App\Jobs\GenerateShippingLabel;
use Illuminate\Support\Facades\Bus;

public function processOrder(Order $order): void
{
    // Each job runs only after the previous one succeeds
    Bus::chain([
        new ValidatePayment($order),
        new ReserveInventory($order),
        new GenerateShippingLabel($order),
        new SendConfirmation($order),
    ])
    ->onQueue('orders')
    ->catch(function (\Throwable $e) use ($order) {
        // Roll back the order if any step fails
        $order->update(['status' => 'failed']);
        logger()->error('Order chain failed', [
            'order_id' => $order->id,
            'step' => $e->getMessage(),
        ]);
    })
    ->dispatch();
}

체이닝은 단계의 순서가 중요한 도메인 워크플로우에 적합합니다. 결제 검증은 재고 예약 전에 완료되어야 하며, 배송 라벨은 확인된 재고에 의존합니다.

Laravel 면접 준비가 되셨나요?

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

횡단 관심사를 위한 큐 미들웨어

큐 미들웨어는 잡 실행을 레이트 제한, 중복 제거, 서킷 브레이커 등 재사용 가능한 로직으로 래핑합니다. 이러한 관심사를 모든 잡에 내장하는 대신, 미들웨어를 통해 잡이 비즈니스 로직에 집중할 수 있도록 합니다.

App/Jobs/Middleware/RateLimitedJob.phpphp
namespace App\Jobs\Middleware;

use Closure;
use Illuminate\Support\Facades\RateLimiter;

class RateLimitedJob
{
    public function __construct(
        private string $key,
        private int $maxAttempts = 10,
        private int $decaySeconds = 60
    ) {}

    public function handle(object $job, Closure $next): void
    {
        // Release job back to queue if rate limit exceeded
        if (RateLimiter::tooManyAttempts($this->key, $this->maxAttempts)) {
            $job->release($this->decaySeconds);
            return;
        }

        RateLimiter::hit($this->key, $this->decaySeconds);

        $next($job);
    }
}

잡 클래스에 middleware() 메서드를 정의하여 미들웨어를 적용합니다:

App/Jobs/CallExternalApi.phpphp
public function middleware(): array
{
    return [
        new RateLimitedJob(
            key: 'external-api',
            maxAttempts: 30,
            decaySeconds: 60
        ),
        // Prevent duplicate jobs from running concurrently
        (new WithoutOverlapping($this->apiResource->id))
            ->releaseAfter(300)
            ->expireAfter(600),
    ];
}

WithoutOverlapping 미들웨어는 원자적 잠금을 사용하여 특정 키로 식별되는 잡의 인스턴스가 한 번에 하나만 실행되도록 보장합니다. 레이트 제한과 결합하면 중복 처리와 API 스로틀링을 모두 방지할 수 있습니다.

실패한 잡 처리와 재시도 전략

프로덕션 큐 시스템에는 견고한 장애 처리가 필수적입니다. Laravel은 실패한 잡을 전체 페이로드, 예외 트레이스, 장애를 발생시킨 큐/연결 정보와 함께 failed_jobs 테이블에 저장합니다. 각 잡 클래스의 failed() 메서드는 모든 재시도가 소진된 후 실행됩니다.

잡별 재시도 동작 설정으로 세밀한 제어가 가능합니다:

App/Jobs/SyncExternalData.phpphp
class SyncExternalData implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;

    // Exponential backoff: 10s, 30s, 60s, 120s, 300s
    public function backoff(): array
    {
        return [10, 30, 60, 120, 300];
    }

    // Job-specific timeout
    public int $timeout = 180;

    // Maximum exceptions before marking as failed
    public int $maxExceptions = 3;

    public function retryUntil(): \DateTime
    {
        // Keep retrying for up to 24 hours
        return now()->addHours(24);
    }

    public function handle(): void
    {
        $response = Http::timeout(30)
            ->retry(2, 1000)
            ->get('https://api.vendor.com/data');

        if ($response->failed()) {
            // Release back to queue with delay for transient failures
            $this->release(60);
            return;
        }

        DataSync::process($response->json());
    }

    public function failed(\Throwable $e): void
    {
        Notification::route('slack', config('services.slack.ops_channel'))
            ->notify(new SyncFailed($e));
    }
}

면접에서 $tries, $maxExceptions, retryUntil()의 차이점은 매우 중요합니다. $tries는 수동 릴리스를 포함한 모든 시도를 카운트합니다. $maxExceptions는 처리되지 않은 예외만 카운트합니다. retryUntil()은 시도 횟수와 무관하게 시간 범위를 설정합니다.

큐 워커 관리와 배포

프로덕션 환경의 큐 워커에는 프로세스 감독, 배포 시 그레이스풀 재시작, 리소스 관리가 필요합니다. Supervisor는 워커를 안정적으로 유지하는 표준 도구입니다.

ini
; /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopwaitsecs=3600
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/worker.log
stopasgroup=true
killasgroup=true

배포 시 주요 고려사항:

  • 그레이스풀 재시작: php artisan queue:restart는 워커에게 현재 잡을 완료한 후 재시작하라는 신호를 보냅니다. 배포 중 잡 손상을 방지합니다.
  • 최대 시간/잡 수: --max-time=3600과 --max-jobs=1000은 워커 프로세스를 주기적으로 재활용하여 메모리 누수를 방지합니다.
  • 슬립 간격: --sleep=3은 빈 큐를 다시 폴링하기까지 워커가 대기하는 시간을 제어합니다. 값을 줄이면 응답성이 향상되지만 데이터베이스/Redis 부하도 증가합니다.
  • 다중 큐: --queue=critical,default,low는 우선순위에 따라 큐를 처리합니다. 워커는 critical 큐를 완전히 소진한 후 default로 넘어갑니다.

Laravel 12.37에서는 백그라운드 큐 연결이 도입되어 Concurrently::defer()를 사용한 잡 지연 실행이 가능합니다. 이 드라이버는 잡을 직렬화하여 별도의 PHP 프로세스에서 실행합니다. 완전한 큐 인프라가 필요하지 않은 경량 잡에 적합합니다.

유니크 잡과 암호화된 페이로드

프로덕션 환경과 면접에서 자주 등장하는 두 가지 패턴이 있습니다. 특정 키에 대해 잡이 한 번만 실행되도록 보장하는 패턴과 잡 페이로드의 민감한 데이터를 보호하는 패턴입니다.

App/Jobs/RebuildSearchIndex.phpphp
use Illuminate\Contracts\Queue\ShouldBeUnique;

class RebuildSearchIndex implements ShouldQueue, ShouldBeUnique
{
    // Lock duration in seconds
    public int $uniqueFor = 3600;

    public function __construct(
        public readonly string $indexName
    ) {}

    // Unique key scopes the lock to this specific index
    public function uniqueId(): string
    {
        return $this->indexName;
    }

    public function handle(): void
    {
        SearchIndex::rebuild($this->indexName);
    }
}

민감한 데이터(사용자 자격 증명, 결제 토큰)를 포함하는 잡에는 ShouldBeEncrypted 인터페이스가 직렬화된 전체 페이로드를 저장 시 암호화합니다:

App/Jobs/ProcessPayment.phpphp
use Illuminate\Contracts\Queue\ShouldBeEncrypted;

class ProcessPayment implements ShouldQueue, ShouldBeEncrypted
{
    public function __construct(
        private string $paymentToken,
        private float $amount
    ) {}

    public function handle(PaymentGateway $gateway): void
    {
        $gateway->charge($this->paymentToken, $this->amount);
    }
}

페이로드는 Redis나 데이터베이스에 저장되기 전에 애플리케이션 키로 암호화됩니다. 워커는 역직렬화 전에 자동으로 복호화합니다.

Laravel 큐 관련 면접 빈출 질문

기술 면접에서는 표면적인 API 지식을 넘어선 큐 아키텍처 이해도를 평가합니다.

큐에 넣은 잡이 삭제된 Eloquent 모델을 참조하면 어떻게 되는가? SerializesModels를 사용하는 경우, 워커는 잡 처리 시 ID로 모델을 조회합니다. 모델이 존재하지 않으면 Laravel은 ModelNotFoundException을 발생시킵니다. 이를 우아하게 처리하려면 $deleteWhenMissingModels 프로퍼티를 true로 설정합니다. 잡은 실패 대신 큐에서 조용히 삭제됩니다.

ShouldBeUnique와 WithoutOverlapping 미들웨어의 차이점은? ShouldBeUnique는 동일한 유니크 키를 가진 잡이 이미 큐에 존재할 때 잡의 디스패치를 방지합니다. WithoutOverlapping은 디스패치는 허용하되 동시 실행을 방지합니다. 동일한 키를 가진 잡이 이미 실행 중이면 새 인스턴스는 큐로 되돌려집니다. 서로 다른 문제를 해결하며 함께 사용할 수 있습니다.

retryUntil()이 $tries보다 선호되는 상황은? 복구 시간을 예측할 수 없는 외부 서비스와 상호작용하는 잡에는 retryUntil()을 사용합니다. 고정된 재시도 횟수($tries = 3)는 짧은 장애 중에 시도를 소진할 수 있습니다. retryUntil()은 시간 범위(예: 24시간)를 설정하고 서비스가 복구되거나 시간이 만료될 때까지 백오프를 적용하며 재시도를 계속합니다.

다중 큐에서 큐 우선순위는 어떻게 작동하는가? queue:work --queue=critical,default,low로 우선순위 시스템을 구성합니다. 워커는 critical 큐를 완전히 소진한 후 default를 확인하고, default 후에 low를 확인합니다. 피크 부하 시 낮은 우선순위 잡은 기아 상태에 빠질 수 있습니다. 엄격한 SLA가 필요한 경우 큐별 전용 워커를 배치하는 것이 더 나은 보장을 제공합니다.

연습을 시작하세요!

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

결론

  • Laravel 큐는 Redis, SQS, 데이터베이스, Laravel 12.37의 새로운 백그라운드 연결을 지원하는 드라이버 독립적 API로 큐 백엔드를 추상화합니다
  • 잡 배치 처리는 집합적 진행 상황 추적으로 병렬 워크로드를 처리하고, 체이닝은 도메인 워크플로우의 순차적 실행을 보장합니다
  • 큐 미들웨어(레이트 제한, WithoutOverlapping)는 횡단 관심사를 잡의 비즈니스 로직에서 분리합니다
  • 실패한 잡 처리는 $tries, $maxExceptions, retryUntil(), 지수 백오프를 결합한 견고한 재시도 전략을 구현합니다
  • ShouldBeUnique는 중복 디스패치를 방지하고, ShouldBeEncrypted는 저장 시 민감한 페이로드를 보호합니다
  • 프로덕션 워커에는 Supervisor, 배포 시 그레이스풀 재시작, --max-time과 --max-jobs를 통한 메모리 관리가 필요합니다
  • 면접 대비에서는 디스패치 시점의 유일성과 실행 시점의 잠금 구분, 모델 직렬화 동작, 큐 우선순위 기아 문제를 다루어야 합니다

Laravel 면접 질문 실전 연습을 위해, SharpSkill 문제 은행에서 큐, 미들웨어, Eloquent 패턴에 대한 상세 해설을 제공합니다.

연습을 시작하세요!

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

오늘의 챌린지

Laravel 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 5월 3일 업데이트

태그

#laravel
#queues
#jobs
#async
#interview

공유

관련 기사