본문으로 건너뛰기
5분 AI.
GITHUB TRENDING GO WEEKLY #11 딥다이브 · 2026-10-23 분석

frankenphp 딥다이브
— Go의 문으로 들어와 PHP의 수명을 바꾸는 서버

앱 서버

빠른 판단

무엇
Caddy의 Go HTTP 서버에 PHP 엔진과 상주 워커 모드를 결합한 PHP 애플리케이션 서버
이럴 때
PHP 앱을 자동 HTTPS와 함께 배포하고 반복 요청의 앱 초기화 비용을 줄이는 워커 모드를 적용할 때
FrankenPHP는 Caddy 웹 서버에 PHP 실행기를 직접 결합한 애플리케이션 서버입니다. PHP-FPM 없이 PHP를 실행하고, 워커 모드에서는 앱을 메모리에 유지해 요청마다 반복하던 초기화를 줄입니다. 이 글은 HTTP 요청 하나가 Go·C·PHP를 통과하는 경로를 따라가며 동시성, 상태 관리, 배포와 관측을 배웁니다. 저장소 php/frankenphp · 주 언어 Go · ⭐11,454 · 포크 500 · MIT (2026-10-23 GitHub 기준) · 코드: 커밋 d138a78 기준.
목차
  1. 프로젝트 한줄 요약
  2. 왜 주목받는가
  3. 기술 스택 전체 지도
  4. 아키텍처 심화 분석
  5. 디렉토리 구조 해부
  6. 학습 포인트 (기술별)
  7. 하드웨어 / 시스템 요구사항
  8. 직접 해볼 수 있는 실습 과제
  9. 관련 기술 심화 학습 로드맵
  10. 핵심 키워드 사전
  11. 참고 링크

1프로젝트 한줄 요약

서버를 합치고, PHP 앱을 언제 초기화할지 다시 설계한다

FrankenPHP는 Go의 HTTP 서버 안에 PHP 인터프리터를 넣은 서버입니다. 일반적인 Nginx + PHP-FPM 구성에서는 웹 서버가 FastCGI로 별도 PHP 프로세스에 일을 넘깁니다. 여기서는 Go와 C의 연결 기능인 CGO를 통해 같은 프로세스 안의 PHP 실행 스레드에 요청을 전달합니다.

PHP를 Go 코드로 번역하는 도구는 아닙니다. PHP 코드는 PHP 엔진이 실행하고, Go는 요청 접수와 스레드 배치를 맡습니다. 이 저장소의 핵심은 언어를 바꾸는 것이 아니라, 요청과 PHP 실행기의 수명을 다르게 묶는 데 있습니다.

용어
애플리케이션 서버와 인터프리터
애플리케이션 서버는 요청을 받아 프로그램을 실행하고 응답을 돌려주는 담당자입니다. 인터프리터는 PHP 같은 언어의 코드를 실제로 실행하는 엔진입니다. FrankenPHP는 웹 서버 기능, 요청 배치 기능, PHP 엔진을 한 실행 환경으로 연결합니다.
실행 방식요청마다 하는 일다음 요청까지 남는 것
일반 모드PHP 요청을 시작하고 스크립트를 실행한 뒤 요청을 정리서버·PHP 스레드·OPcache 등. 앱의 일반 요청 상태는 종료 절차를 거침
워커 모드이미 실행 중인 PHP 스크립트의 콜백으로 요청 처리앱 객체·전역 변수·정적 변수·메모리 캐시 등이 워커 수명 동안 지속

일반 모드에서도 요청마다 운영체제 스레드를 새로 만드는 것은 아닙니다. threadregular.go의 regularThread는 살아 있는 PHP 스레드를 재사용하면서 스크립트를 실행합니다. 워커 모드는 여기서 더 나아가 스크립트 자체를 끝내지 않고 여러 요청을 처리합니다.

한 문장 비유

손님마다 주방을 다시 준비할지, 준비된 주방에서 주문만 받을지의 차이입니다

Caddy는 홀의 접수대, Go의 스레드 관리 코드는 주문 배치 담당자, PHP 엔진은 조리 공간입니다. 일반 모드는 주문마다 앱이 필요한 준비를 하고 정리합니다. 워커 모드는 레시피와 조리 도구를 미리 펼쳐 두고 주문 처리만 반복합니다.

준비 시간을 줄이는 대신 이전 손님의 재료가 다음 접시에 섞이지 않게 해야 합니다. 워커를 배울 때 가장 중요한 질문도 같습니다. 무엇을 재사용하고, 무엇을 요청마다 지울 것인가?

짧은 코드로 보는 수명의 차이

다음은 구조를 설명하는 학습용 예제입니다. 실제 프레임워크의 초기화·종료 코드는 공식 통합을 따릅니다.

<?php
// 워커 스크립트 시작 시 한 번 준비한다.
$dictionary = ['hello' => '안녕하세요'];

$handler = static function () use ($dictionary): void {
    // 요청마다 달라지는 값은 콜백 안에서 읽는다.
    $word = $_GET['word'] ?? 'hello';
    header('Content-Type: text/plain; charset=utf-8');
    echo $dictionary[$word] ?? '등록되지 않은 단어';
};

while (frankenphp_handle_request($handler)) {
    // 실제 앱에서는 요청별 서비스 정리도 이 경계에서 수행한다.
}

frankenphp.c의 PHP_FUNCTION(frankenphp_handle_request)가 이 PHP 함수를 구현합니다. 워커 모드 밖에서 호출하면 예외를 던지므로, 단순히 기존 PHP 파일에 반복문만 붙여 실행할 수는 없습니다.

2왜 주목받는가

배포 단순화와 장기 실행이라는 두 가지 학습 가치

2026-10-23 GitHub Trending Go Weekly #11에 오른 시점의 스타는 11,454개, 포크는 500개입니다. 최신 릴리스 v1.13.2는 이틀 전인 2026-10-21에 공개됐습니다. 다만 순위와 릴리스 날짜만으로 유행의 원인을 확정할 수는 없습니다. 아래는 소스와 공식 문서에서 읽을 수 있는 주목할 만한 기술적 이유입니다.

2-1. 웹 서버와 PHP 실행을 한 배포 단위로 묶는다

caddy/frankenphp/main.go는 Caddy 표준 모듈과 FrankenPHP 모듈을 가져온 뒤 caddycmd.Main()을 호출합니다. 별도 웹 서버를 처음부터 구현하지 않고 Caddy의 설정·인증서·HTTP 기능을 확장하는 방식입니다. docs/migrate.md는 Nginx/Apache와 PHP-FPM을 하나의 FrankenPHP 실행 파일로 대체하는 구성을 설명합니다.

비교 항목Nginx + PHP-FPMFrankenPHP 일반 모드FrankenPHP 워커 모드
PHP 전달 경로FastCGI로 별도 프로세스에 전달프로세스 내부의 CGO·SAPI 경계같은 내부 경로 + 장기 실행 콜백
앱 초기화일반적으로 요청마다 수행일반적으로 요청마다 수행워커 시작 시 수행하고 재사용
설정 중심웹 서버 설정 + FPM 풀 설정Caddyfile의 사이트·frankenphp 설정일반 설정 + 워커 수·상태 정리 정책
HTTPS인증서 발급·갱신 구성을 별도로 선택Caddy 자동 HTTPS 활용동일
장애 격리PHP 자식 프로세스별 경계PHP 실행 스레드들이 한 프로세스 공유동일 + 앱 상태 장기 보존에 주의
이전 작업기존 운영 방식 유지경로·확장·프록시 정책 호환성 확인일반 모드 검증 + 워커 호환성 확인

이 표는 실행 모델 비교입니다. 처리량 순위표가 아닙니다. 워커 모드의 장점은 앱 초기화의 반복을 줄이는 것이며, 실제 개선 폭은 초기화 비용이 전체 요청에서 차지하는 비중에 달려 있습니다.

2-2. PHP 개발자가 Go 시스템 코드를 읽을 연결점이 많다

PHP의 echo가 Go의 응답 쓰기로 이어지고, Go의 채널 대기가 PHP 함수의 요청 대기로 이어집니다. 익숙한 웹 요청을 출발점으로 다른 언어의 동시성 모델을 배울 수 있습니다. server.go는 서버별 설정과 워커를 묶고, internal/state/state.go는 스레드의 안전한 전환을 관리합니다.

README는 Symfony와 Laravel의 공식 워커 통합을 소개합니다. 이는 기존 생태계와 연결되는 장점이지만, 모든 앱이 수정 없이 워커에 적합하다는 뜻은 아닙니다. 공식 docs/worker.md도 앱을 워커 수명에 맞게 조정하고 메모리 누수를 관리해야 한다고 설명합니다.

2-3. 최신 HTTP 기능과 운영 기능을 함께 읽을 수 있다

README는 Early Hints, HTTP/2·HTTP/3, 자동 HTTPS, 실시간 기능, 핫 리로드를 소개합니다. 소스에서는 caddy/caddy.go의 모듈 등록, metrics.go의 Prometheus 계측, hotreload.go의 Mercure 업데이트 발행처럼 각 기능의 연결점을 확인할 수 있습니다.

읽는 관점
특정 기능보다 기능 사이의 경계를 공부한다

인증서 관리는 Caddy, PHP 실행은 PHP 엔진, 요청 배치는 Go 코드가 담당합니다. 좋은 통합 서버는 모든 것을 직접 구현하는 대신, 각 부품의 수명과 실패 처리를 맞춥니다. 이 저장소에서 익혀 다른 시스템에도 적용할 수 있는 설계 원칙입니다.

3기술 스택 전체 지도

의존성 선언, 네이티브 연결, 빌드 파이프라인을 한 지도에 놓기

3-1. 백엔드: Go가 조율하고 C가 PHP 엔진에 연결한다

루트 go.mod와 caddy/go.mod는 모두 go 1.27.0을 선언합니다. 저장소 주소는 php/frankenphp지만 모듈 경로는 여전히 github.com/dunglas/frankenphp입니다. 예제의 import 경로를 저장소 주소에 맞춰 임의로 바꾸면 안 됩니다.

구성 요소실제 선언 버전파일과 역할
Go1.27.0두 go.mod의 최소 Go 버전 선언
Caddyv2.11.7caddy/go.mod · HTTP 서버와 모듈 실행 기반
FrankenPHP 모듈v1.13.2caddy/go.mod · 다만 로컬 replace가 루트 코드를 선택
CertMagicv0.25.6caddy/go.mod · 인증서 자동화 관련 의존성
Mercurev1.1.4두 go.mod · 실시간 업데이트와 핫 리로드 기반
Prometheus 클라이언트v1.25.0두 go.mod · 스레드·워커·대기 요청 계측
Cobrav1.10.2caddy/go.mod · php-server 등 명령 옵션 구성
Otterv2.4.0go.mod · Go 캐시 라이브러리 의존성
Watcherv0.0.0-20260223030516-06f84a1314bego.mod · 파일 변경 감지 모듈의 의사 버전
Testifyv1.12.1두 go.mod · assert·require 기반 테스트
PHP정적 빌드 기본 8.5craft.yml · 패치 버전까지 고정한 선언은 아님
// caddy/go.mod의 핵심 관계
module github.com/dunglas/frankenphp/caddy
go 1.27.0

replace github.com/dunglas/frankenphp => ../

require (
    github.com/caddyserver/caddy/v2 v2.11.7
    github.com/dunglas/frankenphp v1.13.2
)

위 예제는 원본에서 관련 선언만 발췌한 것입니다. replace 때문에 Caddy 모듈을 이 소스 트리에서 빌드하면 릴리스의 코어 대신 상위 디렉터리의 코어를 씁니다. 의존성 표의 버전과 실제 빌드에 들어가는 소스가 언제나 같지는 않으므로, require와 replace를 함께 읽어야 합니다.

용어
CGO와 SAPI
CGO는 Go에서 C 함수와 자료형을 연결하는 기능입니다. SAPI는 PHP 엔진이 바깥 실행 환경과 통신하는 인터페이스입니다. FrankenPHP는 이 두 경계를 이용해 HTTP 본문 읽기, 헤더 쓰기, 로그 기록을 Go 쪽과 연결합니다.

cgo.go에는 Unix의 -lphp -lm -lutil, Linux의 -ldl -lresolv 같은 링크 옵션이 있습니다. 따라서 이 코어는 순수 Go 프로그램처럼 CGO_ENABLED=0만으로 완성할 수 있는 구조가 아닙니다. PHP 라이브러리·헤더와 플랫폼별 네이티브 의존성을 함께 맞춰야 합니다.

3-2. 프론트엔드: 이 저장소에는 별도 SPA 제품이 없다

추적 파일에는 package.json, composer.json, Cargo.toml, pyproject.toml이 없습니다. React나 Vue 앱을 빌드하는 저장소가 아니라 PHP 앱을 실행하는 기반 서버입니다. 프론트엔드 프레임워크와 Composer 의존성은 그 위에 올리는 개별 PHP 애플리케이션의 선택입니다.

JavaScript 파일 7개는 testdata/load-test.js와 testdata/performance/의 부하 테스트입니다. 예를 들어 testdata/performance/hello-world.js는 k6/http를 가져옵니다. JavaScript 파일이 있다는 사실만으로 Node.js 프론트엔드 스택을 추정하면 안 됩니다.

브라우저와 연결되는 핫 리로드 설정은 caddy/hotreload.go에서 확인할 수 있습니다. configureHotReload()는 Mercure 허브가 없으면 오류를 반환하고, FRANKENPHP_HOT_RELOAD 환경 변수에 구독 경로를 전달합니다. 기본으로 모든 페이지가 자동 갱신되는 것은 아닙니다.

3-3. 인프라·빌드: 같은 서버라도 산출물의 조건은 다르다

파일직접 확인할 설정배울 점
Dockerfilecommon 기반의 builder·runner 단계, 빌드 산출물 복사컴파일 도구와 실행 환경을 분리
docker-bake.hclGo 1.27, PHP 8.2·8.3·8.4·8.5, 기본 PHP 8.5버전·운영체제 조합으로 이미지 행렬 생성
craft.ymlphp-version: "8.5", enable-zts: true정적 빌드의 확장·라이브러리·SAPI 선언
build-static.shstatic-php-cli v3 사용, LIBC=musl 또는 gnu완전 정적 빌드와 glibc 연동 빌드 구분
.github/workflows/tests.yamlGo -race, FuzzRequest, PHP 8.6은 experimental기본 검증과 실험적 호환성 검증 구분
.github/workflows/sanitizers.yamlasan·msan 행렬, USE_ZEND_ALLOC=0Go 경합 검사와 C 메모리 검사는 별도 축

Dockerfile은 FROM php-base와 golang-base라는 외부 빌드 문맥을 사용합니다. 실제 버전·운영체제 조합은 docker-bake.hcl까지 읽어야 알 수 있습니다. 또한 Watcher C 라이브러리는 Dockerfile에서 GitHub의 최신 릴리스를 조회하므로, Go 모듈 고정만으로 모든 네이티브 입력이 고정되지는 않습니다.

컨테이너 기본 명령은 --config /etc/frankenphp/Caddyfile --adapter caddyfile이고, Dockerfile이 PHP 엔트리포인트를 frankenphp run 호출로 바꿉니다. 상태 검사는 http://localhost:2019/metrics를 대상으로 합니다. 이 내부 상태 검사 주소를 외부 서비스용 API로 해석하지 않는 것이 중요합니다.

4아키텍처 심화 분석

요청 하나를 추적하고, 그 경로를 지탱하는 네 가지 패턴을 읽는다
브라우저 / HTTP 클라이언트 | v +-------------------------------------------------------------+ | FrankenPHP 프로세스 | | | | Caddy: HTTPS · HTTP/2/3 · 라우팅 · 정적 파일 | | | | | | | PHP 요청 +---> 정적 파일 응답 | | v | | FrankenPHPModule.ServeHTTP() [caddy/module.go] | | v | | Server.ServeHTTP() [server.go] | | | | | +-- 워커 배정 없음 --> 일반 PHP 스레드 풀 | | | 요청마다 스크립트 시작·정리 | | | | | +-- 워커 배정 있음 --> 지정 워커의 PHP 스레드 풀 | | 실행 중인 콜백으로 요청 처리 | | | | | CGO / PHP SAPI 경계 | | v | | PHP 엔진 + 애플리케이션 | | | | | 헤더·본문·로그 콜백 | | v | | Go의 http.ResponseWriter | +-------------------------------------------------------------+ | v 클라이언트 응답 별도 주 PHP 스레드: 엔진 초기화·전체 수명 관리 관측·제어: 상태 머신 / 메트릭 / 파일 감시 / 스레드 증감

4-1. 패턴 ① 어댑터: 서버의 요청을 PHP의 요청으로 바꾸기

caddy/caddy.go의 init()은 php와 php_server 지시어를 등록합니다. caddy/module.go의 parsePhpServer()는 PHP 처리기와 정적 파일 처리기를 함께 구성합니다. 기본 실행 확장자는 .php, 기본 인덱스 파일은 index.php입니다.

  1. FrankenPHPModule.ServeHTTP()가 문서 루트·환경 변수·원래 요청 정보를 준비합니다.
  2. Server.ServeHTTP()가 등록 상태를 검사하고 newContextFromRequest()로 요청 문맥을 만듭니다.
  3. context.go와 cgi.go에서 서버 범위의 워커 매처와 파일 경로를 바탕으로 실행 대상을 결정합니다.
  4. 요청 문맥에 워커가 있으면 worker.handleRequest(), 없으면 handleRequestWithRegularPHPThreads()로 갑니다.
  5. PHP 엔진의 SAPI 콜백이 Go 함수와 연결되어 요청 본문을 읽고 응답을 씁니다.

연결의 실물은 frankenphp.c의 frankenphp_sapi_module입니다. 여기에 frankenphp_ub_write, frankenphp_send_headers, frankenphp_read_post 같은 함수가 등록됩니다. frankenphp.go의 go_ub_write() 등은 Go 쪽 요청 문맥과 응답 작성자를 사용합니다.

비유

어댑터는 같은 일을 서로 다른 양식으로 접수하는 두 부서 사이의 번역 창구입니다. Go는 http.Request를 이해하고 PHP 엔진은 SAPI와 슈퍼글로벌을 이해합니다. 번역 창구가 경로·헤더·입출력을 맞춰 주므로 양쪽이 상대의 내부 구현을 전부 알 필요가 없습니다.

4-2. 패턴 ② 스레드 풀과 역압: 받을 수 있는 만큼만 실행하기

phpthread.go의 boot()는 C.frankenphp_new_php_thread()를 호출합니다. 실제 구현은 frankenphp.c에서 pthread_create()로 PHP 실행 스레드를 만듭니다. PHP 요청마다 goroutine 하나를 PHP 실행기로 삼는 모델이 아니라, Go가 네이티브 PHP 스레드들을 조율하는 모델입니다.

용어
goroutine·스레드·역압
goroutine은 Go 런타임이 관리하는 가벼운 실행 단위이고, 운영체제 스레드와 일대일 대응하지 않습니다. PHP 실행 스레드는 운영체제 수준의 별도 실행 단위입니다. 역압은 처리기가 바쁠 때 상류 요청을 대기시키거나 거절해 처리 능력 이상의 일을 무작정 실행하지 않는 제어입니다.

threadregular.go는 먼저 비어 있는 스레드의 requestChan으로 전달을 시도합니다. 바로 받을 스레드가 없으면 공유 regularRequestChan에서 전달 기회를 기다립니다. 이 채널은 버퍼 없이 만들어집니다. “요청 큐”는 거대한 채널 버퍼가 아니라 전달을 기다리는 요청들과 카운터로 표현됩니다.

// threadregular.go에서 분기 구조만 추린 의사 코드
select {
case regularRequestChan <- fc:
    // PHP 쪽 응답 완료 통지를 기다린다.
    <-fc.done
case scaleChan <- fc:
    // 증설을 요청한 뒤 원래 요청은 계속 대기한다.
case <-fc.ctx.Done():
    // 아직 배정되지 않은 취소 요청을 큐에서 뺀다.
case <-timeoutChan(maxWaitTime):
    // 대기 한도를 넘긴 요청을 거절한다.
}

이 의사 코드에서 생략된 실제 구현은 카운터와 메트릭도 모든 종료 경로에서 정리합니다. max_wait_time의 기본값은 비활성화이므로 설정 없이 대기 시간이 자동으로 제한된다고 생각하면 안 됩니다. 다만 증설 신호를 보내 루프를 반복하면 타이머가 다시 만들어지므로, 이 값이 전체 대기 시간의 엄격한 상한은 아닙니다. 또한 fc.done은 응답 완료 통지입니다. 조기 응답 종료 함수가 쓰이면 PHP 후처리와 스레드의 다음 요청 가능 시점은 그보다 뒤일 수 있습니다.

scaling.go는 최소 대기 5ms, CPU 관측 120ms, CPU 사용률 기준 0.8을 두고 증설을 판단합니다. 증설은 max_threads가 시작 스레드 수보다 클 때 활성화됩니다. 요청이 많다는 이유만으로 무조건 스레드를 늘리지 않는 설계입니다.

4-3. 패턴 ③ 상태 머신: 종료·재시작·역할 전환을 명시하기

internal/state/state.go는 Reserved, Booting, Inactive, Ready, ShuttingDown, RebootReady 등의 상태를 정의합니다. 현재 상태뿐 아니라 원하는 상태를 기다리는 구독 채널도 관리합니다.

대표적인 생성·배정 경로 Reserved --> Booting --> Inactive | TransitionRequested v TransitionInProgress v TransitionComplete --> Ready 요청 수 한도 도달 시의 재기동 Ready --> Rebooting --> RebootReady --> Ready 서버 종료 시의 대표 경로 Ready / Inactive --> ShuttingDown --> Done --> Reserved

phpThread.setHandler()는 기존 처리기가 안전한 전환 지점에 도달하기를 기다린 뒤 새 처리기를 설치합니다. ThreadState.WaitFor()는 상태 확인과 대기 구독 등록을 같은 잠금 안에서 수행합니다. 확인 직후 알림이 먼저 지나가 영원히 기다리는 경쟁 상황을 피하는 방식입니다.

phpMainThread.rebootAllThreads()의 긴 주석도 좋은 교재입니다. PHP의 TSRM 전역 자원을 정리하기 전에 모든 PHP 스레드가 종료되어야 하므로, 살아 있는 스레드를 남겨 두고 재부팅을 진행하지 않습니다. 이 지점에서 “빨리 복구하기”보다 메모리 수명 보장이 먼저라는 판단을 읽을 수 있습니다.

4-4. 패턴 ④ 수명 분리: 스크립트·요청·메모리는 같은 단위가 아니다

일반 모드의 frankenphp.c 내부 php_thread()는 php_request_startup() → 스크립트 실행 → php_request_shutdown()을 반복합니다. 워커는 그 스크립트 실행 안에서 frankenphp_handle_request() 호출을 반복합니다. 따라서 워커 스크립트의 종료와 개별 HTTP 요청의 종료가 서로 다른 사건이 됩니다.

threadworker.go의 waitForWorkerRequest()는 새 요청을 기다리고, go_frankenphp_finish_worker_request()는 요청 문맥을 닫습니다. phpThread에 포함된 runtime.Pinner는 C가 참조하는 Go 메모리의 수명을 유지하며, Unpin()은 다음 워커 요청 대기 등 정리 경계에서 수행됩니다.

요청이 끝났다는 사실만으로 워커의 객체와 외부 참조까지 모두 사라지는 것은 아니므로, 각 자원의 수명 경계를 따로 확인해야 합니다. 특히 docs/worker.md는 대부분의 슈퍼글로벌과 달리 $_ENV는 요청 사이에 초기화되지 않는다고 명시합니다. 앱 전역 변수와 정적 속성도 별도 정리가 필요합니다.

문서와 구현의 차이
워커 실패를 모두 같은 재시작 정책으로 설명하면 틀립니다

docs/worker.md의 실패 절은 비정상 종료에 지수 백오프를 설명합니다. 하지만 threadworker.go의 tearDownWorkerScript()는 이미 요청 대기에 도달한 워커의 종료에는 즉시 재시작 경로를 사용하고, 연속 시작 실패에만 대기를 적용합니다.

대기식도 failureCount * failureCount * 100ms에 상한 1s를 둡니다. 식의 값은 0·100·400·900·1000ms 순서이며 지수식이 아닙니다. 시작 실패 횟수 제한과 파일 감시 여부도 결과에 관여합니다. 커밋 d138a78의 동작을 설명할 때는 실제 분기와 식을 기준으로 읽어야 합니다.

5디렉토리 구조 해부

먼저 실행 경로, 다음에 도구·테스트·배포를 읽는다
frankenphp/ ├── go.mod / go.sum 코어 Go 모듈 ├── frankenphp.go 초기화·종료·Go 콜백 ├── frankenphp.c / frankenphp.h PHP SAPI·C 실행 루프 ├── cgo.go 플랫폼별 C 컴파일·링크 설정 ├── server.go / context.go 서버 범위·요청 문맥 ├── phpthread.go 네이티브 PHP 스레드의 Go 표현 ├── phpmainthread.go PHP 엔진 전체 수명 ├── threadregular.go 일반 요청 처리 ├── threadworker.go / worker.go 워커 실행·요청 배치 ├── scaling.go / metrics.go 스레드 증감·관측 ├── caddy/ │ ├── go.mod Caddy 통합용 별도 Go 모듈 │ ├── app.go / module.go 앱 수명·HTTP 처리·설정 해석 │ └── frankenphp/ │ ├── main.go 실행 파일 진입점 │ └── Caddyfile 기본 서버 설정 ├── internal/ │ ├── state/ 상태 전환과 대기 동기화 │ ├── cpu/ / memory/ 자원 관측과 한도 추정 │ ├── watcher/ 파일 감시 통합 │ ├── extgen/ Go 기반 PHP 확장 코드 생성 │ ├── phpheaders/ / fastabs/ 헤더·경로 처리 보조 │ └── testcli/ · testext/ · testserver/ 테스트용 도구 ├── testdata/ PHP 픽스처·Caddy 설정·부하 시나리오 ├── tests/ 재적재·종료 등 셸 통합 검사 ├── profiles/ 프로파일링용 앱·빌드·측정 도구 ├── package/ 배포 패키지와 초기 페이지 ├── docs/ 기능·운영·내부 구조·번역 문서 ├── .github/ 자동화 작업과 CI 행렬 └── Dockerfile · docker-bake.hcl · craft.yml · build-static.sh
읽을 영역역할초보자가 먼저 던질 질문
루트 Go·C 파일서버의 실제 실행 엔진누가 요청을 받고, 누가 PHP 스레드를 깨우는가?
caddy/코어를 웹 서버 제품으로 연결설정 문자열이 어떤 Go 구조체로 바뀌는가?
internal/state/동시 실행 사이의 전환 조율종료 중인 스레드에 새 역할을 주지 않으려면?
internal/extgen/Go 선언으로 PHP 확장 연결 코드 생성같은 선언을 여러 언어로 일관되게 옮기려면?
testdata/·테스트오류·취소·입출력 경계의 사례성공 요청 외에 어떤 실패 경로를 보장하는가?
배포·CI 파일운영체제·PHP·네이티브 조합 검증한 환경의 빌드 성공을 다른 환경에 일반화할 수 있는가?

중요한 보조 경로: 확장 코드 생성기

internal/extgen/generator.go의 Generator.Generate()는 선언을 분석하고, 스텁 → 인수 정보 → 헤더 → C → Go → 문서를 차례로 생성합니다. internal/extgen/funcparser.go는 parser.ParseFile(..., parser.ParseComments)로 Go 구문 트리와 주석을 함께 읽습니다. 함수 선언을 구문 트리로 분석해 연결 코드를 생성하는 작은 컴파일러 사례입니다.

이 기능의 API에는 EXPERIMENTAL 표시가 있습니다. 서버 요청 경로를 이해한 다음에 읽을 확장 주제이며, 처음부터 전체 생성기를 이해할 필요는 없습니다. 우선 caddy/frankenphp/main.go → caddy/module.go → server.go → 두 스레드 처리기를 읽으면 핵심 연결이 보입니다.

6학습 포인트 (기술별)

문법을 아는 단계에서 수명·동시성·관측을 설명하는 단계로
기술이 저장소에서 배울 것작은 실습 아이디어
Go 채널·select즉시 전달, 공유 대기, 취소, 시간 제한을 하나의 흐름으로 구성일꾼 두 개짜리 별도 Go 예제로 취소 시 대기 수가 줄어드는지 확인
잠금·상태 머신상태 확인과 구독 등록을 원자적인 절차로 묶기WaitFor의 잠금 범위를 종이에 표시하고 알림 누락 가능성을 설명
CGO·메모리C에 전달한 Go 포인터의 보존 기간과 해제 책임pinString·pinCString·Unpin 호출 관계를 수명 표로 정리
PHP 워커요청 상태와 앱 상태를 구분하고 재사용 가능한 경계를 설계워커 한 개에서 정적 카운터와 요청별 변수를 비교
Caddy 모듈등록·설정 해석·준비·시작·요청 처리를 분리php_server 지시어가 처리기와 파일 서버를 만드는 경로 추적
HTTP·시간 제한큐 대기·본문 읽기·PHP 실행·응답 쓰기의 시간을 구분느린 업로드와 빈 스레드 대기가 서로 다른 제한임을 도식화
관측·성능처리량 외에 대기열·사용 중 스레드·재시작을 함께 읽기느린 DB 가정에서 CPU가 낮아도 지연이 커지는 이유 설명
테스트·빌드Go 경합과 C 메모리 오류를 다른 도구로 찾기-race·ASan·MSan이 각각 놓칠 수 있는 문제를 표로 비교

6-1. “요청 완료”를 한 번 더 정의해 보기

context.go의 frankenPHPContext에는 요청 문맥, 응답 작성자, done 채널, isDone과 클라이언트 연결 상태가 들어 있습니다. HTTP 핸들러 반환, 응답 전송 완료, PHP 후처리 완료는 언제나 같은 순간이 아닙니다. frankenphp_finish_request()가 있는 시스템에서 이 구분은 특히 중요합니다.

동시성 코드를 배울 때는 정상 경로보다 취소·종료·재시작에서 누가 마지막 정리를 맡는지 먼저 확인하는 습관이 유용합니다. threadregular_test.go의 TestCancelledRequestLeavesRegularQueue()는 취소된 대기 요청이 큐 카운터에 남지 않는지 확인하는 좋은 출발점입니다.

6-2. 테스트는 운영 사고의 압축된 설명서다

TestRegularRequestChanIsCreatedBeforeInitRuns()는 초기화 전후 틈에 요청이 도착해도 nil 채널에서 영원히 기다리지 않도록 채널이 미리 존재하는지 확인합니다. TestRequestsQueuedBeforeThreadsAreReadyAreHandedOver()는 준비 전 대기 요청이 실제로 전달되고 완료되는지 확인합니다. 함수 이름, 사전 상태, 마지막 단언을 차례로 읽으면 테스트가 막는 사고를 설명할 수 있습니다.

scaling_test.go는 증설 후 Ready 상태와 처리기 타입을 검사하고, 축소 후에는 inactiveThread로 바뀌는지 확인합니다. 축소를 “스레드가 즉시 완전히 사라진다”로 해석하지 않는 근거입니다. worker_internal_test.go와 requestbodytimeout_test.go는 재시작·느린 입력의 경계 사례를 확장해 줍니다.

학습 결과물
설명할 수 있으면 이해한 것이다

파일 이름을 많이 외우는 대신 세 장을 만들어 보세요. 요청 흐름도 한 장, 스레드 상태 전이도 한 장, 앱·요청·메모리 수명 표 한 장입니다. 각 화살표 옆에 실제 함수 이름을 붙이면 추상적인 동시성 개념이 구체적인 실행 경로가 됩니다.

7하드웨어 / 시스템 요구사항

고정된 최소 사양보다 PHP 빌드 조건과 동시 실행 예산이 먼저다

7-1. 실행 파일 사용과 소스 빌드를 구분한다

항목확인된 조건학습·운영 시 해석
운영체제README는 Linux·macOS Apple Silicon·Windows 바이너리 안내플랫폼마다 네이티브 의존성·확장·타임아웃 동작을 확인
PHP 버전checkPHPConfig()는 PHP 8.2 미만을 거절기본 배포의 PHP 8.5와 최소 허용 버전을 구분
PHP 빌드compile 문서: embed·ZTS 활성화, Zend signals 비활성화일반 CLI PHP 설치만으로 libphp·개발 헤더가 충족되지는 않음
Go 빌드go.mod는 1.27.0, CGO와 플랫폼별 C 도구 체인 필요완성 바이너리 실행에 Go 컴파일러를 설치할 필요는 없음
추가 라이브러리Watcher·Brotli 등은 선택 기능과 연결nowatcher·nobrotli·nomercure 빌드 태그도 함께 확인
네트워크Dockerfile은 80·443/TCP, 443/UDP, 2019 선언443/UDP는 HTTP/3 관련. 포트 선언 자체가 외부 공개 설정은 아님
CPU·RAM설정 파일에 모든 앱에 공통인 최소 코어·RAM 수치는 없음앱의 메모리와 동시 실행 수를 바탕으로 예산 결정

문서가 요구하는 정상적인 병렬 실행 환경은 ZTS 빌드입니다. 소스의 Init()에는 ZTS가 없으면 시작 스레드 수를 1로 낮추고 경고하는 경로도 있습니다. 이는 문서의 권장·필수 빌드 조건과 구현의 방어적 처리 사이의 차이이며, NTS 환경의 모든 조합이 검증됐다는 보장은 아닙니다.

checkPHPConfig()는 ZTS와 Zend signals가 함께 활성화된 조합을 거절합니다. Linux에서 Zend Max Execution Timers가 없으면 실행 시간 제한이 동작하지 않는다는 경고도 있습니다. “PHP 8.x라서 된다”보다 빌드 기능까지 확인하는 것이 중요합니다.

7-2. 스레드 수 계산을 숫자로 이해한다

calculateMaxThreads()는 2 × runtime.GOMAXPROCS(0)를 기본 예산 계산에 사용합니다. 아래는 ZTS 환경에서 GOMAXPROCS=4인 예입니다. 운영체제가 표시하는 전체 코어 수와 Go가 사용하는 동시 실행 기준이 반드시 같지는 않습니다.

설정 사례일반 처리 + 워커시작 PHP 처리 스레드 합계
스레드·워커 지정 없음일반 8 + 워커 08
워커 한 종류, num 생략일반 최소 1 + 기본 워커 89
num_threads 2, 워커 num 3일반 2 + 워커 35
위 설정 + max_threads 8시작 5, 남은 증설 여지 35에서 최대 8까지

별도 주 PHP 스레드 등 관리용 실행 단위는 이 처리 슬롯 계산과 구분합니다. num_threads를 명시하면 일반 요청용 스레드에 워커 수가 더해지고, max_threads는 두 종류를 합친 처리 스레드 한도입니다. 따라서 “워커도 기본 두 배”라는 짧은 설명만으로 전체 메모리 사용량을 계산하면 틀리기 쉽습니다.

# 메모리 예산을 잡는 개념식이며 실제 RSS 보장식은 아니다.
필요 예산 ≈ PHP 동시 실행 메모리
          + 장기 보존 앱 상태·공유 캐시
          + Go·Caddy·네이티브 확장·연결 버퍼·운영체제 여유

예: 처리 슬롯 5개 × memory_limit 128MiB = 640MiB
    여기에 위의 추가 메모리와 여유 공간을 별도로 잡는다.

max_threads auto는 phpmainthread.go에서 시스템 메모리를 PHP의 memory_limit로 나눠 한도를 추정하며, 추정 불가 시 시작 스레드 수의 두 배를 사용합니다. Linux의 internal/memory/memory_linux.go는 syscall.Sysinfo()를 사용하므로 이 경로 자체가 컨테이너의 cgroup 한도를 직접 읽는 구현은 아닙니다.

자원 관리의 경계
auto와 memory_limit은 프로세스 전체의 안전 장치가 아닙니다

PHP의 메모리 제한, Go의 GOMEMLIMIT, 컨테이너 메모리 제한은 서로 다른 범위를 제어합니다. auto도 Go·네이티브 메모리와 운영 여유를 모두 예약해 주는 식이 아닙니다. 실제 프로세스 메모리와 대기열을 관측하면서 상한을 정해야 합니다.

docs/known-issues.md는 특정 PHP 버전의 하드 메모리 한도 초과가 전체 프로세스를 종료할 수 있다고 설명합니다. 단일 프로세스에 여러 요청이 함께 있으므로 감독 프로세스의 재시작 정책도 필요합니다. 서버 종료의 유예 시간이 지나도 플랫폼·블로킹 호출에 따라 스레드 종료가 지연될 수 있다는 점도 phpthread.go의 주석에 나옵니다.

8직접 해볼 수 있는 실습 과제

별도의 demo 프로젝트에서 설치된 FrankenPHP로 진행하는 네 단계

앞부분은 소스 읽기만으로 따라갈 수 있습니다. 아래 실행 실습은 PHP를 포함한 FrankenPHP 실행 파일이 준비된 환경을 전제로 합니다. 예제 파일은 별도의 demo/ 프로젝트에 두고, 명령은 그 프로젝트의 루트에서 실행합니다. 외부 데이터베이스나 실제 사용자 데이터 없이도 수명과 대기 동작을 관찰할 수 있습니다.

과제 1

일반 모드로 요청 경로 확인 입문

public/index.php에 아래 코드를 저장합니다. 응답의 PHP_SAPI 값으로 PHP가 어떤 실행 환경에서 돌아가는지 확인합니다.

<?php
header('Content-Type: text/plain; charset=utf-8');
echo '실행 환경: ' . PHP_SAPI . "\n";
echo '요청 경로: ' . ($_SERVER['REQUEST_URI'] ?? '') . "\n";
# 첫 번째 터미널
frankenphp php-server --root public --listen 127.0.0.1:8080

# 두 번째 터미널
curl 'http://127.0.0.1:8080/index.php?hello=1'

완료 기준: 실행 환경에 frankenphp가 나오고 쿼리 문자열을 포함한 요청 경로를 확인합니다. public/note.txt도 만들어 요청한 뒤, PHP 실행과 정적 파일 응답이 갈라지는 지점을 구조도에 표시합니다. 근거 파일은 caddy/module.go의 parsePhpServer()입니다.

과제 2

정적 변수가 요청을 넘어 살아남는지 관찰 중급

앞의 public/index.php를 아래 워커 코드로 교체합니다. 이전 서버를 종료하고, 워커를 한 개만 지정해 다시 시작합니다. 여러 워커를 쓰면 카운터가 각 워커에 따로 생깁니다.

<?php
$handler = static function (): void {
    static $count = 0;
    ++$count;
    $name = $_GET['name'] ?? '익명';
    header('Content-Type: text/plain; charset=utf-8');
    echo "이 워커의 요청 수: {$count}\n";
    echo "이번 요청의 이름: {$name}\n";
};

while (frankenphp_handle_request($handler)) {
    gc_collect_cycles();
}
frankenphp php-server --root public --listen 127.0.0.1:8080 \
  --worker "$PWD/public/index.php,1"

# 다른 터미널에서 순서대로 요청한다.
curl 'http://127.0.0.1:8080/index.php?name=A'
curl 'http://127.0.0.1:8080/index.php?name=B'

완료 기준: 같은 워커에서 카운터가 1·2로 증가하고 이름은 A·B로 바뀝니다. 카운터는 보존되지만 $_GET은 새 요청 값으로 바뀌는 이유를 설명합니다. 정적 카운터가 전체 서비스의 전역 요청 수는 아니라는 점과, 재시작하면 초기화된다는 점도 적습니다.

추가 질문: gc_collect_cycles()만 호출하면 정적 배열에 계속 추가한 값도 사라질까요? 계속 참조되는 객체와 도달할 수 없는 순환 참조는 다릅니다. 수집기 호출이 상태 정리 코드를 대신하지는 않습니다.

과제 3

스레드 부족과 대기 시간 초과 구분 중급

워커 콜백 안에 usleep(200_000);을 넣어 느린 작업을 흉내 냅니다. CLI로 실행하던 서버를 종료하고 아래 Caddyfile로 실행합니다. 일반 처리 1개와 워커 1개의 합계가 2이며, 최대치도 2여서 자동 증설 여지가 없습니다.

{
    debug
    frankenphp {
        num_threads 1
        max_threads 2
        max_wait_time 100ms
        php_ini memory_limit 128M
        worker ./public/index.php 1
    }
}

http://127.0.0.1:8080 {
    root * public
    php_server
}
frankenphp run --config Caddyfile --adapter caddyfile

# 다른 터미널에서 요청 세 개를 겹쳐 보낸다.
for n in 1 2 3; do
  curl -s -o /dev/null -w '%{http_code}\n' \
    'http://127.0.0.1:8080/index.php' &
done
wait

완료 기준: 요청 시점에 따라 일부 요청에서 503을 관찰하고, worker.go의 대기 분기와 ErrMaxWaitTimeExceeded의 HTTP 상태 값을 연결합니다. max_wait_time은 처리 슬롯을 기다리는 시간이며 PHP 실행 시간 제한이 아니라는 점을 설명합니다.

이어서 max_threads 4로 바꾸고 요청 수를 조금씩 늘려 증설 로그가 나타나는 조건을 관찰합니다. CPU 관측과 지연 기준 때문에 증설이 즉시 보장되지는 않습니다. 워커가 늘면 과제 2의 카운터도 여러 개로 나뉜다는 사실을 함께 확인합니다.

과제 4

요청 사이의 정보 누출과 재시작 경계 검증 고급

워커에 “현재 사용자”를 보관하는 간단한 서비스 객체를 만들고, 콜백 종료 시 그 값을 비우는 reset()을 구현합니다. 프레임워크 없이 한다면 요청 처리의 try/finally 경계에서 정리하도록 설계합니다. 테스트 식별자는 A·B 같은 가짜 값만 사용합니다.

  1. 요청 A에 사용자 값을 넣고 다음 익명 요청에서 그 값이 보이지 않는지 확인합니다.
  2. 처리 도중 예외를 발생시켜도 정리가 수행되는지, 예외를 콜백 안에서 처리했는지 확인합니다.
  3. frankenphp 전역 설정에 실험적 옵션 max_requests 10을 넣어 워커 재기동을 관찰합니다.
  4. 정상 요청 30회 동안 응답 오류·재시작·메모리 변화를 기록하고, 상태 초기화와 누수 완화를 구분합니다.
  5. 반복문에 도달하기 전 시작 실패와, 준비된 워커의 처리 중 실패를 tearDownWorkerScript() 분기로 설명합니다.

완료 기준: 사용자 정보가 다음 요청으로 새지 않고, 오류 뒤에도 정상 요청을 처리합니다. max_requests에 의한 재시작을 누수 원인의 수정과 혼동하지 않는 짧은 검증 보고서를 남깁니다. 재시작 뒤에도 반드시 유지할 데이터라면 워커 메모리 밖에 보관해야 한다는 결론까지 연결합니다.

실습의 성공은 빠른 응답 숫자 하나가 아니라, 요청 사이에 남겨도 되는 상태와 반드시 지워야 할 상태를 구분해 설명하는 것입니다. 성능을 비교할 때는 같은 PHP 버전·확장·앱·부하를 사용하고 초기화 구간과 정상 상태를 따로 기록합니다.

9관련 기술 심화 학습 로드맵

6주 동안 요청 흐름에서 시스템 경계까지 넓혀 가기

PHP의 함수·배열·클로저, Go의 구조체·인터페이스를 먼저 익히면 좋습니다. C 문법을 완벽하게 알 필요는 없지만 포인터·메모리 할당·콜백의 기본 뜻은 4주차 전에 정리합니다. 매주 실행 결과와 함께 “왜 이 결과가 나오는가”를 설명하는 산출물을 남깁니다.

주차집중 주제와 읽을 코드완료 결과물
1주차HTTP 요청·응답, Caddy 라우팅 · caddy/frankenphp/main.go, caddy/caddy.go, caddy/module.go정적 파일과 PHP 요청의 두 경로를 그린 구조도, 과제 1
2주차채널·select·취소·잠금 · threadregular.go, internal/state/state.go대기 요청이 완료·취소·시간 초과로 빠지는 경로 표
3주차PHP 앱 부팅·워커 수명·상태 정리 · worker.go, threadworker.go과제 2와 사용자 상태 분리 실험, 일반 모드와 워커 비교 노트
4주차CGO·SAPI·포인터 수명 · cgo.go, phpthread.go, frankenphp.cecho의 출력 경로와 Pin·Unpin의 책임자를 표시한 수명 표
5주차관측·용량·입출력 시간 제한 · scaling.go, metrics.go, requestbodytimeout_test.go과제 3의 한도별 응답·대기·증설 기록
6주차이식성·빌드·회복 · Dockerfile, craft.yml, CI, internal/extgen/플랫폼별 의존성 표와 과제 4의 실패·복구 검증 보고서

다음 갈림길을 고르는 기준

파일을 끝까지 읽는 것보다 하나의 요청·하나의 실패·하나의 자원이 어디서 시작하고 끝나는지 설명할 수 있는 것이 더 좋은 학습 기준입니다. 스레드 수나 성능 수치를 외우기보다 그 값을 바꿨을 때 무엇이 함께 바뀌는지 예측해 보세요.

10핵심 키워드 사전

이 글의 코드를 읽는 데 필요한 최소 어휘
용어쉬운 설명
애플리케이션 서버HTTP 요청을 받아 앱 코드를 실행하고 응답을 돌려주는 프로그램
CaddyHTTPS와 라우팅을 담당하며 모듈로 기능을 확장하는 Go 웹 서버
PHP-FPM여러 PHP 자식 프로세스를 관리하고 FastCGI 요청을 처리하는 실행 관리자
FastCGI웹 서버와 별도 앱 실행기 사이에서 요청·응답 정보를 주고받는 프로토콜
SAPIPHP 엔진과 웹 서버·CLI 같은 바깥 실행 환경을 연결하는 인터페이스
CGOGo 프로그램이 C 함수·자료형·네이티브 라이브러리와 연결되는 기능
ZTS·TSRMZTS는 PHP의 스레드 안전 빌드 방식이며, TSRM은 스레드별 런타임 자원 관리 체계
goroutineGo 런타임이 운영체제 스레드 위에서 배치하는 가벼운 실행 단위
스레드 풀준비된 여러 실행 스레드에 요청을 배정해 재사용하는 구조
채널·selectGo 실행 단위 사이의 전달 통로와, 여러 전달·종료 조건을 기다리는 구문
역압처리 용량이 부족할 때 대기·제한·거절로 상류의 과도한 일을 제어하는 것
상태 머신준비·실행·종료 같은 상태와 허용되는 전환을 명시적으로 관리하는 방식
워커 모드PHP 앱을 부팅한 상태로 유지하며 콜백을 통해 여러 요청을 처리하는 모드
슈퍼글로벌$_GET·$_SERVER처럼 PHP 여러 범위에서 접근하는 요청·환경 정보 변수
OPcachePHP의 컴파일된 명령을 저장해 반복 컴파일을 줄이는 기능. 앱 객체 보존과는 별개
Pin·UnpinC에서 참조할 Go 메모리를 살아 있게 고정하고, 그 고정을 해제하는 동작
백오프실패를 반복할 때 재시도 사이에 기다리는 정책. 증가식과 상한을 함께 확인
Mercure·SSEMercure는 실시간 업데이트를 전달하는 기술이며, SSE는 서버가 브라우저에 이벤트를 보내는 HTTP 방식
Early Hints최종 응답 전 103 상태로 필요한 자원 정보를 먼저 알리는 HTTP 기능
Prometheus 메트릭스레드 수·대기 요청 수 같은 수치를 이름 붙여 관측 시스템에 제공하는 데이터
ASan·MSan·-race각각 메모리 접근 오류, 초기화되지 않은 메모리 사용, Go의 데이터 경합을 찾는 도구
AST·스텁·arginfoAST는 코드의 구문 트리, 스텁은 API 선언, arginfo는 PHP가 쓰는 함수 인수 정보
정적 링크·동적 링크라이브러리를 실행 파일에 묶는 방식과 실행 시 외부 라이브러리에 연결하는 방식

11참고 링크

설명과 구현을 함께 읽기 위한 공식 출발점

공식 저장소·문서·관련 기술

커밋을 포함한 링크는 이 글의 코드 기준을 고정합니다. 일반 공식 문서와 라이브러리 API 문서는 이후 바뀔 수 있으므로, 동작 차이를 발견하면 d138a78 시점의 구현과 설정 파일을 함께 대조합니다.

현재 상태 (2026-10-23 GitHub 기준)

항목기준 정보
선정GitHub Trending Go Weekly #11 · 2026-10-23 분석
스타 / 포크11,454 / 500 · 2026-10-23 GitHub 기준
저장소 생성일2022-03-25
주 언어 / 설명Go / 🧟 The modern PHP app server
최신 릴리스v1.13.2 · 2026-10-21
최근 push2026-10-23
최근 커밋d138a78 · 2026-10-23 · chore: bump Go dependencies (#2751)
추적 파일 규모606개 · 커밋 d138a78 기준
Go160개 파일 / 32,756줄 · 이 중 _test.go 69개 / 20,521줄
비테스트 Go91개 파일 / 12,235줄 · 테스트 보조 도구가 일부 포함되므로 모두 서버 구현 코드는 아님
C / C 헤더C 4개 / 2,452줄 · 헤더 6개 / 666줄
PHP / JavaScriptPHP 115개 / 2,982줄 · JavaScript 7개 / 263줄
셸 / YAML셸 24개 / 1,403줄 · YAML 24개 / 2,336줄
문서Markdown 212개 / 39,063줄 · 여러 언어 번역 포함
라이선스루트 LICENSE는 MIT · 포함되는 외부 구성 요소는 별도 확인

줄 수는 Git 추적 파일을 확장자로 묶어 센 물리적 줄 수의 대략치입니다. 주석·빈 줄·테스트·픽스처·생성 헤더를 포함하며, 외부 의존성의 소스 줄 수를 더한 수치가 아닙니다. Go 줄 수의 약 63%가 _test.go에 있지만 이 비율은 테스트 커버리지나 품질 점수가 아닙니다. 제한된 커밋 스냅샷만으로 전체 커밋 수나 기여자 기여 비중을 추정하지 않습니다.

쓰기 전에 알아둘 한계
호환성·성능·라이선스는 배포하려는 구성별로 판단해야 합니다

README의 폭넓은 PHP 앱 지원 설명에는 조건이 있습니다. 기준 커밋의 알려진 문제 문서는 imap·newrelic·pcov 등 비호환 확장과 imagick 등의 주의점을 명시합니다. 워커는 전역·정적 상태와 $_ENV를 다음 요청으로 유지할 수 있어 앱별 정리가 필요합니다.

이 글에는 실측 처리량·지연·메모리 벤치마크 결과가 없습니다. 자동 HTTPS·HTTP/3·단일 실행 파일이라는 기능도 인증서 접근성, 네트워크, 빌드 형태와 설정에 따라 조건이 달라집니다. 단일 프로세스의 네이티브 치명 오류는 여러 진행 중 요청에 영향을 줄 수 있습니다.

코어는 MIT지만 docs/compile.md는 선택 구성인 Mercure Go 라이브러리를 AGPL로 표시합니다. 코어의 라이선스 표기만으로 선택 모듈·PHP 확장·최종 배포물의 조건까지 같다고 일반화하지 마세요. 설정 기본값, 실험적 API, PHP 버전별 동작은 이후 커밋에서 달라질 수 있습니다.