메인 콘텐츠로 건너뛰기

파이프라인 패턴이란

파이프라인 패턴은 처리 대상 오브젝트(passable)를 일련의 파이프(처리 단계)에 순서대로 통과시키는 디자인 패턴입니다. 각 파이프는 앞 단계의 출력을 받아 처리를 더한 뒤 다음 단계로 전달합니다. Laravel에서는 Illuminate\Pipeline\Pipeline 클래스가 이 패턴을 구현하고 있으며, 미들웨어의 처리가 바로 이 구조로 동작합니다.
リクエスト → [ミドルウェア1] → [ミドルウェア2] → [コントローラー] → レスポンス

Laravel 코어에서의 사용 지점

파이프라인은 Laravel의 핵심에서 널리 사용됩니다.
  • HTTP 미들웨어Illuminate\Foundation\Http\Kernel에서 요청을 미들웨어의 파이프라인에 통과시킴
  • 콘솔 명령어 — Artisan 명령어의 전후 처리
  • Router의 미들웨어 — 라우트 그룹의 미들웨어 적용
HTTP 커널이 파이프라인을 사용하는 부분을 살펴보겠습니다.
// Illuminate\Foundation\Http\Kernel より
protected function sendRequestThroughRouter($request)
{
    return (new Pipeline($this->app))
        ->send($request)
        ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
        ->then($this->dispatchToRouter());
}

기본적인 사용법

send / through / thenReturn

가장 단순한 파이프라인의 구성입니다.
use Illuminate\Pipeline\Pipeline;

$result = app(Pipeline::class)
    ->send('hello')
    ->through([
        function (string $passable, Closure $next): string {
            return $next(strtoupper($passable));
        },
        function (string $passable, Closure $next): string {
            return $next($passable . '!');
        },
    ])
    ->thenReturn();

// $result === 'HELLO!'
  • send($passable) — 파이프라인에 통과시킬 오브젝트를 지정
  • through($pipes) — 파이프의 배열을 지정
  • thenReturn() — 파이프라인을 실행하여 결과를 반환

then

최종적인 처리를 클로저로 지정하고 싶은 경우에는 then()을 사용합니다.
$result = app(Pipeline::class)
    ->send($request)
    ->through([AuthPipe::class, LogPipe::class])
    ->then(function ($request) {
        return 'processed: ' . $request;
    });

pipe

나중에 파이프를 추가하려면 pipe()를 사용합니다.
$pipeline = app(Pipeline::class)
    ->send($data)
    ->through([FirstPipe::class]);

// 条件によって追加のパイプを挿入
if ($needsExtra) {
    $pipeline->pipe(ExtraPipe::class);
}

$result = $pipeline->thenReturn();

클래스 기반의 파이프

클로저 대신 전용 클래스를 사용하면 재사용성이 높아집니다. 각 파이프 클래스는 handle 메서드를 구현합니다.
namespace App\Pipes;

use Closure;

class TrimPipe
{
    public function handle(string $passable, Closure $next): string
    {
        return $next(trim($passable));
    }
}
namespace App\Pipes;

use Closure;

class SanitizePipe
{
    public function handle(string $passable, Closure $next): string
    {
        $sanitized = htmlspecialchars($passable, ENT_QUOTES, 'UTF-8');

        return $next($sanitized);
    }
}
use App\Pipes\TrimPipe;
use App\Pipes\SanitizePipe;
use Illuminate\Pipeline\Pipeline;

$result = app(Pipeline::class)
    ->send('  <script>alert("xss")</script>  ')
    ->through([TrimPipe::class, SanitizePipe::class])
    ->thenReturn();

// $result === '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'

via — 호출할 메서드를 변경하기

기본적으로 파이프의 handle 메서드가 호출되지만, via()로 다른 메서드명을 지정할 수 있습니다.
class ValidatePipe
{
    public function process(array $data, Closure $next): array
    {
        // バリデーション処理
        return $next($data);
    }
}

$result = app(Pipeline::class)
    ->send($data)
    ->through([ValidatePipe::class])
    ->via('process')
    ->thenReturn();

파이프에의 파라미터 전달

클래스명에 :,를 사용하여 파이프에 파라미터를 전달할 수 있습니다. 이는 미들웨어의 throttle:60,1 같은 구문과 같은 구조입니다.
namespace App\Pipes;

use Closure;

class LimitLengthPipe
{
    public function handle(string $passable, Closure $next, int $max = 100): string
    {
        return $next(substr($passable, 0, $max));
    }
}
$result = app(Pipeline::class)
    ->send($longText)
    ->through(['App\Pipes\LimitLengthPipe:50'])
    ->thenReturn();

finally — 파이프라인 종료 후의 처리

finally() 메서드를 사용하면, 파이프라인 종료 후 반드시 실행되는 콜백을 등록할 수 있습니다. 성공·실패와 관계없이 실행됩니다.
$result = app(Pipeline::class)
    ->send($request)
    ->through([LogPipe::class, AuthPipe::class])
    ->finally(function ($passable) {
        // ログ記録やリソース解放など
        logger()->info('Pipeline completed', ['request' => $passable]);
    })
    ->thenReturn();

withinTransaction — 트랜잭션 내에서 실행

Laravel 11 이후, 파이프라인 전체를 데이터베이스 트랜잭션 내에서 실행할 수 있습니다.
$result = app(Pipeline::class)
    ->send($order)
    ->through([
        CreateOrderPipe::class,
        ChargePaymentPipe::class,
        SendConfirmationPipe::class,
    ])
    ->withinTransaction()
    ->thenReturn();
특정 데이터베이스 커넥션을 지정할 수도 있습니다.
->withinTransaction('mysql')
어떤 파이프에서 예외가 발생한 경우, 트랜잭션 전체가 롤백됩니다.

실전 예: 주문 처리 파이프라인

EC 사이트의 주문 처리를 여러 파이프로 구성하는 예입니다.
1

파이프 클래스 만들기

namespace App\Pipes\Order;

use App\Models\Order;
use Closure;

class ValidateInventoryPipe
{
    public function handle(Order $order, Closure $next): Order
    {
        foreach ($order->items as $item) {
            if ($item->product->stock < $item->quantity) {
                throw new \RuntimeException("在庫不足: {$item->product->name}");
            }
        }

        return $next($order);
    }
}
namespace App\Pipes\Order;

use App\Models\Order;
use Closure;

class ReserveInventoryPipe
{
    public function handle(Order $order, Closure $next): Order
    {
        foreach ($order->items as $item) {
            $item->product->decrement('stock', $item->quantity);
        }

        return $next($order);
    }
}
namespace App\Pipes\Order;

use App\Models\Order;
use App\Services\PaymentService;
use Closure;

class ProcessPaymentPipe
{
    public function __construct(
        protected PaymentService $payment,
    ) {}

    public function handle(Order $order, Closure $next): Order
    {
        $this->payment->charge($order->total, $order->payment_token);
        $order->update(['status' => 'paid']);

        return $next($order);
    }
}
2

파이프라인 실행하기

use App\Models\Order;
use App\Pipes\Order\ValidateInventoryPipe;
use App\Pipes\Order\ReserveInventoryPipe;
use App\Pipes\Order\ProcessPaymentPipe;
use Illuminate\Pipeline\Pipeline;

class OrderService
{
    public function process(Order $order): Order
    {
        return app(Pipeline::class)
            ->send($order)
            ->through([
                ValidateInventoryPipe::class,
                ReserveInventoryPipe::class,
                ProcessPaymentPipe::class,
            ])
            ->withinTransaction()
            ->thenReturn();
    }
}

Pipeline의 내부 구현

carry() 메서드가 파이프라인의 핵심 부분입니다. array_reduce를 사용해 파이프를 오른쪽에서 왼쪽으로 접어 클로저 체인을 구축합니다.
// Pipeline::then() の内部
protected function carry()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            if (is_callable($pipe)) {
                return $pipe($passable, $stack);
            } elseif (! is_object($pipe)) {
                [$name, $parameters] = $this->parsePipeString($pipe);
                $pipe = $this->getContainer()->make($name);
                $parameters = array_merge([$passable, $stack], $parameters);
            } else {
                $parameters = [$passable, $stack];
            }

            return $pipe->{$this->method}(...$parameters);
        };
    };
}
array_reverse로 파이프를 역순으로 하여 array_reduce에 전달함으로써, 호출 순서를 올바르게 유지합니다. 최초의 파이프가 최초로 실행되도록 클로저 스택을 안쪽에서부터 쌓아 올려 갑니다.
파이프라인은 “양파형” 아키텍처라고도 불립니다. 요청은 바깥쪽 층(최초의 파이프)에서 안쪽을 향해 통과하고, 응답은 안쪽에서 바깥쪽으로 돌아옵니다. 각 미들웨어가 $next()를 호출하기 전후에 처리를 더할 수 있는 것은 이 때문입니다.

Macroable 트레이트의 병용

Pipeline 클래스는 Macroable 트레이트를 사용하고 있어, 고유의 메서드를 추가할 수 있습니다.
use Illuminate\Pipeline\Pipeline;

Pipeline::macro('sendThroughLogging', function (array $pipes) {
    /** @var Pipeline $this */
    return $this->through(array_map(function ($pipe) {
        return function ($passable, $next) use ($pipe) {
            logger()->debug("Entering pipe: {$pipe}");
            $result = $next($passable);
            logger()->debug("Exiting pipe: {$pipe}");
            return $result;
        };
    }, $pipes));
});

$result = app(Pipeline::class)
    ->send($data)
    ->sendThroughLogging([TrimPipe::class, SanitizePipe::class])
    ->thenReturn();

다음 단계

Macroable 트레이트

기존 클래스에 고유 메서드를 추가하는 Macroable 트레이트의 사용법을 배웁니다.
마지막 수정일 2026년 7월 13일