PHP 코드를 작성하여 배포한 직후 런타임 환경에서 TypeError나 Call to a member function on null 에러가 발생해 급하게 핫픽스를 배포했던 경험이 있으신가요? 단순한 문법 검사(Lint)만으로는 실제 코드가 실행되는 시점에 발생하는 타입 불일치나 잠재적인 비즈니스 로직 버그를 미리 잡아내기 어렵습니다. 다만 대부분의 백엔드 개발자들은 테스트 코드를 매번 작성할 시간이 부족하다는 이유로 배포 전 수동 테스트에만 의존하곤 합니다. 이번 글에서는 PHPStan이 정확히 무엇인지, 서비스 환경에 왜 꼭 필요한지, 그리고 기존 프로젝트에 어떻게 단계적으로 도입하고 실무에 적용하는지 완벽하게 정리해서 소개하겠습니다.
PHPStan은 코드를 직접 실행하지 않고 소스 코드의 구조와 타입 정보를 분석하여 잠재적 에러를 찾아내는 정적 분석(Static Analysis) 도구입니다. PHP는 대표적인 동적 타입 언어이기 때문에 변수의 타입이 실행 시점에 결정됩니다. 이로 인해 개발자가 코드를 작성하는 동안에는 타입 오탈자나 Null 참조 오류를 인지하기 어렵습니다.
단위 테스트(Unit Test)가 특정 입력값에 대한 실행 결과를 검증하는 방식이라면, 정적 분석은 실행 가능한 모든 코드 경로를 전수 조사합니다. 수백 개의 테스트 케이스를 직접 구현하지 않더라도, 함수에 잘못된 인자 타입이 전달되거나 존재하지 않는 메서드가 호출되는 상황을 코딩 단계에서 미리 발견할 수 있습니다.
PHPStan은 Composer를 통해 개발 의존성 패키지로 간편하게 설치할 수 있습니다. 프로젝트 루트 디렉터리에서 아래 명령어를 실행하면 필요한 모든 준비가 완료됩니다.
composer require --dev phpstan/phpstan설치가 완료되면 프로젝트 루트 경로에 phpstan.neon 설정 파일을 생성합니다. PHPStan은 0부터 9까지 총 10단계의 분석 레벨(Rule Level)을 제공합니다. 숫자가 높아질수록 검사 규칙이 엄격해집니다.
- Level 0: 기본적인 문법 오류, 존재하지 않는 클래스/메서드 호출, 변수 미정의 검사
- Level 3: 반환 타입 및 매개변수 타입 일치 여부 검사
- Level 5: 인자 타입의 정밀 검사 및 불필요한 조건문 검사
- Level 8: Null 가능성(Nullable) 처리 및 유니온 타입 완벽 검사
- Level 9: mixed 타입에 대한 엄격한 제한
신규 프로젝트가 아닌 기존 레거시 프로젝트에 도입하는 경우에는 Level 0 또는 Level 1부터 시작하여 단계적으로 레벨을 올려나가는 것이 현실적입니다.
실무에서 자주 발생하는 Null 참조 에러와 타입 불일치 상황을 예시로 살펴보겠습니다. 아래는 사용자 객체를 받아 이메일 수신 거부 상태를 업데이트하는 코드입니다.
아래 코드는 데이터베이스 조회 결과가 null일 수 있는 상황을 고려하지 않았으며, PHPDoc이나 Type Hint가 부실하여 PHPStan 분석 시 에러가 감지됩니다.
<?php
namespace App\Services;
use App\Models\User;
class UserService
{
public function unsubscribeUser($userId)
{
$user = User::find($userId); // null 반환 가능
// $user가 null인 경우 Fatal Error 발생
return $user->setEmailOptIn(false);
}
}
매개변수와 반환값의 타입을 명확히 지정하고, null 반환 가능성을 안전하게 가드 클로즈(Guard Clause)로 처리했습니다.
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\User;
class UserService
{
public function unsubscribeUser(int $userId): bool
{
$user = User::find($userId);
if ($user === null) {
return false;
}
return $user->setEmailOptIn(false);
}
}
분석 명령어를 실행하면 분석 대상 파일과 발견된 오류의 위치, 원인이 명확히 표시됩니다.
./vendor/bin/phpstan analyse src --level=5------ ------------------------------------------------------------------
Line Services/UserService.php
------ ------------------------------------------------------------------
13 Cannot call method setEmailOptIn() on App\Models\User|null.
13 Method App\Services\UserService::unsubscribeUser() has no return
type specified.
------ ------------------------------------------------------------------
[ERROR] Found 2 errors
PHPStan을 도입할 때 개발팀이 자주 범하는 실수들을 비교해 두었습니다. 올바른 접근법을 적용하여 코드 품질 향상 효과를 극대화해야 합니다.
| 구분 | ✗ 잘못된 방식 | ✓ 올바른 방식 |
|---|---|---|
| 레벨 설정 | 기존 레거시 프로젝트에 처음부터 Level 8/9를 적용하여 수백 개의 오류 폭탄을 맞고 도구 도입 포기 | Level 0부터 시작하여 기존 오류를 해결하거나, Baseline 기능을 활용하여 신규 코드부터 엄격하게 검사 |
| 타입 지정 | 배열 내부에 들어가는 객체 타입을 지정하지 않고 단순 array로 선언하여 내부 타입 검사 우회 | PHPDoc의 @var array<int, User> 구문을 활용하여 배열 내부 요소의 타입까지 정밀하게 명시 |
| CI/CD 연동 | 개발자의 로컬 환경 수동 실행에만 의존하여 정적 분석을 누락한 채 Git push 및 배포 진행 | GitHub Actions, GitLab CI 등 CI 파이프라인에 PHPStan 검사 단계를 추가하여 에러 발생 시 PR 합병 차단 |
이미 수천 줄의 레거시 코드가 존재하는 경우 아래 명령어로 Baseline 파일을 생성하면 기존 에러를 모두 예외 목록으로 등록할 수 있습니다.
./vendor/bin/phpstan analyse --generate-baseline이렇게 하면 phpstan-baseline.neon 파일이 생성되며, 기존 에러는 무시하고 앞으로 새로 작성하거나 수정하는 코드에 대해서만 높은 엄격도의 타입 검사를 강제할 수 있습니다.
PHPStan은 개발자가 코드 작성 중 놓치기 쉬운 타입 오류와 런타임 버그를 미리 차단해 주는 가장 강력한 품질 관리 도구입니다. 배포 후 서비스 장애를 복구하는 데 드는 시간과 비용을 고려할 때, 정적 분석 도구를 도입하는 작은 습관이 모여서 서비스의 높은 안정성을 만든다는 점을 잊지 말자. 이 글의 설치 가이드와 Baseline 설정 방식을 참고해 현재 프로젝트에 PHPStan을 즉시 도입해 보면, 런타임 에러 없는 견고한 백엔드 시스템을 얻을 수 있을 것이다.