본문으로 건너뛰기
버전: 12.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 메서드를 이용하여 프로세스의 "표준 입력"으로 데이터를 전달할 수 있습니다.

$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 메서드를 사용하면 프로세스의 입력과 출력을 프로그램의 입출력과 직접적으로 연결할 수 있습니다. 이를 통해 vim, nano 등 에디터를 프로세스로 열 수도 있습니다.

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

프로세스 출력

앞서 설명한 것처럼, 프로세스 실행 결과는 output(표준 출력)과 errorOutput(표준 에러) 메서드로 확인할 수 있습니다.

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');

파이프라인

때로는 한 프로세스의 출력을 다른 프로세스의 입력으로 넘겨주고 싶을 때가 있습니다. 이런 동작을 "파이핑"이라고 하며, 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"',
]);

파이프라인 실행 도중, 실시간으로 출력을 받고 싶다면 두 번째 인수로 클로저를 전달할 수 있습니다. 이 클로저는 "타입"(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 및 시그널

실행 중인 프로세스의 운영체제 PID(Process 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);
}

run 메서드와 마찬가지로, 비동기 프로세스도 start 메서드의 두 번째 인자로 클로저를 전달하면 실시간으로 출력을 받아올 수 있습니다.

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

$result = $process->wait();

프로세스가 끝날 때까지 기다리는 대신, waitUntil 메서드를 사용하면 출력값을 기준으로 기다림을 중단할 수도 있습니다. 전달한 클로저가 true를 반환하면 라라벨이 추가 출력을 기다리지 않습니다.

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

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

비동기 프로세스 타임아웃

비동기 프로세스가 실행 중일 때, ensureNotTimedOut 메서드를 사용해 타임아웃이 발생하지 않았는지 확인할 수 있습니다. 만약 프로세스가 타임아웃되었다면 타임아웃 예외가 발생합니다.

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

while ($process->running()) {
$process->ensureNotTimedOut();

// ...

sleep(1);
}

동시성 프로세스

라라벨에서는 복수의 비동기 프로세스 풀을 간편하게 관리할 수 있어 여러 작업을 동시에 실행하는 것도 매우 쉽습니다. 먼저, 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 메서드로 각각의 결과를 받아올 수 있습니다. wait 메서드는 배열로 접근 가능한 객체를 반환하며, 각 프로세스의 키를 통해 해당 결과 인스턴스에 접근할 수 있습니다.

$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();

풀 프로세스 이름 지정

숫자 키로 풀의 결과에 접근하는 것은 직관적이지 않을 수 있습니다. 라라벨에서는 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();

풀 프로세스 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 파사드는 배열을 통해 프로세스별로 다른 페이크 결과를 지정할 수도 있습니다.

이 배열의 키는 실제로 페이크하고자 하는 명령 패턴이 되고, 값은 해당 명령어가 실행될 때 반환할 페이크 결과입니다. * 문자(와일드카드)도 사용할 수 있습니다. 배열에 지정되지 않은 프로세스 명령은 실제로 실행됩니다. 각각의 페이크 결과는 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는 마지막 종료 코드를, iterationsrunning 메서드가 몇 번 true를 반환할지를 의미합니다.

사용 가능한 assertion

앞서 설명한 것처럼, 라라벨에서는 테스트에서 사용할 수 있는 다양한 프로세스 assertion을 제공합니다. 아래에서 각 assertion의 사용법을 살펴봅니다.

assertRan

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

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

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

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

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

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);

불필요한 프로세스 실행 방지

모든 프로세스 실행이 반드시 페이크되어야 한다는 것을 확인하고 싶으면, preventStrayProcesses 메서드를 사용하면 됩니다. 이 메서드를 호출한 이후에는, 배열에 지정되지 않은 프로세스가 실행되면 실제 프로세스가 실행되지 않고 대신 예외가 발생합니다.

use Illuminate\Support\Facades\Process;

Process::preventStrayProcesses();

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

// 페이크 결과 반환...
Process::run('ls -la');

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