PHP 코드를 작성하여 배포한 직후 런타임 환경에서 TypeError나 Call to a member function on null 에러가 발생해 급하게 핫픽스를 배포했던 경험이 있으신가요? 단순한 문법 검사(Lint)만으로는 실제 코드가 실행되는 시점에 발생하는 타입 불일치나 잠재적인 비즈니스 로직 버그를 미리 잡아내기 어렵습니다. 다만 대부분의 백엔드 개발자들은 테스트 코드를 매번 작성할 시간이 부족하다는 이유로 배포 전 수동 테스트에만 의존하곤 합니다. 이번 글에서는 PHPStan이 정확히 무엇인지, 서비스 환경에 왜 꼭 필요한지, 그리고 기존 프로젝트에 어떻게 단계적으로 도입하고 실무에 적용하는지 완벽하게 정리해서 소개하겠습니다.

 

1. PHPStan 기초 이해 - 정적 분석이란 무엇인가?

PHPStan은 코드를 직접 실행하지 않고 소스 코드의 구조와 타입 정보를 분석하여 잠재적 에러를 찾아내는 정적 분석(Static Analysis) 도구입니다. PHP는 대표적인 동적 타입 언어이기 때문에 변수의 타입이 실행 시점에 결정됩니다. 이로 인해 개발자가 코드를 작성하는 동안에는 타입 오탈자나 Null 참조 오류를 인지하기 어렵습니다.

단위 테스트(Unit Test)가 특정 입력값에 대한 실행 결과를 검증하는 방식이라면, 정적 분석은 실행 가능한 모든 코드 경로를 전수 조사합니다. 수백 개의 테스트 케이스를 직접 구현하지 않더라도, 함수에 잘못된 인자 타입이 전달되거나 존재하지 않는 메서드가 호출되는 상황을 코딩 단계에서 미리 발견할 수 있습니다.

 

2. PHPStan 설치 및 분석 레벨 설정하기

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부터 시작하여 단계적으로 레벨을 올려나가는 것이 현실적입니다.

 

3. 실전 예제 - 런타임 에러를 사전에 포착하는 코드 검증

실무에서 자주 발생하는 Null 참조 에러와 타입 불일치 상황을 예시로 살펴보겠습니다. 아래는 사용자 객체를 받아 이메일 수신 거부 상태를 업데이트하는 코드입니다.

 

✗ 잘못된 코드 (런타임 TypeError 발생 가능성)

아래 코드는 데이터베이스 조회 결과가 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);
    }
}

 

✓ 올바른 코드 (PHPStan 검사를 통과하는 안전한 코드)

매개변수와 반환값의 타입을 명확히 지정하고, 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);
    }
}

 

PHPStan 실행 결과 확인

분석 명령어를 실행하면 분석 대상 파일과 발견된 오류의 위치, 원인이 명확히 표시됩니다.

./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

 

4. 주의사항 및 흔한 실수

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 활용 팁

이미 수천 줄의 레거시 코드가 존재하는 경우 아래 명령어로 Baseline 파일을 생성하면 기존 에러를 모두 예외 목록으로 등록할 수 있습니다.

./vendor/bin/phpstan analyse --generate-baseline

이렇게 하면 phpstan-baseline.neon 파일이 생성되며, 기존 에러는 무시하고 앞으로 새로 작성하거나 수정하는 코드에 대해서만 높은 엄격도의 타입 검사를 강제할 수 있습니다.

 

5. 마무리 및 정리가이드

PHPStan은 개발자가 코드 작성 중 놓치기 쉬운 타입 오류와 런타임 버그를 미리 차단해 주는 가장 강력한 품질 관리 도구입니다. 배포 후 서비스 장애를 복구하는 데 드는 시간과 비용을 고려할 때, 정적 분석 도구를 도입하는 작은 습관이 모여서 서비스의 높은 안정성을 만든다는 점을 잊지 말자. 이 글의 설치 가이드와 Baseline 설정 방식을 참고해 현재 프로젝트에 PHPStan을 즉시 도입해 보면, 런타임 에러 없는 견고한 백엔드 시스템을 얻을 수 있을 것이다.