Node.js로 서버를 만들면 이렇게 시작한다.

const http = require('http');
http.createServer((req, res) => {
  res.end('Hello World');
}).listen(3000);

3줄이면 HTTP 서버가 뜬다. Nginx 같은 외부 웹 서버가 필요 없다.

Java(Spring Boot)도 마찬가지다.

java -jar app.jar  # Tomcat이 내장되어 있다

그런데 PHP(Laravel)는 이렇게 배포한다.

Nginx → PHP-FPM → Laravel

왜 PHP만 앞에 웹 서버를 따로 세우는가? PHP가 부족해서가 아니다. 태어난 시대가 달라서다.


1995년: PHP는 Apache의 플러그인이었다

PHP의 원래 이름은 Personal Home Page Tools다. 독립적인 서버 프로그램이 아니라, Apache 웹 서버 안에서 돌아가는 플러그인이었다.

당시 웹의 구조는 단순했다.

브라우저 → Apache → HTML 파일 서빙

Apache가 .html 파일을 서빙하다가 .php 파일을 만나면, “이건 동적이니까 PHP한테 맡기자”고 PHP 모듈을 호출했다.

브라우저 → Apache → .html이면 직접 서빙
                  → .php면 mod_php 호출 → PHP가 실행 → 결과 HTML 반환

mod_php — Apache 프로세스 안에 PHP 인터프리터가 내장되어 돌아가는 방식. 웹 서버가 주인이고, PHP가 하인이었다. 이 관계가 PHP의 DNA에 새겨진 거다.

이건 1995년에는 합리적이었다. PHP 스크립트가 10줄이었으니까. “웹 서버는 이미 있고, 거기에 동적 기능만 끼워넣으면 된다”는 접근이 당연했다.


2000년대: mod_php의 한계

웹이 커지면서 문제가 생겼다.

mod_php는 Apache의 모든 프로세스에 PHP 인터프리터를 로드했다. 정적 파일(이미지, CSS, JS)을 서빙하는 프로세스에도 PHP가 올라가 있었다. 요청의 80%는 정적 파일인데, 전부 PHP 메모리를 물고 있는 셈이었다.

Apache 프로세스 1: [PHP 로드됨] → 이미지 서빙 (PHP 안 씀, 메모리 낭비)
Apache 프로세스 2: [PHP 로드됨] → CSS 서빙 (PHP 안 씀, 메모리 낭비)
Apache 프로세스 3: [PHP 로드됨] → .php 처리 (PHP 사용)

그리고 Nginx가 등장했다. Apache보다 정적 파일 서빙이 압도적으로 빨랐다. 하지만 Nginx는 mod_php를 지원하지 않았다. PHP를 Nginx 안에 내장할 수 없었다.


2009년: PHP-FPM — 분리의 시작

해결책: PHP를 웹 서버에서 완전히 분리한다.

이전: Apache ─[mod_php 내장]─ PHP가 Apache 안에서 실행
이후: Nginx ─[FastCGI 통신]─→ PHP-FPM (별도 프로세스)

PHP-FPM(FastCGI Process Manager) — PHP 전용 프로세스 매니저. Nginx와는 FastCGI라는 프로토콜로 네트워크 통신한다.

# Nginx 설정
location ~ \.php$ {
    fastcgi_pass 127.0.0.1:9000;  # PHP-FPM에 네트워크로 전달
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

이제 Nginx는 정적 파일만 처리하고, PHP가 필요한 요청만 PHP-FPM에 넘긴다. 메모리 낭비가 사라졌다.

하지만 관계는 변하지 않았다. 여전히 웹 서버(Nginx)가 주인이고, PHP가 하인이다. PHP는 스스로 HTTP를 처리하지 못하고, 앞에 누군가가 요청을 받아서 넘겨줘야 한다.


2009년, 같은 해: Node.js가 태어나다

같은 2009년, Node.js가 나왔다. 이 시점에는 “프로그램이 직접 HTTP를 처리하는” 패러다임이 이미 보편화되어 있었다.

// Node.js의 첫 번째 예제
const http = require('http');
http.createServer((req, res) => {
  res.end('Hello World');
}).listen(3000);

Node.js는 처음부터 “나 자체가 서버”로 설계됐다. Apache나 Nginx에 기생할 필요가 없었다. Java(Tomcat), Go(net/http), Python(gunicorn)도 전부 같은 방향이었다.

PHP의 세계:   Nginx → PHP-FPM → 코드 (2단계, 외부 웹 서버 필요)
Node.js 세계: Express → 코드 (1단계, 웹 서버 내장)
Java 세계:    Tomcat → 코드 (1단계, 웹 서버 내장)

PHP만 “외부 웹 서버가 필요한 언어”로 남았다.


2022~: FrankenPHP — PHP도 드디어 “나 자체가 서버”

FrankenPHP가 이 30년 된 구조를 바꾼다.

Go로 만든 웹 서버(Caddy) 안에 PHP 인터프리터를 내장시켰다. Nginx가 필요 없다.

이전: Nginx → (FastCGI) → PHP-FPM → Laravel
이후: FrankenPHP(Caddy + PHP 내장) → Laravel
시대구조PHP의 위치
1995Apache + mod_phpApache 의 모듈
2009Nginx + PHP-FPMNginx 의 별도 프로세스
2022~FrankenPHP웹 서버 에 다시 내장 (하지만 이번엔 Go+Caddy 기반)

1995년과 2022년이 “PHP가 웹 서버 안에 있다”는 점에서 비슷해 보이지만, 결정적 차이가 있다.

1995 mod_php:    Apache가 PHP를 부품으로 씀 (웹 서버가 주인)
2022 FrankenPHP: PHP를 위해 Go가 웹 서버를 만듦 (PHP가 주인)

30년 만에 PHP가 하인에서 주인이 된 거다.


전체 타임라인

1995  Apache + mod_php     "웹 서버 안의 플러그인"

  │   웹이 커지면서 메모리 낭비 문제

2009  Nginx + PHP-FPM      "웹 서버 뒤의 별도 프로세스"

  │   같은 해 Node.js 등장 — "나 자체가 서버"
  │   PHP만 외부 웹 서버가 필요한 언어로 남음

2022  FrankenPHP            "Go 웹 서버 안에 PHP 내장"
      Nginx 없이 독립 실행
      Worker 모드로 프로세스 상주

왜 이 역사를 알아야 하는가

“Laravel 배포할 때 왜 Nginx 설정을 만져야 하지?” — 이 질문의 답이 여기 있다. PHP가 못해서가 아니라, 30년 전에 “웹 서버의 플러그인”으로 태어났기 때문이다.

다른 언어는 처음부터 독립 서버로 설계됐다. PHP는 이제서야 FrankenPHP/Octane으로 그 방향에 합류하고 있다.

Nginx + PHP-FPM 구조가 나쁜 건 아니다. 20년간 검증됐고, 안정적이고, 대부분의 프로젝트에서 충분하다. 하지만 “왜 이 구조인가”를 이해하면, 언제 FrankenPHP로 전환해야 하는지, 왜 Node.js 프로젝트에는 Nginx가 필요 없는지, 기술 선택의 근거를 댈 수 있다.

기술 선택에서 “뭘 쓸 것인가”보다 중요한 건 “왜 이게 이렇게 생겼는가”를 아는 것이다.