Laravel 13의 Process facade를 사용해 외부 명령의 인수, 타임아웃, 실패 예외, 테스트에서의 fake와 배열 명령의 대조를 설명합니다.
페이지 듣기약 6분
패키지가 ffmpeg나 git 같은 외부 명령을 호출하면, 이용하는 애플리케이션의 환경 차이나 실패가 그대로 패키지의 동작이 됩니다. shell_exec()나 exec()를 직접 호출하지 말고 Process facade로 실행하며, 테스트에서는 Process::fake()로 대체합니다.
이 페이지는 Laravel 13을 대상으로 Framework v13.42.0의 구현을 확인하고 있습니다. 프로세스와 패키지 테스트를 전제로 합니다. 여기서 다루는 배열 명령의 대조는 Process 내부에서 사용되는 Symfony Process의 인수 변환을 전제로 합니다.
직접 실행을 피하는 이유
직접 함수를 호출하면 테스트에서 대체할 수 없고, 타임아웃이나 종료 코드의 처리도 호출하는 쪽마다 제각각이 됩니다. Process facade를 거치면 실행 기록, fake, 타임아웃, 실패 시 예외를 통일할 수 있습니다.
배열로 명령을 전달하기
문자열 명령은 sh의 해석을 거치므로, 사용자 입력을 포함하면 따옴표 문제나 주입의 여지가 생깁니다. 패키지 내부에서는 배열로 인수를 전달합니다.
<?phpnamespace Vendor\Package\Support;use Illuminate\Support\Facades\Process;final class VideoConverter{ public function toMp4(string $source, string $target): void { Process::timeout(120) ->run(['ffmpeg', '-y', '-i', $source, $target]) ->throw(); }}
PendingProcess의 기본 타임아웃은 60초, 유휴 타임아웃은 null입니다. 오래 걸리는 변환에서는 timeout()으로 명시하고, 출력이 멈추는 종류의 처리에서는 idleTimeout()을 함께 사용합니다.
실패 시 예외와 메시지
ProcessResult::throw()는 종료 코드가 성공이 아닌 경우 ProcessFailedException을 던집니다. 메시지에는 실행한 명령, 종료 코드, 표준 출력과 표준 에러 출력이 포함됩니다.
명령의 인수에 비밀 정보를 넣으면 ProcessFailedException의 메시지에 포함되어 로그나 화면에 나올 수 있습니다. 토큰 등은 인수로 전달하지 말고 env()로 전달하거나 명령의 설정 파일에 쓰는 방식을 선택합니다.
타임아웃은 ProcessTimedOutException, 유휴 타임아웃은 ProcessIdleTimedOutException으로 표현됩니다. 패키지 이용자에게는 어떤 예외를 던지는지, 잡았을 때 무엇을 기대할 수 있는지를 명시합니다.
테스트에서 fake하기
테스트에서는 Process::fake()로 외부 명령을 대체합니다. Process::result()는 표준 출력, 표준 에러, 종료 코드를 지정할 수 있습니다.
<?phpnamespace Vendor\Package\Tests\Feature;use Illuminate\Contracts\Process\ProcessResult;use Illuminate\Process\Exceptions\ProcessFailedException;use Illuminate\Process\PendingProcess;use Illuminate\Support\Facades\Process;use Vendor\Package\Support\VideoConverter;use Vendor\Package\Tests\TestCase;class VideoConverterTest extends TestCase{ public function test_it_runs_ffmpeg_with_the_expected_arguments(): void { Process::fake([ '*' => Process::result(output: 'converted'), ]); (new VideoConverter)->toMp4('/tmp/in.mov', '/tmp/out.mp4'); Process::assertRan( fn (PendingProcess $process, ProcessResult $result) => $process->command === ['ffmpeg', '-y', '-i', '/tmp/in.mov', '/tmp/out.mp4'] ); } public function test_it_fails_when_ffmpeg_exits_with_an_error(): void { Process::fake([ '*' => Process::result(errorOutput: 'Invalid data', exitCode: 1), ]); $this->expectException(ProcessFailedException::class); (new VideoConverter)->toMp4('/tmp/in.mov', '/tmp/out.mp4'); }}
assertRan()에 클로저를 전달하면 PendingProcess의 command, timeout, environment 등을 확인할 수 있습니다. 배열 명령은 $process->command와 완전 일치로 비교되므로, 인수의 순서나 값의 변경은 테스트에서 검출됩니다.
배열 명령의 fake 키
Process 내부에서는 fake()의 키를 getCommandline()의 결과와 Str::is()로 대조합니다. 배열 명령은 셸용으로 따옴표가 붙은 문자열로 변환되므로, 'ffmpeg*' 같은 문자열 패턴은 일치하지 않습니다. 배열 명령의 테스트에서는 다음 중 하나를 사용합니다.
방법
사용처
주의점
'*' 키
명령에 관계없이 같은 결과를 반환
다른 명령이 호출되어도 알아차리기 어려움
Process::assertRan()
호출 후에 인수를 검증
fake의 응답은 별도로 준비
클로저 fake
인수에 따라 결과를 변경
처리가 복잡해지므로 테스트의 목적을 좁힘
예기치 않은 외부 명령 방지하기
테스트에서 실행되어서는 안 되는 외부 명령을 검출하려면 preventStrayProcesses()를 사용합니다. fake와 일치하지 않는 명령은 RuntimeException이 되며, 실제로는 실행되지 않습니다.
protected function setUp(): void{ parent::setUp(); Process::preventStrayProcesses();}
주입된 Factory를 사용하는 경우
패키지의 생성자에서 Illuminate\Process\Factory를 받는 설계에서는 Process::fake()의 상태가 주입 대상까지 도달하는지 확인합니다. Facade는 해석된 인스턴스를 캐시하고, swap()으로 컨테이너의 인스턴스를 대체합니다. 한편 Factory가 컨테이너에서 공유되는지는 프레임워크의 명시적인 등록에 의존할 수 없으므로, 테스트에서 실제로 확인합니다.주입 대상까지 도달하지 않는 경우에는 테스트에서 Factory를 명시적으로 대체하거나, Process facade를 경유하는 설계로 합니다. 어느 경우든 assertRan()으로 호출이 기록된 것을 확인하면 대체 누락을 검출할 수 있습니다.