author
postNo
status
thumbnail
description
category
tags
createdAt
updatedAt
같은 코드를 왜 요청마다 다시 준비할까?1. PHP 코드는 실행 전에 어떤 과정을 거칠까?2. OPcache를 켜면 무엇이 달라질까?3. 내부에서는 어떻게 캐시를 찾고 재사용할까?컴파일 진입점에서 캐시를 먼저 확인한다opcode만 따로 떼어 저장하는 것은 아니다같은 캐시 인스턴스를 사용하는 worker가 공유한다4. 실제로 캐시된 코드는 다시 실행될까?5. 코드를 바꾸면 캐시는 언제 바뀔까?무효화와 전체 초기화의 범위를 구분한다실제 요청을 처리하는 PHP에 적용해야 한다6. 메모리는 어떻게 쓰이고, 왜 부족해질까?7. 설정 방법정리참고 자료
같은 코드를 왜 요청마다 다시 준비할까?
PHP 웹 서비스에 같은 요청이 연달아 들어온다고 생각해보자. 사용자의 입력과 데이터베이스 값은 달라질 수 있지만, 서버에 배포된 PHP 코드는 대부분 그대로다. 그런데 실행할 코드가 같아도 PHP는 그 코드를 실행 가능한 형태로 준비하는 비용을 치른다.
OPcache는 이 반복을 줄인다. PHP 소스를 파싱하고 컴파일해 만든 opcode를 공유 메모리에 보관한 뒤 재사용한다. opcode는 Zend Engine이 실행하는 명령이다. 여기서 캐시하는 대상은 완성된 HTML이나 데이터베이스 조회 결과가 아니라 컴파일된 코드다. PHP 공식 문서: OPcache
나는 OPcache를 이해할 때 “얼마나 빨라질까?”보다 “어느 작업이 사라지고 어느 작업은 남을까?”를 먼저 구분하는 편이 낫다고 생각한다. 이 구분이 되어야 캐시 적중률이 높은데도 응답이 느린 이유와, 배포한 코드가 바로 반영되지 않는 이유를 함께 설명할 수 있다.
이 글은 일반적인 Linux·PHP-FPM 웹 요청을 중심으로 설명한다. 내부 구현은 PHP 8.4 계열 소스를 참고했고, 작은 실행 예제는 PHP 8.4.25에서 확인했다. 장시간 살아 있는 애플리케이션 서버의 상태 관리나 JIT 자체의 최적화 알고리즘은 별도 주제다.
1. PHP 코드는 실행 전에 어떤 과정을 거칠까?
PHP를 인터프리터 언어라고 부르지만, 소스 문자열을 매번 한 줄씩 읽어 곧바로 실행하는 것으로 이해하면 중간 과정이 빠진다. Zend Engine은 소스를 분석하고 내부 명령으로 바꾼 뒤 실행한다.
- 토큰화: 소스를 변수, 연산자, 키워드 같은 단위로 나눈다.
- 파싱: 토큰의 문법과 구조를 분석해 AST를 만든다. AST는 코드의 구조를 나타내는 트리다.
- 컴파일: AST를 Zend VM이 실행할 opcode로 바꾼다. VM은 이 명령을 해석하고 실행하는 가상 머신이다.
- 실행: 만들어진 명령에 따라 계산하고 함수를 호출하며 응답을 만든다.

PHP 7에서 도입된 AST 기반 컴파일 구조는 공식 RFC에서 확인할 수 있다. 실제 구현에는 선언 처리와 여러 최적화 단계가 더 있지만, 처음에는 “소스 분석 → 실행할 명령 생성 → 명령 실행”으로 나누면 충분하다. PHP RFC: Abstract Syntax Tree
예를 들어 다음 코드는 입력값에 1을 더한다.
이 함수의 opcode에는 인자를 받아 덧셈을 하고 반환하는 명령 등이 들어간다.
$value에 어떤 숫자가 들어오는지는 실행할 때 결정된다. 따라서 같은 명령을 재사용해도 호출마다 다른 결과를 얻을 수 있다.프레임워크와 라이브러리까지 많은 PHP 코드를 읽는 애플리케이션이라면, 이 준비 작업도 반복 비용이 된다. 운영체제가 원본 데이터를 메모리에 보관해 디스크 읽기를 줄여주더라도 PHP의 파싱과 컴파일까지 대신해주지는 않는다.
2. OPcache를 켜면 무엇이 달라질까?
캐시에 없는 PHP 코드를 처음 불러올 때는 소스를 분석하고 컴파일한다. OPcache는 설정된 최적화를 적용한 결과를 캐시에 보관한다. 이후 같은 코드를 불러올 때 유효한 캐시가 있으면 그 결과를 사용한다.

그림의 위쪽은 OPcache가 활성화되어 있지만 아직 해당 코드가 캐시되지 않은 경우다. 아래쪽은 유효한 캐시에 적중한 경우다. 아래쪽에서도 마지막 실행 단계는 매번 수행한다.
OPcache의 최적화는 불필요한 명령 제거, 상수로 계산할 수 있는 표현식 정리 등을 포함한다. 다만 요청 입력이나 외부 응답에 따라 달라지는 일을 미리 실행해 저장하는 것은 아니다. PHP 공식 문서: 최적화 설정
예를 들어 캐시된 코드가 데이터베이스를 조회하면 그 조회는 다시 실행한다. 반복문도 다시 돌고, 외부 API도 다시 호출한다. “OPcache를 켰는데 느린 SQL은 그대로다”라는 결과는 이 동작 때문이다.
프레임워크 초기화도 구분해야 한다. 초기화에 필요한 코드의 컴파일 비용은 줄어들지만, 요청마다 객체를 만들고 설정을 읽는 실행 로직까지 전부 없어지는 것은 아니다.
3. 내부에서는 어떻게 캐시를 찾고 재사용할까?
컴파일 진입점에서 캐시를 먼저 확인한다
PHP 8.4 계열 구현을 보면 OPcache는
zend_compile_file이 가리키는 함수를 persistent_compile_file로 교체한다. 파일을 컴파일하려는 시점에 OPcache가 먼저 개입할 수 있는 구조다.이 함수는 경로 등의 정보를 반영한 키로 캐시를 찾고, 필요한 유효성 검사를 거친다. 캐시가 없거나 오래된 항목이면 컴파일 경로로 들어간다. 유효한 항목이면 저장된 스크립트를 현재 실행에 사용할 수 있도록 불러온다. 단순히 URL 문자열 하나를 키로 HTML을 찾는 구조와는 다르다.
아래는 설명용 의사 코드다. PHP 내부의 실제 분기 순서를 전부 옮긴 것은 아니다.
캐시에 저장할 수 없는 코드도 일반 컴파일 경로로 실행할 수 있다. 그러므로 요청이 성공했다는 사실만으로 캐시에 저장됐다고 판단할 수는 없다. PHP 8.4 캐시 조회·컴파일 구현
opcode만 따로 떼어 저장하는 것은 아니다
실행에는 명령 배열 외에도 함수와 클래스의 정의, 리터럴, 문자열 같은 정보가 필요하다. OPcache는 이런 정보를 포함한 스크립트 구조를 지속해서 사용할 수 있는 형태로 보관한다. 구현에서는
zend_persistent_script, zend_op_array 같은 구조와 zend_accel_script_persist 함수를 확인할 수 있다.여기서 persistent는 요청이 끝나도 재사용할 수 있다는 뜻이다. 해당 메모리에 있는 모든 값이 애플리케이션 전역 변수처럼 공유된다는 뜻은 아니다. 캐시할 코드와 요청 실행 중 바뀌는 상태를 구분해야 한다. PHP 8.4 컴파일 결과 저장 구현
같은 캐시 인스턴스를 사용하는 worker가 공유한다
PHP-FPM은 요청을 처리하는 worker 프로세스를 관리한다. 같은 OPcache 인스턴스를 사용하는 worker들은 공유 메모리에 있는 컴파일 결과를 재사용할 수 있다. 각 worker가 모든 컴파일 결과를 독립적으로 다시 만드는 비용을 줄이는 구조다.

공유 메모리는 여러 프로세스가 같은 메모리 영역을 볼 수 있도록 운영체제가 제공하는 기능이다. PHP의 일반적인 요청 변수와 객체는 요청 실행 상태에 속한다. OPcache에 코드를 저장했다고 사용자의 장바구니나 지역 변수가 다음 요청으로 자동 보존되지는 않는다.
또한 “서버 전체가 무조건 하나의 캐시를 쓴다”라고 일반화하면 안 된다. 프로세스 구성과 실행 방식에 따라 경계가 달라진다. 별도 서버나 컨테이너 사이에서 OPcache를 자동으로 동기화해주는 분산 캐시 기능은 없다. PHP 8.4 공유 메모리 할당 구현
4. 실제로 캐시된 코드는 다시 실행될까?
작은 예제로 컴파일과 실행을 분리해보자. 아래 코드는 임시 스크립트를 만든 뒤 먼저 컴파일만 하고, 이후
include로 두 번 실행한다. opcache_compile_file()은 스크립트를 실행하지 않고 컴파일해 캐시에 넣는 함수다. PHP 공식 문서: opcache_compile_file실험은 공식 PHP Docker 이미지의 PHP 8.4.25 CLI·NTS에서 실행했다.
opcache.enable_cli=1, opcache.file_update_protection=0, opcache.jit=disable을 적용했다. 갓 만든 임시 코드도 곧바로 캐시하려고 이 실험에서만 파일 갱신 보호 시간을 0으로 두었다. 일반 운영 설정으로 그대로 권하는 값은 아니다.실행 결과는 다음과 같았다.
컴파일 직후에는 카운터가 0이다. 캐시에 들어갔지만 아직 코드가 실행되지는 않았다. 이후 두 번의
include에서 캐시 적중이 2번 늘었고 반환값은 1, 2로 달라졌다.
이 실험은 같은 CLI 프로세스 안에서 컴파일 결과를 재사용해도 연산이 반복된다는 사실을 확인한다. HTTP 요청 간 변수 유지나 PHP-FPM worker 사이의 공유를 검증한 실험은 아니다. 처리 시간도 측정하지 않았으므로 성능 향상률을 보여주는 결과로 읽어서는 안 된다.
5. 코드를 바꾸면 캐시는 언제 바뀔까?
OPcache를 사용하면 디스크의 소스와 메모리에 보관된 컴파일 결과를 함께 생각해야 한다. 코드를 수정했는데 예전 동작이 보인다면, 요청을 처리하는 PHP가 어느 버전의 캐시를 사용하는지 확인할 필요가 있다.
opcache.validate_timestamps=1이면 변경 시각을 검사해 갱신 여부를 판단한다. opcache.revalidate_freq는 그 검사 간격이다. 0이면 요청마다 검사한다. 캐시를 끄거나 매번 무조건 재컴파일한다는 뜻은 아니다. 검사 시점은 요청 처리에 연결되므로 주기적으로 모든 코드를 읽는 백그라운드 작업으로 이해해서는 안 된다.opcache.validate_timestamps=0이면 자동 변경 확인에 기대어 배포할 수 없다. 배포 절차가 캐시 무효화나 PHP 실행 환경 교체까지 담당해야 한다. PHP 공식 문서: 변경 시각 검증
무효화와 전체 초기화의 범위를 구분한다
opcache_invalidate($path, true)는 지정한 스크립트의 메모리 캐시를 무효화한다. 관련 코드 전체의 배포를 원자적으로 바꾸거나 이미 실행 중인 요청을 새 코드로 바꾸는 기능은 아니다. PHP 공식 문서: opcache_invalidateopcache_reset()은 현재 OPcache 인스턴스의 메모리 캐시를 초기화한다. 재시작이 예약되거나 진행 중인 상태가 있을 수 있으므로 반환값만으로 모든 요청이 즉시 새 코드를 쓰기 시작했다고 가정하지 않는다. 디스크의 file cache까지 지워주는 함수도 아니다. PHP 공식 문서: opcache_reset실제 요청을 처리하는 PHP에 적용해야 한다
CLI와 PHP-FPM은 실행 환경이 다르다. 터미널에서 실행한 PHP가 웹 요청을 처리하는 PHP와 같은 설정·캐시를 사용한다고 가정하면 안 된다. CLI에서 초기화 함수를 호출하는 방식만으로 FPM 캐시 갱신을 보장할 수 없다.
내가 배포 절차를 정한다면 새 코드가 준비된 PHP 인스턴스를 올리고, 주요 경로를 호출해 준비 상태를 확인한 뒤 트래픽을 전환하는 방식을 먼저 검토하겠다. 기존 인스턴스는 처리 중인 요청을 마치게 한다. 실행 환경을 교체하기 어렵다면 웹 요청을 처리하는 캐시 인스턴스에 갱신이 적용됐는지 확인하는 절차가 필요하다.
여러 서버가 있으면 각 서버의 갱신 상태를 확인해야 한다. PHP-FPM이 코드를 실행하는 구성에서 Nginx만 재시작하는 것으로 이 절차를 대신할 수는 없다. 경로 전환 방식으로 배포한다면 OPcache뿐 아니라 경로 해석과 기존 요청의 수명도 함께 점검한다.
6. 메모리는 어떻게 쓰이고, 왜 부족해질까?
OPcache에는 컴파일된 명령뿐 아니라 이를 찾기 위한 해시 테이블과 메타데이터도 들어간다. 소스가 짧다고 캐시에서도 같은 크기를 차지하는 것은 아니다. 라이브러리가 많으면 캐시할 스크립트와 문자열도 늘어난다.
interned string은 같은 문자열을 여러 번 저장하는 비용을 줄이기 위한 공유 표현이다. 함수명이나 클래스명처럼 코드에서 반복되는 문자열을 떠올리면 된다. 요청에서 만들어진 모든 문자열을 무제한으로 보관하는 사용자 데이터 캐시는 아니다.무효화된 스크립트의 메모리를 그 자리에서 모두 돌려받는다고 생각해서도 안 된다. 기존 참조를 고려해야 하므로 사용하지 못하는 영역이
wasted_memory로 남을 수 있다. 공간 부족과 재시작 조건이 맞으면 캐시 재시작이 예약된다. 자주 쓰지 않는 항목 하나를 지워 공간을 즉시 만드는 일반적인 LRU 캐시와 같은 방식으로 용량을 계산하지 않는 편이 안전하다. PHP 8.4 캐시 무효화·재시작 구현용량이 부족하면 모든 PHP 요청이 바로 실패한다는 뜻은 아니다. 새 스크립트를 캐시에 넣지 못해 컴파일을 반복하거나 캐시를 다시 채우는 비용이 생길 수 있다. 재시작과 미스가 반복된다면 메모리 크기, 캐시 키 수, 배포로 새로 생기는 코드 경로를 함께 확인한다.
7. 설정 방법
OPcache가 설치·로드된 환경에서 시작할 수 있는 예다. 용량 값은 설명용이며 서비스에 맞춘 권장값이나 측정 결과는 아니다.
설정 | 확인할 내용 |
memory_consumption | 공유 캐시 용량. MB 단위다. |
interned_strings_buffer | 공유 문자열 영역. MB 단위다. |
max_accelerated_files | 캐시 해시 테이블의 키 수 한도. 실제 용량은 내부 규칙에 따라 조정된다. |
validate_timestamps, revalidate_freq | 코드 변경 검사 여부와 간격. |
enable_cli | CLI에서 사용할지 별도로 결정한다. |
기본값과 변경 가능한 범위는 PHP 버전별 공식 문서와 실제 실행 환경을 대조한다. 특히 개발 중에는 코드 변경 확인이 동작하도록 두는 편이 이해하기 쉽다. 운영에서 검사를 끄려면 앞서 설명한 배포 절차가 먼저 준비되어야 한다. PHP 공식 문서: OPcache 설정
설정 파일을 수정한 뒤에는 웹 요청을 처리하는 PHP에서 적용값을 확인한다. CLI의 설정 출력만 보고 완료했다고 판단하지 않는다.
정리
OPcache는 같은 PHP 코드를 반복해서 파싱하고 컴파일하는 비용을 줄인다. 이를 위해 컴파일 결과를 공유 메모리에 보관하고, 유효한 캐시가 있으면 다음 실행에서 재사용한다.
설정만 외우기보다 세 가지를 기억하면 된다. 캐시된 코드도 실행된다. 공유 범위는 OPcache 인스턴스를 따른다. 코드 배포에는 캐시 갱신이 포함되어야 한다.
일반적인 PHP-FPM 서비스라면 OPcache의 활성화 여부와 용량을 기본 점검 항목으로 두는것을 추천한다.