대규모 PHP 프로젝트에서 테스트 실행 시간은 개발 생산성에 직접적인 영향을 미칩니다. PestPHP의 병렬 테스트 기능을 활용하면 실행 시간을 획기적으로 단축할 수 있지만, 여러 워커 프로세스가 독립적으로 실행되면서 코드 커버리지 데이터가 누락되거나 리포트가 손상되는 문제가 빈번하게 발생합니다. 이러한 '속도와 정확성' 사이의 트레이드오프를 해결하기 위한 기술적 배경과 세 가지 최적화 방안을 살펴봅니다.
병렬 테스트의 아키텍처적 제약
PestPHP의 병렬 테스트는 src/Plugins/Parallel.php를 통해 구현되며, 내부적으로 ParaTest 라이브러리를 사용하여 테스트 스위트를 여러 워커 프로세스로 분할합니다. 이 과정에서 다음과 같은 기술적 충돌이 발생합니다.
- 데이터 단편화: 각 워커가 생성한 커버리지 데이터가 개별적으로 존재하여 자동 통합이 되지 않음
- 리소스 경합: 동일한 리포트 파일에 여러 프로세스가 동시 쓰기를 시도할 경우 데이터 유실 발생
- 인자 호환성: 병렬 모드에서는
--todo,--retry등의 특정 옵션 사용이 제한됨
// 병렬 모드와 호환되지 않는 주요 인자 예시
private const array INCOMPATIBLE_OPTS = [
'--retry',
'--todo',
'--notes',
'--pull-request'
];
솔루션 1: 사후 처리 병합 방식 (Post-processing Merge)
가장 안정적인 방법으로, 각 워커가 독립적인 커버리지 파일을 생성하게 한 뒤 메인 프로세스 종료 시 이를 하나로 합치는 방식입니다.
- 각 워커별로 고유한 이름의
.cov파일 생성 - 테스트 완료 후
php-code-coverage라이브러리를 사용해 병합
# 1. 병렬 테스트 실행 및 워커별 커버리지 저장
vendor/bin/pest --parallel --coverage-php ./build/coverage/result_{$ID}.cov
# 2. PHP 스크립트를 이용한 데이터 통합
php -r "
require 'vendor/autoload.php';
\$cov = new \SebastianBergmann\CodeCoverage\CodeCoverage;
foreach (glob('./build/coverage/*.cov') as \$file) {
\$cov->merge(include \$file);
}
(new \SebastianBergmann\CodeCoverage\Report\Html\Facade)->process(\$cov, './build/html-report');
"
이 방식은 모든 환경에서 호환되지만, 중간 파일 저장을 위한 디스크 I/O 비용이 발생한다는 점을 고려해야 합니다.
솔루션 2: 전역 변수를 활용한 메모리 공유 방식
PestPHP의 글로벌 변수 공유 메커니즘을 사용하여 워커의 데이터를 실시간으로 수집할 수 있습니다. Parallel::setGlobal 메서드를 활용하여 데이터를 전송합니다.
// 워커 프로세스에서 데이터 전송
$currentCoverage = $tester->getRawCoverageData();
Parallel::setGlobal('partial_cov_data', serialize($currentCoverage));
// 메인 프로세스에서 수집 및 집계
$aggregatedResults = [];
foreach (Parallel::getWorkerPool() as $worker) {
$data = Parallel::getGlobal('partial_cov_data', $worker);
if ($data) {
$aggregatedResults[] = unserialize($data);
}
}
이 방식은 I/O 비용을 줄여주지만, 테스트 케이스가 수천 개에 달할 경우 공유 메모리 부족으로 인한 세그멘테이션 오류가 발생할 수 있으므로 중소규모 프로젝트에 적합합니다.
솔루션 3: 분산 추적 모드 (Xdebug 원격 수집)
Xdebug의 커버리지 모드와 외부 수집 서버를 연동하는 고급 설정입니다. 파일 시스템에 의존하지 않고 네트워크를 통해 데이터를 즉시 전송합니다.
; php.ini 설정 예시
xdebug.mode=coverage
xdebug.client_host=127.0.0.1
xdebug.client_port=9005
xdebug.start_with_request=yes
실시간으로 데이터를 중앙 집중화할 수 있지만, 네트워크 오버헤드와 별도의 수집 서버 구축이 필요하다는 비용적 측면이 존재합니다.
성능 벤치마크 및 권장 설정
다음은 8코어 CPU 환경에서 500개의 유닛 테스트를 수행했을 때의 성능 지표입니다.
| 실행 방식 | 소요 시간 | 커버리지 정확도 |
|---|---|---|
| 단일 스레드 (기본) | 185초 | 100% |
| 병렬 실행 (최적화 없음) | 32초 | 유실 발생 (약 40%) |
| 병렬 + 사후 병합 (솔루션 1) | 39초 | 100% |
| 병렬 + 메모리 공유 (솔루션 2) | 35초 | 100% |
운영 시 주의사항
병렬 테스트 환경에서 커버리지 리포트가 0%로 나오거나 특정 파일이 누락된다면 phpunit.xml의 경로 설정을 확인해야 합니다. 특히 절대 경로를 사용하지 않을 경우 워커 프로세스의 작업 디렉토리 인식 오류로 인해 파일 매핑이 실패할 수 있습니다.
<!-- phpunit.xml 권장 설정 -->
<coverage cacheDirectory="./.phpunit.cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
메모리 부족 문제가 발생할 경우 PHP_MEMORY_LIMIT 환경 변수를 통해 워커당 할당 메모리를 증설하거나, --processes 옵션을 통해 CPU 코어 수보다 약간 적은 수의 워커를 할당하여 시스템 안정성을 확보하는 것이 좋습니다.