메인 콘텐츠로 건너뛰기

병행 처리란

여러 외부 API에의 요청이나 데이터베이스의 집계 처리 등, 서로 의존하지 않는 여러 처리를 순서대로 실행하면 합계 시간은 각각의 처리 시간의 합계가 됩니다. 이것들을 동시에 실행하면 합계 시간은 가장 시간이 걸리는 처리 1건분으로 단축할 수 있습니다. Laravel의 Concurrency 파사드는 이러한 병행 실행을 단순한 API로 실현합니다.
Concurrency 파사드는 Laravel 11에서 도입되었으며, Laravel 13에서도 계속 이용할 수 있습니다. 기본 드라이버는 자식 PHP 프로세스를 사용하기 때문에 추가 패키지 없이 동작합니다.

구조

Concurrency 파사드는 전달된 클로저를 직렬화하여 숨겨진 Artisan 명령어에 보내고, 각각 별도의 PHP 프로세스로서 실행합니다. 처리가 완료되면 반환값을 직렬화하여 부모 프로세스에 반환합니다. 3개의 드라이버가 준비되어 있습니다.
드라이버설명
process기본값. 자식 PHP 프로세스를 기동해 실행. Web 요청에서도 동작
forkspatie/fork 패키지가 필요. 프로세스를 fork하기 때문에 CLI에서만 동작하지만 고속
sync병행 실행하지 않고 순서대로 실행. 테스트 용도에 적합

기본적인 사용법

Concurrency::run()

run() 메서드에 클로저의 배열을 전달하면, 그것들이 병행 실행됩니다. 반환값은 각 클로저가 반환한 값을 배열로 받습니다.
use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;

[$userCount, $orderCount] = Concurrency::run([
    fn () => DB::table('users')->count(),
    fn () => DB::table('orders')->count(),
]);
배열의 구조 분해로 각 결과를 변수로 받을 수 있습니다. 클로저의 순서와 반환값의 순서는 일치합니다.

드라이버 지정

특정 드라이버를 사용하고 싶은 경우 driver() 메서드로 지정합니다.
$results = Concurrency::driver('fork')->run([
    fn () => fetchFromApiA(),
    fn () => fetchFromApiB(),
]);
기본 드라이버를 변경하려면 설정 파일을 공개해 default 옵션을 업데이트합니다.
php artisan config:publish concurrency

fork 드라이버의 사용법

fork 드라이버는 process 드라이버보다 고속이지만, PHP의 CLI 환경(Artisan 명령어나 큐 워커)에서만 사용할 수 있습니다. Web 요청 중에는 사용할 수 없습니다. 사용 전에 spatie/fork 패키지를 설치하십시오.
composer require spatie/fork
fork 드라이버는 Web 요청 중에는 동작하지 않습니다. Artisan 명령어나 큐 워커 중에 병행 처리를 수행하고 싶을 때 사용하십시오.

실행 결과를 받지 않기: Concurrency::defer()

처리의 결과에는 관심이 없고, HTTP 응답을 반환한 후에 백그라운드에서 실행하고 싶은 경우는 defer() 메서드를 사용합니다.
use App\Services\Metrics;
use Illuminate\Support\Facades\Concurrency;

Concurrency::defer([
    fn () => Metrics::report('users'),
    fn () => Metrics::report('orders'),
]);
defer()를 호출한 시점에서는 클로저는 실행되지 않습니다. HTTP 응답이 사용자에게 전송된 후에 병행하여 실행됩니다.
분석 데이터의 기록이나 캐시의 워밍업 등, 사용자를 기다리게 할 필요가 없는 처리에 defer()는 최적입니다.

실전 예시: 여러 외부 API를 동시에 호출

EC 사이트의 대시보드에서 재고 관리 API, 매출 집계 API, 배송 상황 API의 3개에서 정보를 취득하는 예시를 생각합니다.

순차 실행(개선 전)

use Illuminate\Support\Facades\Http;

public function dashboard(): array
{
    // 각 요청이 순서대로 완료할 때까지 대기(합계 약 3초)
    $inventory = Http::get('https://api.example.com/inventory')->json();
    $sales     = Http::get('https://api.example.com/sales')->json();
    $shipping  = Http::get('https://api.example.com/shipping')->json();

    return compact('inventory', 'sales', 'shipping');
}

병행 실행(개선 후)

use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\Http;

public function dashboard(): array
{
    // 3개의 요청을 동시에 실행(가장 느린 API 1건분의 시간으로 완료)
    [$inventory, $sales, $shipping] = Concurrency::run([
        fn () => Http::get('https://api.example.com/inventory')->json(),
        fn () => Http::get('https://api.example.com/sales')->json(),
        fn () => Http::get('https://api.example.com/shipping')->json(),
    ]);

    return compact('inventory', 'sales', 'shipping');
}
각 API가 1초 걸리는 경우, 순차 실행에서는 합계 3초가 걸리지만 병행 실행에서는 약 1초에 완료됩니다.

여러 데이터베이스 집계를 동시 실행

use Illuminate\Support\Facades\Concurrency;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;

public function statistics(): array
{
    [$totalUsers, $activeUsers, $totalOrders, $revenue] = Concurrency::run([
        fn () => DB::table('users')->count(),
        fn () => DB::table('users')->where('active', true)->count(),
        fn () => DB::table('orders')->count(),
        fn () => DB::table('orders')->sum('total_amount'),
    ]);

    return [
        'total_users'  => $totalUsers,
        'active_users' => $activeUsers,
        'total_orders' => $totalOrders,
        'revenue'      => $revenue,
    ];
}
데이터베이스 집계에 process 드라이버를 사용하는 경우, 자식 프로세스마다 새로운 데이터베이스 접속이 확립됩니다. 동시 실행 수가 많은 경우 데이터베이스의 최대 접속 수에 주의해 주십시오.

테스트 시의 설정

테스트 환경에서는 sync 드라이버를 사용하면 클로저를 순서대로 실행합니다. 병행 실행에 의한 프로세스의 기동 비용이 없어 테스트가 고속화됩니다.
// tests/Feature/DashboardTest.php
use Illuminate\Support\Facades\Concurrency;

public function test_dashboard_returns_correct_data(): void
{
    Concurrency::fake(); // sync 드라이버로 전환

    $response = $this->get('/dashboard');

    $response->assertStatus(200);
}
또는 .env.testing에서 기본 드라이버를 변경하는 방법도 있습니다.
CONCURRENCY_DRIVER=sync

주의 사항

클로저는 직렬화되어 자식 프로세스에 전달됩니다. 직렬화할 수 없는 객체(데이터베이스 접속, 파일 핸들, 리소스 등)를 클로저의 외측에서 캡처할 수는 없습니다. 필요한 객체는 클로저 안에서 새로 생성해 주십시오.
// NG: 외측의 Eloquent 모델을 캡처
$user = User::find(1);
Concurrency::run([
    fn () => $user->orders()->count(), // 직렬화할 수 없을 가능성이 있음
]);

// OK: 클로저 안에서 데이터를 취득
$userId = 1;
Concurrency::run([
    fn () => Order::where('user_id', $userId)->count(),
]);
process 드라이버는 자식 프로세스의 기동 비용이 있기 때문에, 처리 자체가 매우 짧은(수 밀리초 이하) 경우는 병행 실행해도 빨라지지 않는 경우가 있습니다. HTTP 요청이나 데이터베이스 집계 등, 처리에 100ms 이상 걸리는 것을 병행화할 때 효과를 발휘합니다.
fork 드라이버는 PHP의 프로세스를 fork하기 때문에 Web 요청 중(FPM이나 Apache)에는 사용할 수 없습니다. Artisan 명령어나 큐 워커 안에서만 사용해 주십시오.
병행 실행 중에 예외가 발생하면 run()은 예외를 다시 던집니다. 개별 처리에서 예외가 발생해도 다른 처리를 계속시키고 싶은 경우, 클로저 안에서 try/catch를 사용해 주십시오.
Concurrency::run([
    function () {
        try {
            return Http::get('https://api.example.com/data')->json();
        } catch (\Exception $e) {
            return null; // 실패 시는 null 을 반환
        }
    },
]);

다음 단계

큐와 잡

처리를 백그라운드에서 비동기 실행하는 큐와 잡의 사용법을 배웁니다
마지막 수정일 2026년 7월 13일