PHP 레거시의 위험은 포인터가 아니라 노후한 실행 환경, 암묵적인 타입 변환, 권한 검사, 그리고 DB와 외부 연동에 숨어 있다. 대부분의 경우 정답은 PHP를 버리는 것이 아니라, PHP를 지원되는 버전으로 올리고 검증과 배포 체계를 먼저 갖추는 것이다.

정도현 - 로보코 수석 컨설턴트


1편에서는 20년 넘은 C/C++ 애플리케이션을 바이브 코딩으로 현대화하는 방법을 다뤘다.1 이번 글은 레거시 웹, 그중에서도 PHP를 다룬다.

PHP는 1995년에 등장해 웹의 역사와 거의 같은 시간을 함께 지나왔다. 그 사이 수많은 회사 홈페이지, 쇼핑몰, 커뮤니티, 사내 업무 시스템이 PHP로 만들어졌고, 그중 상당수는 지금도 운영 중이다. 국내만 해도 그누보드와 영카트로 만든 사이트, 직접 만든 PHP5 관리자 화면, 오래된 Laravel과 CodeIgniter 프로젝트를 컨설팅 현장에서 자주 만난다. 이런 프로젝트에서도 “에이전트가 일을 제대로 못 한다"는 이야기를 듣는다.

1편의 결론은 PHP에도 그대로 적용된다. 에이전트가 실패하는 주된 이유는 자기가 한 변경이 맞는지 확인할 방법이 없기 때문이며, 그래서 현대화의 첫 투자는 검증 환경이어야 한다. 그러나 무엇을 검증해야 하는지는 C/C++와 다르다. 이 글은 2026년 10월 16일 기준으로 조사한 공개 사례·공식 지원 정책·도구 문서를 바탕으로 PHP에 맞는 현대화 전략을 정리한다. 사례의 수치는 모두 발표 주체의 자체 보고이며, 내가 독립적으로 재현한 것은 아니다.

시리즈: 바이브 코딩으로 레거시 현대화하기

  1. 20년 된 C/C++ 애플리케이션
  2. 웹의 역사와 함께 쌓인 PHP (이 글)

TL;DR

  • PHP 레거시의 주요 위험은 포인터 수명이 아니라 지원이 끝난 실행 환경, 동적 타입과 암묵적 동작, 전역 상태, 인증·인가, DB와 외부 연동, 테스트와 배포 체계의 부재다.
  • 대부분의 프로젝트에는 PHP를 유지하면서 런타임과 의존성을 지원 버전으로 올리고, 검증·배포 체계를 만든 뒤, 필요한 부분만 재구성하는 방식이 적합하다.
  • 규칙이 명확한 대량 변환은 Rector 같은 결정적 도구에 맡기고, 에이전트는 조사·잔여 예외 수정·테스트 작성을 맡는다. 사람은 업무 동작·권한·데이터·전환 위험을 판단한다.
  • 웹 프로젝트의 동등성은 화면이 뜨는 것으로 확인되지 않는다. HTTP 요청, 사용자 권한, 세션, DB 변경, 메일·결제 같은 외부 효과를 한 시나리오로 비교해야 한다.
  • 공개 사례에서 에이전트는 권한 검사 누락(IDOR), 메일 발송 누락, +1 같은 상수 보정, DB 컬럼을 nullable로 바꾸는 우회를 실제로 만들었다. 이런 실패를 잡는 검증 기준을 먼저 세워야 한다.

1. PHP 레거시는 C/C++ 레거시와 무엇이 다른가

C/C++ 레거시의 핵심 난제는 포인터의 소유권과 객체 수명이었다. PHP는 요청이 끝나면 메모리가 정리되는 실행 모델이라 이 문제가 거의 없다. 대신 다른 곳에서 문제가 생긴다.

구분 C/C++ 레거시 PHP 레거시
핵심 위험 포인터 소유권, 객체 수명, 메모리 결함 실행 환경 노후화, 암묵적 타입 변환, 권한, DB·외부 부작용
사실상의 명세 원본 바이너리의 동작과 ABI 운영 중인 서버의 코드, DB 스키마와 데이터, URL
동등성 비교 단위 함수·라이브러리의 입출력 HTTP 요청, 사용자 역할, 세션, DB 변경, 외부 효과
대표 결정적 도구 clang-tidy, LibTooling, C2Rust Rector, PHPStan, PHPCompatibility, Composer
흔한 잘못된 목표 전체를 Rust로 번역 전체를 최신 프레임워크와 SPA로 재작성

내가 현장에서 본 PHP 레거시에서 에이전트의 작업 루프가 끊기는 지점은 대략 네 곳이다.

첫째, 저장소의 코드가 운영 코드와 다르다. FTP로 서버에서 직접 수정한 파일, 벤더나 CMS 코어를 직접 고친 부분, Git에 없는 설정 파일이 흔하다. 에이전트가 저장소만 읽고 작업하면 실제로 운영되지 않는 코드를 기준으로 판단하게 된다.

둘째, 실제 실행 환경을 재현하기 어렵다. 웹 서버의 PHP와 cron이 쓰는 CLI PHP의 버전이나 php.ini가 다를 수 있다. 지원이 끝난 PHP 5.x와 오래된 확장 모듈, EUC-KR 같은 문자 인코딩, 특정 MySQL 설정에 동작이 의존하는 경우도 많다.

셋째, 코드 검색만으로는 진입점을 다 찾을 수 없다. URL로 직접 호출되는 PHP 파일, 동적 include, eval, DB에 저장된 SQL이나 규칙이 있으면 심벌 검색으로는 호출 관계가 드러나지 않는다. 그래서 접근 로그와 cron 스케줄을 함께 확인해야 한다.

넷째, DB와 외부 연동이 사실상의 명세다. ORM 모델 클래스가 실제 DB 스키마와 다른 경우가 많다. 실제 DDL, 인덱스, trigger, 데이터 분포가 업무 규칙을 담고 있다. 메일·SMS·결제(PG) 연동은 테스트 중에 실제로 실행되면 되돌릴 수 없다.

2. 공개 사례가 보여주는 것

PHP 현대화 사례는 C/C++보다 많다. 다만 단순한 AI 보조 코딩, 도구를 직접 실행하는 에이전트, 규칙 기반 자동 변환이 섞여 있으므로 구분해서 읽어야 한다.

사례 원본과 목표 도구 발표된 결과 적용 한계
DVHARDWARE2 2002년부터 쌓인 절차형 PHP CMS → PHP 8.5와 계층 구조 Codex, Docker, PHPStan, Deptrac 초기 기능 대응 버전을 3일 저녁 작업으로 확보, 이후 개선을 이어가 배포 비교적 단순한 CRUD CMS이고, 기능 동등성은 운영자 자체 판단
Progrit3 7개 저장소, PHP 8.2→8.4, Laravel 10→12 Claude Code, Rector 일반 개발과 병행해 큰 문제 없이 완료했다고 보고 기간·비용·자동화율 미공개
Rocket4 PHP 화면·로직 → Angular 중심 구조로의 잔여 전환 Claude Code 교육 포함 4개월, 선임 엔지니어 작업 처리량 82건→201건 조직 변화가 함께 반영된 수치이며, PHP 서버 전체 교체의 근거는 아님
Boldare5 Symfony 5.4→6.4 Claude 수일로 예상한 작업을 수시간에 수행 포괄적인 기존 테스트가 있었음, 프로젝트 규모 미공개
2am.tech6 오래된 Laravel 앱 → 새 Laravel API와 React GitHub Copilot 에이전트 수직 기능 단위로 이식하며 권한·메일·상태 결함을 발견·수정 기간과 생산성 수치 미공개
FuelPHP→Laravel7 40만 줄 이상, 2천 개 이상 파일 사용자 정의 Rector 규칙 약 80개 PHP 파일의 99%를 자동 변환 2021년 작업으로 LLM 사례가 아님, 약 11개월에 걸친 QA와 canary 필요

이 표에서 가장 배울 것이 많은 부분은 성과 수치가 아니라 공개된 실패다.

DVHARDWARE의 운영자는 PHP-Nuke에서 출발해 24년간 수정된 사이트를 현대화했다. 먼저 코드를 Git에 넣고, Docker와 DB 복사본으로 로컬 실행 환경을 만든 뒤, 신구 버전을 병행하며 옮겼다. 그런데 에이전트는 사용자 수 계산이 맞지 않자 원인을 고치는 대신 결과에 +1을 더해 숫자를 맞췄다. 원본의 잘못된 가정과 불필요한 계층을 그대로 옮기기도 했다. 운영자는 생성되는 코드의 양이 많아서 리뷰가 느슨해지는 문제도 보고했다.

2am.tech의 사례는 더 심각한 실패를 공개했다. 메일 템플릿은 옮겼지만 실제 발송 호출이 연결되지 않았다. JWT 흐름에서 빠진 필드를 해결하는 대신 DB 컬럼을 nullable로 바꿔 우회하려 했다. 사용자 ID를 파라미터로 받는 API에서 소유권 검사가 빠져, 다른 사용자의 데이터에 접근할 수 있는 IDOR(Insecure Direct Object Reference) 취약점이 생겼다. 일부 쓰기 API는 일반 사용자에게 열려 있었다.

이 실패들에서 세 가지 교훈을 얻는다. 첫째, UI에서 버튼을 숨기는 것과 서버에서 권한을 검사하는 것은 별개다. 둘째, 화면이 뜨고 HTTP 200이 나와도 메일·DB·이벤트의 연결이 빠질 수 있다. 셋째, ORM 모델에서 유추한 스키마를 실제 DB 스키마로 취급하면 안 된다. 2am.tech 팀도 작업 단위를 DTO·컨트롤러 같은 계층별 작업에서 route→controller→service→repository를 관통하는 수직 기능 단위 로 바꾼 뒤에야 이런 누락을 체계적으로 잡을 수 있었다.

성공 사례에도 공통점이 있다. Boldare는 포괄적인 기존 테스트가 수정 직후마다 피드백을 주었다. Progrit은 공식 변경 문서로 영향 범위와 수정 방침을 먼저 정리하고, 그 내용을 공통 프롬프트로 만들어 7개 저장소에 적용했으며, 단순 반복 작업은 Rector에 맡겼다. 즉 테스트가 이미 있고 변경 범위가 제한된 프로젝트에서는 빠른 성과가 나지만, 테스트 없는 절차형 PHP5 프로젝트에 같은 속도를 기대할 수는 없다.

3. 목표 버전부터 정한다

현대화의 목표 버전은 공식 지원 정책으로 정한다. 아래는 2026년 10월 16일에 PHP 공식 사이트에서 확인한 지원 일정이다.8

PHP 버전 일반 지원 종료 보안 지원 종료 신규 현대화 프로젝트에서의 해석
8.2 2024-12-31 2026-12-31 보안 지원 종료가 임박해 최종 목표로 부적합
8.3 2025-12-31 2027-12-31 제약이 있을 때의 중간 목표
8.4 2026-12-31 2028-12-31 플러그인·확장 호환성에 따라 현실적인 선택
8.5 2027-12-31 2029-12-31 의존성 지원이 확인되면 우선 검토할 목표

프레임워크도 같은 방식으로 판단한다. Symfony 7.4 LTS는 2029년 11월까지 보안 수정을 받는다. Laravel 13은 PHP 8.3-8.5를 지원하고 2028년 3월까지 보안 수정을 받는다. Laravel 12의 보안 수정은 2027년 2월에 끝나므로, 새로 시작하는 프로젝트라면 13 호환성부터 평가하는 것이 낫다.9

“PHP부터 올린다"가 항상 맞는 순서는 아니다. 오래된 프레임워크는 최신 PHP에서 실행되지 않을 수 있기 때문이다. 그래서 현재 프레임워크의 최신 패치 → 지원 PHP 범위가 넓은 중간 프레임워크 버전 → PHP 업그레이드 → 목표 프레임워크 같은 계단을 밟아야 할 수 있다. Composer의 충돌 원인과 공식 업그레이드 가이드로 이 계단을 설계하며, 중간 버전을 모두 운영에 배포할 필요는 없다.

4. 프로젝트 유형별 경로

PHP 레거시는 형태가 다양하므로 유형마다 경로가 다르다. 아래 표는 주요 유형별 기본 경로와 피해야 할 접근이다.

유형 기본 경로 피할 접근
절차형 PHP5와 HTML 혼합 재현·백업·특성화 테스트 → DB 접근과 공통 로직 분리 → PHP 업그레이드 첫 단계에서 Laravel과 SPA로 전체 변환
PHP7 자체 MVC Composer·autoload 도입과 호환성 분석 → 런타임 업그레이드 → 모듈 경계 정리 파일 이동·namespace 변경·DB 교체를 동시에 진행
오래된 Laravel·Symfony 공식 버전별 가이드를 따라 단계 업그레이드, Rector와 에이전트 병행 Composer 버전 숫자만 최신으로 바꾸기
CodeIgniter 2/3, FuelPHP, Zend1 유지·래핑·제품 교체·부분 재구축을 비교 호환되지 않는 메이저 버전 간 단순 업그레이드 약속
WordPress 등 CMS 코어와 확장을 표준 방식으로 갱신하고, 직접 만든 테마·플러그인만 수정 에이전트로 CMS 코어를 직접 수정
그누보드·영카트·XE·Rhymix 정확한 제품·fork·버전과 코어 수정분부터 식별 커뮤니티의 “잘 된다"는 답변을 호환성 보증으로 사용
유지만 필요한 작은 사이트 지원 환경으로 이전, 쓰지 않는 기능 제거, 정적화나 SaaS 대체 검토 업무 가치보다 큰 플랫폼 투자

CodeIgniter 3에서 4로의 전환은 공식 가이드가 하위 호환되지 않는 재작성이라고 명시한다.10 Drupal 7은 2025년 1월 5일에 커뮤니티 보안 지원이 끝났고, 현대 Drupal로의 이전은 코어 파일 교체가 아니라 데이터·콘텐츠 타입·권한·URL의 이관 프로젝트다. 이런 경우는 PHP 문법 변환만으로 완료를 판단하면 안 된다.

국내 구축형 사이트에서는 우선 점검할 항목이 따로 있다. EUC-KR과 CP949 인코딩, 첨부파일의 한글 파일명, Excel 다운로드, 주소 검색, 본인인증·PG·SMS 연동, 웹 에디터의 업로드 경로, 오래된 웹 서버의 rewrite 규칙이다. 그누보드 코어가 최신 PHP에서 동작해도 오래된 skin·plugin·PG 모듈은 동작하지 않을 수 있다.

Laravel이나 Symfony를 이미 쓰는 고객이라면 그 생태계를 유지하는 편이 보통 유리하다. 다른 언어는 특별한 제약이나 운영상 이익이 있을 때 기능 단위로만 선택한다. PHP에서 벗어나는 것 자체를 목표로 삼지 않는다.

5. Rector와 에이전트의 역할을 나눈다

1편에서 결정적 도구와 에이전트 판단을 나누라고 했다. PHP에서는 그 결정적 도구의 중심이 Rector다. Rector는 PHP 코드를 AST(구문 트리) 수준에서 분석해 정해진 규칙대로 변환하는 도구다. 버전 업그레이드용 규칙 묶음과 프레임워크별 규칙을 제공하고, 사용자 정의 규칙도 만들 수 있다.11

FuelPHP에서 Laravel로 옮긴 사례는 이 방식의 위력과 한계를 함께 보여준다. 이 팀은 약 80개의 사용자 정의 Rector 규칙으로 Query Builder, namespace, 설정, request·response 처리를 변환했고, PHP 파일의 99%가 자동 변환되었다고 보고했다. 그러나 라우트와 인증 일부는 수작업으로 처리했다. 또한 빈 응답의 상태 코드나 insert 결과의 형태처럼 프레임워크 사이의 작은 의미 차이가 실제 문제가 되었다. 작업은 2021년 1월부터 11월까지 이어졌고 QA와 canary 배포가 중요했다.7 파일 99% 자동 변환은 작업 시간 99% 절감이나 검증 없는 배포를 뜻하지 않는다.

이 사례는 LLM 이전의 것이지만, 오늘날의 에이전트와 결합하면 역할 분담이 분명해진다. 에이전트는 반복 패턴을 찾아 Rector 규칙의 초안과 테스트 fixture를 작성하고, 규칙이 처리하지 못한 잔여 예외를 해결한다. 확정된 패턴은 실행 가능한 Rector 규칙으로 쌓아 두고, 매번 LLM이 새로 추론하게 하지 않는다. 이렇게 하면 비용이 줄고 결과가 재현 가능해진다. 또한 규칙 기반 변환의 diff와 에이전트 수정의 diff를 분리하면 회귀가 생겼을 때 원인을 좁히기 쉽다.

에이전트에게 제공할 문맥도 정비한다. PrestaShop은 PR 41279에서 .ai 디렉터리를 공통 규칙의 원본으로 두고, CLAUDE.md, AGENTS.md, Cursor·Copilot·Gemini·Windsurf용 진입 파일이 모두 같은 정보를 가리키게 했다.12 이 PR은 문서와 도구 변경이며 제품 전환의 완료 성과는 아니다. 그러나 규칙의 원본은 하나로 두고 여러 에이전트의 진입 파일이 그것을 가리키게 하는 구조는 그대로 가져올 만하다. Laravel 프로젝트라면 route·schema·문서 정보를 에이전트에 제공하는 Laravel Boost를, PhpStorm 사용자라면 IDE의 타입·참조 정보를 외부 에이전트에 제공하는 MCP 서버를 검토할 수 있다.13 다만 Laravel Boost는 Laravel 10 이상과 PHP 8.1 이상에서만 설치할 수 있으므로 PHP5나 오래된 Laravel 프로젝트에 바로 쓸 수 있다고 가정하면 안 된다.

6. 업그레이드에서 의미가 바뀌는 지점

PHP 버전을 올리면 코드가 그대로여도 동작이 바뀌는 곳이 있다. 에이전트는 이런 곳을 문법 오류처럼 “고치려” 하는 경향이 있으므로, 사람이 업무 의미를 먼저 정해야 한다.

위험 예시 확인 방법
제거된 기능 mysql_*, ereg, 오래된 생성자 문법 공식 버전별 migration guide, PHPCompatibility
타입 비교 숫자 문자열·null·false·빈 값, 0과 문자열의 비교 경계값 기반 특성화 테스트
오류 처리 false 반환을 기대하던 코드가 TypeError나 예외를 받음 실패 경로와 전역 error handler 검사
동적 속성 선언하지 않은 객체 필드 실제 필드 계약 확인
문자열과 인코딩 EUC-KR·CP949·UTF-8, 바이트와 문자 길이 DB·폼·파일·다운로드 왕복 테스트
DB 드라이버 예외 모드, fetch 타입, 영향받은 행 수 같은 데이터에서 쿼리 결과와 오류 비교
직렬화 세션·캐시·큐에 저장된 PHP 객체 신구 버전 간 양방향 호환 확인

예를 들어 PHP 8에서는 0 == "foo"의 결과가 이전 버전과 다르다.14 이를 모든 ==를 ===로 바꿔서 해결하면 안 된다. 그 비교가 어떤 입력을 받고 어떤 업무 의미를 갖는지 먼저 정해야 한다. 마찬가지로 PHP 8.2의 동적 속성 경고를 #[AllowDynamicProperties]를 일괄 적용해 숨기지 말고, 실제 필드로 선언할 것과 확장 지점으로 남길 것을 구분한다. PHP 7에서 제거된 mysql 확장을 PDO나 MySQLi로 옮길 때도 함수 이름만 바꾸지 말고 연결 charset, SQL 파라미터 바인딩, 트랜잭션, 영향받은 행 수를 검증한다.

조사 자료를 검증하는 일도 필요하다. Progrit 사례의 원문에는 “Carbon 3가 기본적으로 immutable로 바뀌었다"는 AI 생성 분석이 포함되어 있었다. 그러나 Carbon 공식 문서는 Carbon은 값이 변경되는 객체이고 CarbonImmutable은 새 객체를 반환한다고 구분한다.15 특정 애플리케이션이 immutable을 기본으로 설정할 수는 있지만, 이를 라이브러리 전체의 변경으로 단정하면 안 된다. AI가 작성한 조사 결과는 특히 실행 증거로 확인해야 한다.

7. DB와 외부 부작용을 통제한다

PHP 프로젝트에서는 DB가 사실상의 명세인 경우가 많다. 그래서 데이터를 다루는 기본 규칙을 먼저 정한다. 원본 DB와 업로드 파일의 복구가 가능한지 검증한 다음에 변경을 시작한다. 초기에는 DB 스키마를 가능한 한 유지하고 애플리케이션 호환부터 확보한다. 스키마를 바꿔야 한다면 컬럼 추가 → 데이터 채우기 → 비교 → 소비자 전환 → 구 컬럼 제거의 단계로 나눈다. 그리고 SQLite 기반 단위 테스트만으로 운영 MySQL이나 MariaDB의 동작을 검증했다고 보지 않는다. SQL mode, collation, 스토리지 엔진의 차이가 정렬·중복 키·날짜 처리 결과를 바꾸기 때문이다.

메일·SMS·PG·webhook 연동에는 원칙이 하나 있다. 같은 입력을 두 번 실행해도 실제 외부 효과가 두 번 일어나지 않게 해야 한다. 테스트 어댑터, 샌드박스, 발송 내용 캡처, 멱등성 키(idempotency key)를 사용한다. 신구 버전을 병행 실행할 때 신규 구현은 발송 의도만 기록하거나 쓰기가 차단된 상태로 둔다. 신구 양쪽에 동시에 쓰는 dual write를 기본 해결책으로 삼지 않으며, DB에 쓰는 주체는 하나로 유지한다. cron과 큐 워커도 신구 양쪽에서 동시에 돌지 않게 한다.

에이전트에게는 운영 DB 원본을 그대로 주지 않는다. 필요한 스키마와 비식별·합성 데이터를 만들어 제공한다. Laravel Boost나 MCP 서버처럼 DB 조회·명령 실행 능력이 있는 도구는 격리된 개발·검증 환경에만 연결한다. 도구가 그런 기능을 지원한다는 이유로 운영 DB 접근 권한을 넓게 부여하지 않는다.

8. 웹을 위한 검증 하네스

1편의 C/C++ 차등 실행은 같은 입력에 대한 함수나 라이브러리의 출력을 비교했다. 웹 프로젝트에서는 출력 비교만으로 동등성을 확인할 수 없다. HTTP 요청, 사용자 권한, 세션, DB 변경 전후 상태, 파일, 외부 효과 를 하나의 시나리오로 묶어 비교해야 한다.

비교 실행은 다음 순서로 진행한다. 먼저 같은 기준 데이터에서 구버전과 신버전에 각각 별도의 DB·파일·세션·캐시 공간을 만든다. 시나리오마다 각 시스템에 따로 로그인해 CSRF 토큰과 쿠키를 발급받는다. 같은 의미의 요청을 실행하고, 상태 코드·redirect·응답·DB 변경분·파일·메일 발송 의도를 수집한다. 타임스탬프나 자동 증가 ID처럼 비교에서 제외할 값은 문서로 정해 두고 정규화한다. 마지막으로 차이를 원본 결함·의도한 변경·신규 결함·환경 차이로 분류한다. 두 앱을 같은 DB에 연결한 채 쓰기 시나리오를 재생하면, 원본 실행 결과가 신버전의 초기 상태를 오염시키므로 피해야 한다.

검증 영역 최소 시나리오
비회원·회원·관리자 URL·API 직접 호출, 만료된 세션
사용자·테넌트 격리 다른 사용자 ID·조직 ID·파일 ID로 요청
입력·검색 빈 값, 0, null, 다국어, 최대 길이, SQL 특수문자
쓰기 생성·수정·삭제, 중복 제출, 재시도
외부 효과 메일·SMS·PG·webhook의 누락·중복·잘못된 수신자
파일 업로드·다운로드, 한글 파일명, 접근 제어
날짜·금액 월말, 윤년, 시간대, 반올림
비동기·배치 cron·큐의 timeout과 재실행

사용자·테넌트 격리 시나리오는 2am.tech 사례의 IDOR를 잡기 위한 것이다. Playwright의 화면 스냅샷 비교는 레이아웃 회귀를 찾는 데 유용하지만, 권한·정산·메일 발송을 증명하지는 않는다.16 또한 “테스트 커버리지 80%” 같은 숫자를 단독 성과 지표로 삼지 않는다. 합격 기준은 필수 업무 시나리오 전부 통과, 분류되지 않은 차이 0건, 미해결 권한·데이터 결함 0건, 합의한 성능 예산 충족, 복원 연습 완료처럼 정한다.

에이전트에게 주는 금지 사항에는 공개 사례의 실패를 그대로 반영한다. 테스트 기대값을 완화하거나 skip으로 통과시키지 않는다. nullable 변경, 상수 보정, 권한 완화로 차이를 숨기지 않는다. 승인 없이 인증·인가·DB 스키마를 바꾸지 않는다. 실제 메일·SMS·PG 부작용을 일으키지 않는다. 그리고 실행하지 못한 검사는 “미실행"으로 보고하게 한다.

9. 단계별 절차

PHP 현대화도 1편과 같은 방식으로 단계마다 통과 조건을 둔다. 차이는 G0에서 운영 코드와 데이터를 확보하는 단계가 따로 있고, 마지막에 지속 관리 단계가 붙는다는 점이다.

단계 수행 작업 통과 조건
G0 확보 Git, 운영 코드, DB, 업로드 파일, 설정, 백업 복원 복구 가능한 기준 버전과 접근 권한
G1 재현 구버전과 목표 환경 구성, 외부 연동 stub 대표 URL·CLI·cron을 격리 환경에서 실행
G2 행동 보호 특성화·권한·DB·세션·부작용 테스트 원본 기준선과 holdout 데이터 확보
G3 경로 결정 버전 계단, 패키지 대체, 화면·데이터 설계 결정 목표, 금지 변경, 예산, 롤백 합의
G4 자동 변환 Rector와 전용 도구를 작은 배치로 적용 규칙 fixture, diff, 정적 분석과 테스트 통과
G5 에이전트 수정 잔여 타입·설정·연동·업무 예외 해결 검증 기준을 완화하지 않은 수정과 실행 증거
G6 반증 신구 비교, 권한·데이터·보안·성능 검증 원인이 분류되지 않은 차이와 미해결 차단 결함 없음
G7 전환 배포 artifact, canary, 관측, 복구 연습 세션·데이터 호환과 부작용 통제
G8 지속 관리 의존성 갱신과 지원 종료 대응 담당자, 일정, 정기 검증 확정

G8은 PHP에서 특히 중요하다. PHP와 프레임워크는 매년 새 버전이 나오고 2-3년 주기로 지원이 끝난다. 한 번 최신 버전으로 올리는 것보다 업그레이드 가능한 상태를 계속 유지하는 것 이 더 큰 가치다. 익명 고객을 대상으로 한 Bosun의 파일럿은 이 방향을 보여준다. 어떤 내부 패키지가 외부 패키지 갱신을 막는지는 AI 없이 결정적인 의존성 탐색으로 찾고, 버전 결정·공식 문서 조사·수정·테스트·영향 설명은 에이전트 단계로 묶어 한 번에 메이저 버전 하나씩 작은 PR을 만들었다.17

세션 처리도 전환 단계에서 주의할 부분이다. 세션을 공유할 수 없는 두 버전 사이로 요청을 무작위로 보내면 로그아웃, CSRF 실패, 서명 오류가 생긴다. 사용자 그룹별로 고정 라우팅을 하거나, 계획된 재로그인을 하거나, 버전별 세션 공간을 분리하는 방법 중 업무에 맞는 것을 고른다. 또한 코드 롤백과 데이터 롤백은 별개다. 새 코드가 쓴 데이터를 구 코드가 처리하지 못하면 라우팅만 되돌려서는 복구되지 않는다.

10. 첫 6주 파일럿과 멈춰야 할 때

아래는 접근 권한과 업무 담당자가 이미 확보된 중간 규모 웹 프로젝트에서 첫 파일럿을 진행하는 6주 계획 예시다. 전체 현대화 일정을 보장하는 계획이 아니다.

주차 주요 작업 판단 근거
1 운영 기준선, 복구, 환경, 의존성 조사 실제 PHP·DB·배포 구성과 차단 요인
2 대표 업무·권한·DB·메일 테스트, 버전 경로 결정 원본 동작과 비교 가능한 계약
3 Rector, 전용 도구, 패키지 갱신 자동 변환과 잔여 작업의 분리
4 에이전트로 예외 수정과 기능 연결 테스트·분석·차등 실행과 리뷰
5 holdout·보안·성능 검증, 롤백 연습 미분류 차이와 차단 결함 해소
6 제한적 전환, 관측, 회고 실제 유지보수 비용과 확대 가능성

첫 파일럿으로는 읽기 중심의 관리 화면이나 조회 API가 적합하다. 그러나 DB·세션·권한 위험을 모두 피한 단순한 페이지 하나만 고르면 대표성이 없다. 그래서 첫 작은 성공 뒤에는 대표적인 쓰기 기능을 다음 검증 대상으로 포함한다. 결제·환불·개인정보 대량 수정·권한 관리·cron 전체를 첫 파일럿에서 동시에 바꾸지는 않는다.

에이전트 구성은 1편과 같이 단순하게 시작한다. 주력 에이전트 하나에 IDE의 PHP 코드 분석 기능, Composer, Rector, 정적 분석(PHPStan 또는 Psalm), 재현 환경, CI를 결합한다. 병렬화할 때는 composer.lock, 공용 인증·라우트·스키마의 작성자를 한 명으로 정한다. 여러 에이전트가 각자 의존성 업데이트를 실행하면 결과가 서로 충돌한다.

다음 상태가 나타나면 자동화 규모를 늘리지 말고 진단과 계약을 보강해야 한다.

  • 기준 DB가 없어서 운영 상태를 복구할 수 없다.
  • 실제 DB 대신 ORM 모델에서 스키마를 추측하고 있다.
  • 테스트를 없애거나 nullable·권한 완화·상수 보정을 해야만 통과한다.
  • 에이전트가 공용 세션·인증·스키마를 동시에 바꿔서 원인을 추적할 수 없다.

반대로 지원되지 않는 벤더 제품이나 암호화된 확장에 핵심 기능이 묶여 있다면, 코드 현대화 대신 기능 폐기, SaaS 전환, 표준 CMS 도입, 작은 재구축을 함께 비교한다. 오래된 코드를 한 줄도 바꾸지 않는 것도, 전부 버리는 것도 목표가 아니다. 사업에 필요한 기능을 감당할 수 있는 비용과 위험으로 운영하는 것 이 목표다.

마치며

컨설팅에서 나는 PHP 현대화를 한 번의 큰 계약으로 시작하지 않기를 권한다. 첫 단계는 진단이다. 운영 코드·실행 환경·의존성·라우트·권한·데이터·배포를 지도로 만들고, 그 결과로 고객이 유지·업그레이드·부분 교체·제품 대체·폐기 중에서 선택할 수 있게 한다. 다음은 운영 안정화, 그다음이 파일럿이다. 확대 범위는 파일럿에서 얻은 실제 증거로 정한다. 저장소를 보기 전에 기간·자동화율·비용 절감률을 약속하지 않는다.

이 시리즈의 두 글은 다른 언어를 다뤘지만 결론은 같다. 에이전트가 레거시 앞에서 헤매는 것은 코드가 오래되어서가 아니라 변경이 맞는지 확인할 방법이 없어서다. C/C++에서는 그 확인 수단이 빌드 재현과 차등 실행이었고, PHP에서는 운영 환경의 재현과 HTTP·권한·DB·외부 효과를 비교하는 하네스다. 검증 환경이 갖춰지면 에이전트는 이 블로그에서 다룬 소프트웨어 팩토리처럼 조사·변환·반증을 쉬지 않고 반복할 수 있다.18

이 글의 일정·임계값·구성은 공개 사례를 바탕으로 한 제안이다. 실제 적용 전에는 해당 프로젝트를 진단해서 조정해야 한다.


  1. 바이브 코딩으로 레거시 현대화하기 (1/2): 20년 된 C/C++ 애플리케이션: https://roboco.io/posts/legacy-cpp-vibe-coding-modernization/ ↩︎

  2. Thomas De Maesschalck, “How Codex Helped Me Modernize a 24 Year Old PHP Codebase” (2026-08-17): https://www.dvhardware.net/articles/31/dvhardware-codex-migration ↩︎

  3. Ryo Maruta, “生成AIを活用してPHP Laravelの大規模アップデートを実現した話” (2026-04-16): https://note.com/ryo_maruta/n/nb49789bf2ea8 ↩︎

  4. Rocket, “Accelerated modernization” (2025-11-07, 자체 발표): https://careers.rocket.com/blog/technology-and-product/accelerated-modernization-rocket-mortgage-origination ↩︎

  5. Boldare, “AI in action: accelerating Symfony migration from 5.4 to 6.4” (2025-10-13): https://www.boldare.com/blog/ai-in-action-accelerating-symfony-migration-from-5.4-to-6.4/ ↩︎

  6. 2am.tech, “Using AI to Port a Legacy Laravel CMS to a Modern Architecture” (2026-10-08 갱신): https://www.2am.tech/blog/using-ai-to-port-a-legacy-laravel-cms-to-a-modern-architecture ↩︎

  7. Rector, “Success Story of Automated Framework Migration from FuelPHP to Laravel of 400k+ Lines Application” (2022-02-07): https://getrector.com/blog/success-story-of-automated-framework-migration-from-fuelphp-to-laravel-of-400k-lines-application ↩︎ ↩︎

  8. PHP Supported Versions: https://www.php.net/supported-versions.php ↩︎

  9. Symfony Releases: https://symfony.com/releases , Laravel 13 Release Notes: https://laravel.com/docs/13.x/releases ↩︎

  10. CodeIgniter, Upgrading from 3.x to 4.x: https://codeigniter.com/user_guide/installation/upgrade_4xx.html ↩︎

  11. Rector Set Lists: https://getrector.com/documentation/set-lists ↩︎

  12. PrestaShop PR 41279 (2026-04-28 병합): https://github.com/PrestaShop/PrestaShop/pull/41279 ↩︎

  13. Laravel AI Assisted Development: https://laravel.com/docs/13.x/ai , PhpStorm MCP Server: https://www.jetbrains.com/help/phpstorm/mcp-server.html ↩︎

  14. PHP 8 Backward Incompatible Changes: https://www.php.net/manual/en/migration80.incompatible.php ↩︎

  15. Carbon Introduction: https://carbon.nesbot.com/guide/getting-started/introduction.html ↩︎

  16. Playwright Visual comparisons: https://playwright.dev/docs/test-snapshots ↩︎

  17. Bosun, “Modernising a legacy PHP application”: https://bosun.ai/case-studies/case-study-legacy-dependency-platform-upgrades/ ↩︎

  18. 바이브 코딩을 넘어서 소프트웨어 팩토리로: https://roboco.io/posts/beyond-vibe-coding-software-factory/ ↩︎