
Rust로 PHP 가상 머신(Virtual Machine) 작성하기 (AI의 많은 도움을 받아)
요약
LLM의 도움을 받아 Rust로 PHP 가상 머신(VM)을 구축한 엔지니어링 경험을 다룹니다. 단순한 코드 재작성을 넘어 AI를 활용해 생소한 코드베이스를 이해하고 구현 전략을 탐색하는 과정을 설명합니다.
핵심 포인트
- LLM은 복잡한 엔지니어링 프로젝트의 진입 장벽을 낮추는 도구로 활용 가능
- AI가 생성한 초기 프로토타입은 정확성은 높으나 성능 최적화에는 한계가 있음
- 기존 엔진의 레지스터 기반 구조와 AI가 생성한 스택 기반 구조의 차이 분석
- 단순한 언어 재작성이 아닌 Rust의 철학을 반영한 설계의 중요성
몇 년 전만 해도 누군가 저에게 PHP 엔진을 Rust로 다시 작성하라고 말했다면, 저는 아마 웃었을 것입니다 😆. 기술적으로 불가능해서가 아니라, 전통적으로 그런 프로젝트는 수년간 C 코드를 읽고, 수십 년간 쌓인 최적화 기법을 역공학(Reverse Engineering)하며, 엄청난 양의 인내심을 요구하기 때문입니다.
오늘날 대규모 언어 모델(Large Language Models, LLM)은 우리가 야심 찬 엔지니어링 프로젝트에 접근하는 방식을 바꾸어 놓았습니다. LLM이 마법처럼 바로 프로덕션에 사용 가능한 소프트웨어를 만들어내지는 않지만, 생소한 코드베이스를 이해하고, 구현 전략을 탐색하며, 아이디어를 검증하는 비용을 획기적으로 줄여줍니다.
Bun이나 React Compiler와 같은 프로젝트들은 성숙한 생태계를 다시 살펴보는 것이 더욱 현실적인 일이 되었음을 보여줍니다. 이는 AI가 엔지니어를 대체하기 때문이 아니라, 엔지니어의 역량을 배가시키기 때문입니다.
이 글은 Rust로 새로운 PHP 가상 머신(Virtual Machine, VM)을 구축한 이야기이며, 무엇이 효과적이었고 무엇이 실패했는지, 그리고 왜 목표가 또 다른 PHP 인터프리터를 만드는 것이 아니었는지에 대해 다룹니다.
첫 번째 프로토타입: 놀라울 정도로 쉽고 끔찍하게 느림
많은 AI 지원 프로젝트와 마찬가지로, 첫 번째 버전은 의도적으로 순진한(naive) 접근 방식으로 시작되었습니다. PHP 소스 코드를 깊이 있게 연구하지 않은 채, 모델에게 자체 지식을 바탕으로 VM을 생성하도록 요청한 다음, 무언가 실패할 때마다 예시와 수정을 통해 가이드했습니다.
머지않아 PHP의 의미 있는 부분 집합(subset)을 실행할 수 있는 기능적인 프로토타입을 갖게 되었지만, 성능은 믿을 수 없을 정도로 느렸습니다. 이는 전혀 놀라운 일이 아니었습니다. 생성된 구현 방식이 성능보다는 정확성에 치중했기 때문입니다.
게다가 PHP는 레지스터 기반 VM(Registry VM)임에도 불구하고, 모델은 스택 기반 VM(Stack VM)을 생성했습니다.

이러한 차이점 때문에 어느 시점에서는 매우 고통스러운 상황이 발생했을 것이라고 생각합니다. 값의 파괴 순서(destruction orders)나 에러 발생 타이밍(timing of errors emission)과 같이, 이러한 종류의 차이는 특정 동작이 동일하게 작동하지 않도록 만들 수 있습니다. 현재의 PHP 생태계를 고려할 때, 기존의 일부 라이브러리들이 그러한 순서에 의존하고 있을 것이라고 확신합니다. 실제 PHP 마이그레이션을 모방하는 것이 목표이므로, 우리는 이를 존중해야 한다고 믿습니다.
(하루 만에 완료된) 프로토타입은 이 아이디어가 실행 가능하다는 것을 증명했을 뿐, 그것이 최종 아키텍처가 아님은 분명했습니다.
PHP를 똑같이 복제하는 것이 목표는 아니었습니다
그 시점에서 하나의 선택지는 Bun이 재작성(rewrite)을 통해 취했던 경로를 따라 원본 구현을 가능한 한 가깝게 재현하는 것이었습니다. 여기에는 분명한 장점이 있습니다. PHP 엔진에는 수백 명의 기여자들로부터 얻은 20년 이상의 지식과 최적화가 담겨 있으며, 그 아키텍처를 재현하는 것은 종종 성능을 재현하는 것을 의미하기 때문입니다.
그러한 재작성은 일반적으로 대상 언어(target language)의 방식(way of doing things)을 깨뜨립니다. Bun 재작성 풀 리퀘스트(pull request)를 보십시오. 그곳은 unsafe로 가득 차 있습니다.
제 의견으로는, 이는 Rust를 사용하는 목적을 완전히 무색하게 만들며 이 PR을 완전히 무의미하게 만듭니다.
이 프로젝트의 목적은 단순히 Zend Engine을 다른 언어로 재작성하는 것이 아니라, PHP가 어떻게 작동하는지 이해하고, 만약 처음부터 Rust의 철학을 중심으로 설계되었다면 그 VM이 어떤 모습일지 탐구하는 것이었습니다.
이는 소유권(ownership)을 수용하고, 전역 가변 상태(global mutable state)를 피하며, unsafe 코드를 최소화하고, 언어와 끊임없이 싸우기보다는 언어의 장점을 활용하는 것을 의미했습니다. 그렇지 않다면 Rust를 선택할 이유가 거의 없을 것입니다.
PHP를 복제하는 대신 PHP로부터 배우기
PHP를 복제하지 않는다는 것이 PHP를 무시한다는 뜻은 아닙니다. 프로젝트의 상당 부분은 Zend Engine 코드를 읽고, AI에게 특정 메커니즘이 왜 존재하는지 묻고, 역사적 결정들을 추적하며, 대안적인 구현 방식에 대해 논의하는 과정으로 이루어졌습니다.
PHP가 어떻게 작동하는지, 왜 그런 방식으로 작동하는지, 그리고 Rust에서 유사한 동작을 어떻게 구현할 수 있는지에 대해 많은 것을 배웠습니다.
그러한 대화들을 통해 특정 최적화가 오래된 CPU 동작 방식을 반영하고 있다는 점, 기묘해 보이는 코드가 임계 경로(critical path)에서의 할당(allocation)을 피하기 위한 것이라는 점, 또는 Rust가 더 자연스러운 구현을 제공한다는 점 등을 자주 발견할 수 있었습니다. 또한, PHP가 이미 수년 전에 최선의 해결책을 찾아냈다는 사실도 그만큼 자주 확인되었습니다.
목표는 점차 PHP를 다시 작성하는 것에서, PHP가 왜 오늘날과 같은 모습이 되었는지 이해하는 것으로 옮겨갔습니다.
Building the VM I always wanted
엔진을 연구하면서 수년 동안 PHP에서 보고 싶었던 아이디어들을 실험해 볼 기회도 얻었습니다.
Respecting ownership and avoiding global state
첫 번째 제약 사항 중 하나는 Rust 방식대로 일을 처리하는 것, 즉 전역 가변 상태(global mutable state)를 피하는 것이었습니다. 대신, 이 VM은 각자 자신만의 상태를 가진 채 여러 번 인스턴스화될 수 있는 독립적인(self-contained) 객체로 설계되었습니다.
이를 통해 아키텍처를 더 쉽게 추론할 수 있게 되었으며, 동일한 스레드 내에서 여러 개의 독립적인 VM을 실행할 수 있게 되었습니다. 또한 테스트와 임베딩(embedding)을 단순화하며, Rust의 프로그래밍 모델과 훨씬 더 자연스럽게 어우러집니다. Rust에서 전역 가변 상태는 안전하게 만들기 위해 주변에 RefCell을 강제하여 성능을 저하시키거나, unsafe 코드를 사용해야 하므로(금기 사항) 문제가 됩니다.
A fork model for requests
모든 요청마다 새로운 런타임(runtime)을 다시 구축하는 대신, VM을 일시 중지하고 쓰기 시 복사(copy-on-write) 의미론을 사용하여 복제(clone)할 수 있습니다. 복제본은 요청을 실행한 후 폐기되며, 건드리지 않은 원본은 즉시 다음 요청을 위한 베이스로 서비스할 준비가 됩니다.

코드에서는 다음과 같이 작동합니다:
<?php
require 'vendor/autoload.php';
// 프리로드된 상태(preloaded state)를 초기화합니다
...
이 모드는 각 요청이 처음부터 다시 시작되는 클래식한 PHP 모델과, 요청 간에 상태가 유지되는 FrankenPHP 워커 모드(worker mode)와 같은 장기 실행 프로세스(long-running process) 사이의 중간 단계에 위치합니다. 이를 통해 VM은 요청 간의 격리(isolation)를 유지하면서도 애플리케이션 상태를 반복적으로 재구성하는 것을 피할 수 있습니다.
AI를 토론 파트너로 활용하기
이 프로젝트에서 가장 예상치 못했던 부분은 코드 생성(code generation)이 아니라, 기술적 토론의 깊이였습니다. 우리는 PHP가 왜 특정 방식으로 객체를 배치했는지, CPU 캐시(CPU caches) 때문에 특정 최적화가 존재하는지, 혹은 Rust의 타입 시스템(type system)이 런타임 체크(runtime check)를 제거할 수 있는지 등을 탐구할 수 있었습니다.
또한 컴파일러(compiler)가 어떻게 작동하는지, VM을 어떻게 설계하는지, 그리고 Zend Engine의 내부 구조에 대해서도 많은 것을 배웠습니다.
AI가 첫 번째 시도에서 정답을 맞히는 경우는 드물었지만, 거의 항상 유용했습니다. AI는 단순한 코드 생성기라기보다 기술적인 스파링 파트너(sparring partner)에 가까워졌으며, 수천 줄의 C 코드를 요약하고, 대안적인 설계를 제안하며, 질문을 실험으로 전환할 수 있도록 도와주었습니다.
하지만 AI는 특정 규칙을 따르라고 지시하고 구조를 수정하기 위해 여러 번 시도한 후에도 여전히 지저분한 코드를 작성하곤 합니다. 현재의 코드는 완벽과는 거리가 멉니다.
결국, 이 프로젝트는 AI에 의해 작성된 것이 아니라, AI에 의해 가속화된 것입니다. 이것이 이러한 도구들이 야심 찬 소프트웨어 프로젝트에 가져다주는 가장 중요한 변화일지도 모릅니다.
AI가 모든 것을 대신 코딩해 줄까 봐 두려워하는 사람들에게 말하자면, 결코 그렇지 않다고 말씀드릴 수 있습니다. 만약 당신이 코드와 그 근간이 되는 개념을 이해하지 못한다면, 처참하게 실패할 것입니다. AI는 도구이자 파트너이지, 지식과 경험을 대체하는 것이 아닙니다. 하지만 AI는 위험하기도 합니다. AI가 놀라운 일을 해냈다고 믿었다가 그것이 틀렸다는 것을 발견한 횟수는 셀 수 없을 정도입니다. AI는 당신을 도울 수 있는 도구이지만, 당신을 잘못된 길로 인도할 수도 있습니다. 주의를 기울여야 하며 항상 결과를 검증해야 합니다.
왜 Rust인가요? 왜 [여기에 본인의 언어를 입력하세요]가 아닌가요?
이 실험은 C로 수행할 수도 있었고(그저 PHP의 포크를 만드는 방식), Zig를 사용하거나 심지어 PHP를 사용할 수도 있었습니다.
제가 이 언어를 선택한 데에는 주로 두 가지 이유가 있습니다:
- 저는 이 언어를 잘 알고 있습니다. 즉, 코드를 읽을 때 언어 자체에 대해 고민할 필요 없이 로직에만 집중할 수 있으며, 이는 검토 시간을 크게 절약해 줍니다.
- 저는 Rust가 실제로 AI와 함께 작업하기에 가장 좋은 언어라고 생각합니다. 단순히 Rust가 매우 많은 제약 조건(constraints)을 가지고 있기 때문입니다. 개발자에게는 이 제약이 고통스러울 수 있지만, AI가 코드를 작성할 때는 이를 더 안전하게 만들어 줍니다. AI는 완벽하지 않으며, 언젠가 완벽해질 것이라고 생각하지도 않습니다. 완벽함이란 가변적인 상태이며 주관적이기 때문입니다. 또한 실수를 하는 것도 학습 과정의 일부입니다. 따라서 제 관점에서 AI가 유능해지려면 결정론적인(deterministic) 도구가 필요하며, 결정론적인 제약 조건이 많을수록 더 강력해집니다. AI가 이러한 제약 조건에 대해 자동 피드백을 받을 수 있고 자신이 하는 일을 조정할 수 있기 때문입니다. 이는 또한 최종 사용자를 위해 구축된 결과물에 대해 훨씬 더 큰 신뢰를 더해줍니다.
프로젝트의 현재 진행 상황
GitHub에서 프로젝트를 확인하실 수 있습니다: https://github.com/jolicode/rphp.
아직 갈 길이 멀지만, 현재 기본적인 호환성 테스트의 약 80%를 통과했습니다. 많은 기능과 예외 케이스(edge cases)가 남아 있지만, VM은 이미 상당한 양의 실제 PHP 코드를 실행할 수 있습니다.
외부 또는 네이티브 함수 호출이 없는 순수 PHP 코드의 경우, 현재 PHP보다 5배에서 15배 정도 느립니다. Zend Engine이 20년 동안 수행해 온 프로파일링(profiling)과 최적화(optimization)를 고려하면 놀라운 일은 아니지만, 순수 인터프리터(interpreter) 속도는 전체 그림의 일부일 뿐입니다.
하지만 포크 모드(fork mode)는 동일한 애플리케이션에서 VM의 클래식 실행 모드(classic execution mode)보다 약 30배 더 빠릅니다. 이는 지금까지 얻은 결과 중 가장 고무적인데, 인터프리터(interpreter) 성능만이 유일하게 사용할 수 있는 레버(lever)가 아님을 시사하기 때문입니다. Symfony 데모 애플리케이션에서 애플리케이션 상태의 반복적인 재구축을 피하는 것만으로도, 인터프리터 속도가 더 느림에도 불구하고 VM을 클래식 모드의 PHP / FrankendPHP보다 더 빠르게 만들기에 충분했습니다.
포크 모델(fork model)의 아키텍처적 이점을 유지하면서 순수 코드 실행(pure code execution) 측면의 격차를 점진적으로 좁힐 수 있다면, 그 결과는 실제 애플리케이션을 위한 진정으로 빠른 엔진이 될 수 있을 것입니다.
이 프로젝트는 약 한 달 동안 작성되었으며, 매일 무엇이 중요한지에 대해 조정, 읽기, 그리고 다시 집중하는 과정을 거쳤습니다. 또한 AI가 테스트를 반복하며 현재 구현과 예상/기존 동작 사이의 격차를 메우는 작업을 수행하는 등 많은 자율적인 작업이 있었습니다. 이번 기회에는 Max 20x Claude 플랜(약 200€, 한 달만 사용)을 사용했으며, 주로 Opus 4.8 / Sonnet 4.6을 사용했고, 특정 시점에는 Fable도 사용했습니다. 이 세 모델 사이의 차이를 느낄 수는 있었지만, 우리가 끊임없이 조정했다는 점을 고려하면 세 모델 중 어느 것을 사용했더라도 동일한 결과를 냈을 것이라고 느꼈습니다.
다음 단계는 무엇인가? (What’s next?)
다음에 무엇을 해야 할지 잘 모르겠습니다. 이 프로젝트는 학습과 실험 측면에서 이미 성공적이었지만, 프로덕션 환경에 즉시 투입할 수 있는 PHP 엔진이 되기에는 아직 갈 길이 멉니다. VM을 계속 개선할 수도 있고, 이 프로젝트에서 얻은 지식을 바탕으로 새로운 프로젝트를 시작할 수도 있습니다.
이러한 아이디어 중 일부는 실수가 될 수도 있고, 다른 일부는 놀라울 정도로 효과적임이 증명될 수도 있습니다. 어느 쪽이든, 이 여정은 이미 그 자체로 가치가 있었습니다.
댓글 및 토론
이 고객들은 우리의 전문 지식을 활용했습니다

Axiatel은 회사가 수행한 개발 사항을 평가하기 위해 JoliCode에 의뢰했습니다. 짧은 감사(Audit)를 마친 후, 우리는 현재 상황에 대한 결산 보고서, 목표 애플리케이션 아키텍처(Application Architecture), 그리고 제시된 애플리케이션 모델로의 원활한 마이그레이션(Migration)을 위한 권장 사항을 제시했습니다.

디지털 전략 혁신의 일환으로, Orpi France는 orpi.com 웹사이트의 개편과 수많은 새로운 서비스의 통합을 주도하기 위해 JoliCode에 의뢰했습니다. 이 마이그레이션을 수행하기 위해 우리는 PHP, Symfony, RabbitMQ, Elasticsearch 및 Docker를 사용한 마이크로서비스(Microservices) 아키텍처에 기반하였습니다.

우리는 Phraseanet API를 중심으로 범용 사이트 플랫폼을 개발했습니다. Symfony2 컴포넌트의 Silex를 사용하여, 우리는 Alchemy가 고객들을 위해 확장 가능한 사이트를 구축할 수 있도록 지원했습니다. 이 제품은 완전히 구성 가능(Configurable)하며 다양한 인증 시스템(Ldap, OAuth2, Doctrine 또는 익명)을 지원합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Lobste.rs AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기