본문으로 건너뛰기
버전: 11.x

프로세스 (Processes)

소개

라라벨은 Symfony Process 컴포넌트를 기반으로 간결하고 직관적인 API를 제공합니다. 이를 통해 라라벨 애플리케이션에서 외부 프로세스를 손쉽게 실행할 수 있습니다. 라라벨의 프로세스 기능은 가장 일반적으로 사용되는 사례에 초점을 맞추며, 개발자가 즐겁게 사용할 수 있는 경험을 제공합니다.

프로세스 호출

프로세스를 실행하려면 Process 파사드가 제공하는 runstart 메서드를 사용할 수 있습니다. run 메서드는 프로세스를 실행하고 완료될 때까지 기다리며, start 메서드는 비동기적으로 프로세스를 실행할 때 사용합니다. 이 문서에서는 두 가지 방식 모두를 살펴봅니다. 먼저, 기본적인 동기 프로세스를 실행하고 그 결과를 확인하는 방법을 살펴보겠습니다.

use Illuminate\Support\Facades\Process;

$result = Process::run('ls -la');

return $result->output();

run 메서드가 반환하는 Illuminate\Contracts\Process\ProcessResult 인스턴스에는 프로세스 결과를 확인할 수 있는 다양한 유용한 메서드가 포함되어 있습니다.

$result = Process::run('ls -la');

$result->successful();
$result->failed();
$result->exitCode();
$result->output();
$result->errorOutput();

예외 던지기

프로세스 결과에서, 종료 코드가 0보다 큰 경우(즉, 실패를 의미할 때) Illuminate\Process\Exceptions\ProcessFailedException 예외를 던지려면 throw 또는 throwIf 메서드를 사용할 수 있습니다. 프로세스가 실패하지 않은 경우, 프로세스 결과 인스턴스가 반환됩니다.

$result = Process::run('ls -la')->throw();

$result = Process::run('ls -la')->throwIf($condition);

프로세스 옵션

프로세스를 실행하기 전에 그 동작을 세부적으로 조정해야 할 수도 있습니다. 라라벨은 작업 디렉터리, 타임아웃, 환경 변수 등 다양한 프로세스 옵션을 설정할 수 있게 지원합니다.

작업 디렉터리 경로

path 메서드를 사용해 프로세스의 작업 디렉터리를 지정할 수 있습니다. 이 메서드를 호출하지 않으면, 현재 실행 중인 PHP 스크립트의 작업 디렉터리를 그대로 사용합니다.

$result = Process::path(__DIR__)->run('ls -la');

입력 값

input 메서드를 사용하면 프로세스의 표준 입력(standard input)으로 값을 전달할 수 있습니다.

$result = Process::input('Hello World')->run('cat');

타임아웃

기본적으로 프로세스가 60초 이상 실행되면 Illuminate\Process\Exceptions\ProcessTimedOutException 예외가 발생합니다. timeout 메서드를 사용해 이 동작을 원하는 시간(초 단위)으로 변경할 수 있습니다.

$result = Process::timeout(120)->run('bash import.sh');

또한, 아예 프로세스 타임아웃을 비활성화하려면 forever 메서드를 호출하면 됩니다.

$result = Process::forever()->run('bash import.sh');

idleTimeout 메서드를 이용해, 출력이 없이 프로세스가 최대로 허용되는(최대) 시간(초 단위)도 지정할 수 있습니다.

$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');

환경 변수

env 메서드를 통해 프로세스에 환경 변수를 전달할 수 있습니다. 실행된 프로세스는 시스템에 정의된 모든 환경 변수도 함께 상속받습니다.

$result = Process::forever()
->env(['IMPORT_PATH' => __DIR__])
->run('bash import.sh');

상속된 환경 변수 중에서 특정 변수를 제거하고 싶다면, 해당 변수에 false 값을 지정하면 됩니다.

$result = Process::forever()
->env(['LOAD_PATH' => false])
->run('bash import.sh');

TTY 모드

tty 메서드를 사용하면 프로세스의 입출력을 자신의 프로그램과 연결(TTY 모드 활성화)할 수 있습니다. 이를 통해, Vim이나 Nano 같은 에디터를 프로세스로 실행할 수 있습니다.

Process::forever()->tty()->run('vim');

프로세스 출력

앞서 설명한 것처럼, 프로세스 결과 인스턴스의 output(stdout)과 errorOutput(stderr) 메서드로 출력을 확인할 수 있습니다.

use Illuminate\Support\Facades\Process;

$result = Process::run('ls -la');

echo $result->output();
echo $result->errorOutput();

실행 도중 실시간으로 출력을 받고 싶다면, run 메서드의 두 번째 인자로 클로저를 전달하면 됩니다. 이 클로저는 "타입"(출력 타입, stdout 또는 stderr)과 출력 문자열을 인자로 받습니다.

$result = Process::run('ls -la', function (string $type, string $output) {
echo $output;
});

라라벨은 또한, 프로세스의 출력에 특정 문자열이 포함되어 있는지 쉽게 확인할 수 있도록 seeInOutputseeInErrorOutput 메서드를 제공합니다.

if (Process::run('ls -la')->seeInOutput('laravel')) {
// ...
}

프로세스 출력 비활성화

프로세스의 출력이 너무 많아 굳이 확인할 필요가 없다면, 출력 수집을 완전히 비활성화하여 메모리 사용량을 줄일 수 있습니다. 이를 위해, 프로세스를 생성할 때 quietly 메서드를 호출하면 됩니다.

use Illuminate\Support\Facades\Process;

$result = Process::quietly()->run('bash import.sh');

파이프라인

한 프로세스의 출력을 다른 프로세스의 입력으로 사용하고 싶은 경우가 있습니다. 이런 방식을 '파이핑(piping)'이라고 하며, Process 파사드의 pipe 메서드를 사용하면 쉽게 구현할 수 있습니다. pipe 메서드는 파이프라인에 정의된 프로세스들을 동기적으로 실행하며, 마지막 프로세스의 결과를 반환합니다.

use Illuminate\Process\Pipe;
use Illuminate\Support\Facades\Process;

$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
});

if ($result->successful()) {
// ...
}

파이프라인을 개별적으로 설정할 필요가 없다면, 명령어 문자열 배열을 바로 전달하는 것도 가능합니다.

$result = Process::pipe([
'cat example.txt',
'grep -i "laravel"',
]);

파이프라인의 실행 중 실시간 출력을 수집하려면 pipe 메서드의 두 번째 인자로 클로저를 전달하면 됩니다. 이 클로저는 "타입"(stdout 또는 stderr)과 출력 문자열을 인자로 받습니다.

$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
}, function (string $type, string $output) {
echo $output;
});

또한 as 메서드를 사용해, 파이프라인의 각 프로세스에 문자열 키를 지정할 수 있습니다. 이 키는 pipe 메서드에 전달되는 출력 클로저에도 함께 전달되어, 어떤 프로세스의 출력인지 쉽게 구분할 수 있습니다.

$result = Process::pipe(function (Pipe $pipe) {
$pipe->as('first')->command('cat example.txt');
$pipe->as('second')->command('grep -i "laravel"');
})->start(function (string $type, string $output, string $key) {
// ...
});

비동기 프로세스

run 메서드는 프로세스를 동기적으로 실행하지만, start 메서드는 프로세스를 비동기적으로 실행합니다. 이를 통해, 프로세스가 백그라운드에서 동작하는 동안 애플리케이션의 다른 작업도 동시에 처리할 수 있습니다. 프로세스를 실행한 후에는 running 메서드를 사용해 해당 프로세스가 아직 실행 중인지 확인할 수 있습니다.

$process = Process::timeout(120)->start('bash import.sh');

while ($process->running()) {
// ...
}

$result = $process->wait();

보시다시피, wait 메서드를 호출하면 프로세스가 끝날 때까지 기다린 뒤, 프로세스 결과 인스턴스를 반환받을 수 있습니다.

$process = Process::timeout(120)->start('bash import.sh');

// ...

$result = $process->wait();

프로세스 ID와 시그널

id 메서드를 사용하면 실행 중인 프로세스의 운영체제 프로세스 ID를 가져올 수 있습니다.

$process = Process::start('bash import.sh');

return $process->id();

signal 메서드를 이용해 실행 중인 프로세스에 "시그널"을 보낼 수 있습니다. 미리 정의된 시그널 상수 목록은 PHP 공식 문서에서 확인할 수 있습니다.

$process->signal(SIGUSR2);

비동기 프로세스 출력

비동기 프로세스가 실행 중일 때, outputerrorOutput 메서드로 지금까지의 전체 출력을 읽을 수 있고, latestOutputlatestErrorOutput 메서드를 이용하면 마지막으로 가져간 이후의 새로운 출력만 확인할 수 있습니다.

$process = Process::timeout(120)->start('bash import.sh');

while ($process->running()) {
echo $process->latestOutput();
echo $process->latestErrorOutput();

sleep(1);
}

동기 실행 방식처럼, 비동기 프로세스 실행 시에도 start 메서드의 두 번째 인자로 클로저를 전달해 실시간으로 출력을 받을 수 있습니다. 이때 클로저에는 출력 타입과(즉, stdout 또는 stderr) 출력 문자열이 인자로 전달됩니다.

$process = Process::start('bash import.sh', function (string $type, string $output) {
echo $output;
});

$result = $process->wait();

프로세스가 끝날 때까지 무조건 기다리는 대신, 출력 내용에 따라 기다림을 중단하고 싶다면 waitUntil 메서드를 사용할 수 있습니다. 이때 전달하는 클로저가 true를 반환하면 대기(waiting)를 멈춥니다.

$process = Process::start('bash import.sh');

$process->waitUntil(function (string $type, string $output) {
return $output === 'Ready...';
});

동시 실행 프로세스

라라벨은 동시에 여러 비동기 프로세스를 "pool(풀)"로 묶어 편리하게 관리할 수 있도록 지원합니다. 이를 위해 pool 메서드를 사용하며, 이 메서드는 Illuminate\Process\Pool 인스턴스를 전달받는 클로저를 인자로 받습니다.

이 클로저 안에서 풀에 포함할 프로세스를 자유롭게 정의할 수 있습니다. 프로세스 풀을 start 메서드로 시작하면, 실행 중인 프로세스들의 컬렉션running 메서드를 통해 확인할 수 있습니다.

use Illuminate\Process\Pool;
use Illuminate\Support\Facades\Process;

$pool = Process::pool(function (Pool $pool) {
$pool->path(__DIR__)->command('bash import-1.sh');
$pool->path(__DIR__)->command('bash import-2.sh');
$pool->path(__DIR__)->command('bash import-3.sh');
})->start(function (string $type, string $output, int $key) {
// ...
});

while ($pool->running()->isNotEmpty()) {
// ...
}

$results = $pool->wait();

위 예제처럼, 모든 풀 프로세스가 완료될 때까지 기다렸다가, wait 메서드를 호출해 각각의 프로세스 결과 인스턴스를 key(각각의 인덱스)로 접근할 수 있습니다.

$results = $pool->wait();

echo $results[0]->output();

더 편하게, concurrently 메서드를 사용하면 비동기 프로세스 풀을 즉시 시작하고, 결과도 바로 받아볼 수 있습니다. 이 방법은 PHP의 배열 구조 분해 문법과 함께 사용하면 매우 직관적입니다.

[$first, $second, $third] = Process::concurrently(function (Pool $pool) {
$pool->path(__DIR__)->command('ls -la');
$pool->path(app_path())->command('ls -la');
$pool->path(storage_path())->command('ls -la');
});

echo $first->output();

Pool 프로세스 이름 지정

프로세스 풀의 결과를 숫자 인덱스로 접근하는 것은 직관적이지 않으므로, as 메서드를 사용해 각 프로세스에 문자열 키를 부여할 수 있습니다. 이 키는 start 메서드에 전달하는 클로저에도 함께 전달되어, 어떤 프로세스의 출력인지 쉽게 알 수 있습니다.

$pool = Process::pool(function (Pool $pool) {
$pool->as('first')->command('bash import-1.sh');
$pool->as('second')->command('bash import-2.sh');
$pool->as('third')->command('bash import-3.sh');
})->start(function (string $type, string $output, string $key) {
// ...
});

$results = $pool->wait();

return $results['first']->output();

Pool 프로세스의 ID와 시그널

풀의 running 메서드는 풀 안의 모든 실행 중인 프로세스를 컬렉션으로 제공합니다. 이 컬렉션을 통해 각 풀 프로세스의 ID도 쉽게 접근할 수 있습니다.

$processIds = $pool->running()->each->id();

또한, 편의상 signal 메서드를 풀에 사용하면, 풀 내의 모든 프로세스에 동시에 시그널을 전달할 수 있습니다.

$pool->signal(SIGUSR2);

테스트

라라벨의 다양한 서비스와 마찬가지로, 프로세스 기능 또한 쉽고 명확하게 테스트할 수 있도록 도와줍니다. Process 파사드의 fake 메서드를 호출하면 프로세스 실행 시 더미(가짜) 결과를 반환하도록 할 수 있습니다.

프로세스 가짜 처리하기

라라벨이 프로세스를 가짜로 반환하는 기능을 살펴보기 위해, 다음과 같이 프로세스를 실행하는 라우트를 상상해보겠습니다.

use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Route;

Route::get('/import', function () {
Process::run('bash import.sh');

return 'Import complete!';
});

이 라우트를 테스트할 때는, Process 파사드의 fake 메서드를 인자 없이 호출해 모든 실행된 프로세스에 대해 성공 결과를 가짜로 반환하게 만들 수 있습니다. 또한, 해당 프로세스가 실제로 "실행되었는지" assertion도 할 수 있습니다.

<?php

use Illuminate\Process\PendingProcess;
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Support\Facades\Process;

test('process is invoked', function () {
Process::fake();

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

// 간단한 프로세스 assertion...
Process::assertRan('bash import.sh');

// 또는, 프로세스 설정 옵션까지 확인...
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
});
<?php

namespace Tests\Feature;

use Illuminate\Process\PendingProcess;
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Support\Facades\Process;
use Tests\TestCase;

class ExampleTest extends TestCase
{
public function test_process_is_invoked(): void
{
Process::fake();

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

// 간단한 프로세스 assertion...
Process::assertRan('bash import.sh');

// 또는, 프로세스 설정 옵션까지 확인...
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
}
}

앞서 설명한 대로, Process 파사드의 fake 메서드는 항상 출력이 없는 성공 결과를 반환하도록 합니다. 하지만, Process 파사드의 result 메서드를 활용하면 가짜 프로세스의 출력값과 종료 코드를 지정할 수 있습니다.

Process::fake([
'*' => Process::result(
output: 'Test output',
errorOutput: 'Test error output',
exitCode: 1,
),
]);

특정 프로세스 가짜 처리하기

앞선 예제에서 살펴본 것처럼, Process 파사드는 fake 메서드에 배열을 전달해 명령어별로 서로 다른 가짜 결과를 지정할 수 있습니다.

배열의 키는 가짜로 만들 명령 패턴이며, 각 키에 해당하는 값을 지정합니다. 이때 * 문자를 와일드카드로 사용할 수 있습니다. 가짜로 지정하지 않은 명령어는 실제로 실행됩니다. 명령어에 대해 더미 결과를 쉽게 생성하려면 Process 파사드의 result 메서드를 사용할 수 있습니다.

Process::fake([
'cat *' => Process::result(
output: 'Test "cat" output',
),
'ls *' => Process::result(
output: 'Test "ls" output',
),
]);

가짜 프로세스의 종료 코드나 에러 출력을 따로 설정할 필요가 없다면, 간단히 문자열로 결과를 지정할 수도 있습니다.

Process::fake([
'cat *' => 'Test "cat" output',
'ls *' => 'Test "ls" output',
]);

프로세스 시퀀스 가짜 처리하기

테스트하는 코드가 같은 명령어로 여러 번 프로세스를 실행하는 경우, 각 실행마다 서로 다른 결과를 가짜로 반환하고 싶을 수 있습니다. 이를 위해 Process 파사드의 sequence 메서드를 사용할 수 있습니다.

Process::fake([
'ls *' => Process::sequence()
->push(Process::result('First invocation'))
->push(Process::result('Second invocation')),
]);

비동기 프로세스 라이프사이클 가짜 처리

지금까지는 주로 run 메서드를 이용한 동기 프로세스의 가짜 처리에 대해 살펴보았습니다. 하지만, start로 실행되는 비동기 프로세스를 테스트할 때는 좀 더 세밀한 설정이 필요할 수 있습니다.

예를 들어, 아래와 같이 비동기 프로세스와 상호작용하는 라우트가 있다고 가정해봅시다.

use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;

Route::get('/import', function () {
$process = Process::start('bash import.sh');

while ($process->running()) {
Log::info($process->latestOutput());
Log::info($process->latestErrorOutput());
}

return 'Done';
});

이 프로세스를 올바르게 가짜 처리하려면, running 메서드의 반환값이 "true"로 나와야 하는 횟수와, 순차적으로 반환될 여러 줄의 출력도 지정할 수 있어야 합니다. 이를 위해 Process 파사드의 describe 메서드를 사용합니다.

Process::fake([
'bash import.sh' => Process::describe()
->output('First line of standard output')
->errorOutput('First line of error output')
->output('Second line of standard output')
->exitCode(0)
->iterations(3),
]);

위 예제에서, outputerrorOutput 메서드로 여러 줄의 출력(표준 출력/에러 출력)을 순차적으로 지정할 수 있습니다. exitCode 메서드로 프로세스의 종료 코드를 지정할 수 있으며, iterations 메서드로 running 메서드가 몇 번 "true"를 반환할지도 설정할 수 있습니다.

사용 가능한 assertion

앞서 설명한 대로 라라벨은 기능 테스트에서 사용할 수 있는 다양한 프로세스 assertion을 제공합니다. 아래에서 각 assertion에 대해 간단하게 설명합니다.

assertRan

특정 프로세스가 실행되었는지 assertion합니다.

use Illuminate\Support\Facades\Process;

Process::assertRan('ls -la');

assertRan 메서드는 클로저도 받을 수 있습니다. 이때 클로저에는 프로세스 인스턴스와 결과 인스턴스가 전달되어 옵션을 직접 확인할 수 있습니다. 클로저가 true를 반환하면 assertion이 통과합니다.

Process::assertRan(fn ($process, $result) =>
$process->command === 'ls -la' &&
$process->path === __DIR__ &&
$process->timeout === 60
);

이때 $processIlluminate\Process\PendingProcess의 인스턴스이고, $resultIlluminate\Contracts\Process\ProcessResult의 인스턴스입니다.

assertDidntRun

특정 프로세스가 실행되지 않았는지 assertion합니다.

use Illuminate\Support\Facades\Process;

Process::assertDidntRun('ls -la');

assertRan 메서드처럼, assertDidntRun에도 클로저를 전달할 수 있습니다. 이 경우 클로저가 true를 반환한다면 assertion이 실패합니다.

Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
$process->command === 'ls -la'
);

assertRanTimes

특정 프로세스가 지정한 횟수만큼 실행되었는지 assertion합니다.

use Illuminate\Support\Facades\Process;

Process::assertRanTimes('ls -la', times: 3);

assertRanTimes 메서드 역시 클로저를 인자로 받을 수 있습니다. 이때 클로저가 true를 반환하고, 프로세스가 지정한 횟수만큼 실행되었을 때에만 assertion이 통과합니다.

Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'ls -la';
}, times: 3);

의도치 않은 프로세스 실행 방지

개별 테스트나 전체 테스트 스위트에서 모든 실행된 프로세스가 가짜(fake) 결과만 반환하도록 보장하고 싶다면, preventStrayProcesses 메서드를 사용할 수 있습니다. 이 메서드를 호출한 후에는, 가짜 결과가 지정되지 않은 프로세스를 실행하려고 할 때 예외가 발생하게 되어, 실제 프로세스가 실행되지 않습니다.

use Illuminate\Support\Facades\Process;

Process::preventStrayProcesses();

Process::fake([
'ls *' => 'Test output...',
]);

// 가짜 결과가 반환됨...
Process::run('ls -la');

// 예외가 발생함...
Process::run('bash import.sh');