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
#queues
#jobs
#async
#interview

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

Laravel๊ณผ PHP ๋ฉด์ ‘ ์งˆ๋ฌธ - ์ข…ํ•ฉ ๊ฐ€์ด๋“œ

Laravel๊ณผ PHP ๋ฉด์ ‘ ์งˆ๋ฌธ: 2026๋…„ ํ•ต์‹ฌ 25์„ 

Laravel๊ณผ PHP ๋ฉด์ ‘์—์„œ ๊ฐ€์žฅ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” 25๊ฐ€์ง€ ์งˆ๋ฌธ์„ ์ƒ์„ธํžˆ ๋‹ค๋ฃน๋‹ˆ๋‹ค. Eloquent ORM, ๋ฏธ๋“ค์›จ์–ด, ํ, ํ…Œ์ŠคํŠธ, ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด์— ๋Œ€ํ•œ ์ƒ์„ธํ•œ ๋‹ต๋ณ€๊ณผ ์ฝ”๋“œ ์˜ˆ์ œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Laravel ๋ฏธ๋“ค์›จ์–ด ์•„ํ‚คํ…์ฒ˜ - ์š”์ฒญ ํŒŒ์ดํ”„๋ผ์ธ, ์ธ์ฆ, ์†๋„ ์ œํ•œ ๊ตฌ์กฐ ๋‹ค์ด์–ด๊ทธ๋žจ

Laravel Middleware ์™„๋ฒฝ ๊ฐ€์ด๋“œ: ์ธ์ฆ, ์†๋„ ์ œํ•œ, ์ปค์Šคํ…€ ๋ฏธ๋“ค์›จ์–ด ๊ตฌ์ถ•

Laravel ๋ฏธ๋“ค์›จ์–ด์˜ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์กฐ๋ถ€ํ„ฐ ์ธ์ฆ, ์†๋„ ์ œํ•œ(Rate Limiting), ์ปค์Šคํ…€ ๋ฏธ๋“ค์›จ์–ด ์ž‘์„ฑ, ํ”„๋กœ๋•์…˜ ํŒจํ„ด๊นŒ์ง€ ์‹ค๋ฌด ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ ์ฒด๊ณ„์ ์œผ๋กœ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

Laravel๋ฅผ ์œ„ํ•œ Eloquent ORM ํŒจํ„ด๊ณผ ์ตœ์ ํ™”

Eloquent ORM: Laravel๋ฅผ ์œ„ํ•œ ํŒจํ„ด๊ณผ ์ตœ์ ํ™”

๊ณ ๊ธ‰ ํŒจํ„ด๊ณผ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์œผ๋กœ Eloquent ORM์„ ๋งˆ์Šคํ„ฐํ•ฉ๋‹ˆ๋‹ค. Laravel ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ eager loading, query scope, accessor, mutator ๋ฐ ์„ฑ๋Šฅ.