PHP는 스크립트 언어로 인터프리터 컴파일러가 php 파일을 실행한다. Zend engine 기반으로 실행되며 처리 과정은 다음과 같다.
(Zend engine은 PHP를 실행시키는 주체이며 여러 컴포넌트의 구성으로 이루어져 있다. 각 컴포넌트들은 php 파일을 토큰화, 파싱, 컴파일, 실행 등의 역할을 한다.)
PHP 실행 과정
토큰화(lexer) -> 파싱 -> AST -> opcode(컴파일러에 의해 변환됨) -> 실행 (Zend VM에 의해 실행됨)
PHP7 이전엔 파싱 단계에서 opcode까지 생성하였지만, 이후엔 각 기능을 모듈화 (토큰화, 파싱, 컴파일) 하여 분리하였다.
모듈화의 장점
- 복잡한 컴파일 과정을 작은 단계로 나눠 처리할 수 있음
- 각 단계가 독립적이어서 유지보수와 디버깅이 쉬움
- 최적화가 필요한 부분을 쉽게 식별하고 수정할 수 있음
컴파일러에 도달하기전 토큰화, 파싱 작업을 거쳐 AST라는 산출물이 나오고 이를 컴파일러에게 전달하면 opcode (바이트 코드)로 변환하고 Zend VM은 비로소 기계가 이해할수 있는 이 명령어 집합을 실행시키는데 이땐 CPU 명령어들을 호출하며 처리가 된다.
컴파일 과정에서 다양한 처리를 거치는데 각 작업의 역할과 이유를 알아보자.
토큰화 (lexer)
(PHP에선 토큰화에 re2c 사용)
우선 토큰화가 등장하게된 배경부터 살펴보자. 만약 원시코드를 그대로 파싱한다면 다음과 같은 문제점이 있다.
- 매 문자를 개별적으로 분석해야 함
- 컨텍스트 파악이 어려움
- 구문 분석이 매우 복잡해짐
그래서 원시 코드를 직접 파싱하지 않고 토큰화 하면 다음과 같은 형태로 바뀐다.
<?php
$result = 5 + 2 * 3;
// 아래는 위 코드의 토큰화 결과
T_OPEN_TAG (<?php)
T_VARIABLE ($result)
T_ASSIGN (=)
T_LNUMBER (5)
T_PLUS (+)
T_LNUMBER (2)
T_MUL (*)
T_LNUMBER (3)
T_SEMICOLON (;)
그리고 이렇게 변환하는 이유는 다음과 같은 이점이 있기 때문이다.
- 코드를 의미 있는 최소 단위로 분리
- 문법 분석이 용이해짐 (파싱을 위한 전처리 이기도 하다)
- 불필요한 요소(공백, 주석 등) 처리
파싱
(PHP에서 파싱에는 Bison이 사용됨)
파싱에는 BNF와 같은 프로그래밍언어의 문법을 나타내기 위한 표기법이 활용되는데 아래와 같이 프로그래밍 문법에 대해 정의해 놓은 표기법들이 있다.
프로그래밍 언어 설계시 필요하며 PHP 문법을 BNF로 표기한 예시는 아래와 같다.
# PHP 스크립트 구조
<php_script> ::= "<?php" <statement_list> "?>" | "<?php" <statement_list>
<statement_list> ::= <statement> | <statement_list> <statement>
<statement> ::= <variable_statement> |
<function_statement> |
<class_statement> |
<if_statement> |
<loop_statement> |
<expression_statement> ";"
# 변수 정의
<variable_statement> ::= "$" <identifier> "=" <expression> ";"
<identifier> ::= <letter> | <identifier> <letter> | <identifier> <digit> | <identifier> "_"
<letter> ::= "a" | ... | "z" | "A" | ... | "Z"
<digit> ::= "0" | ... | "9"
이러한 BNF 표기법이 등장한 배경은 무엇일까? 우선 자연어로 프로그래밍언어 문법에 대해 설명해보자.
"변수 선언은 'var' 키워드로 시작하며, 그 뒤에 변수명이 오고, 콜론 후에 타입을 명시한다. 여러 변수는 쉼표로 구분할 수 있다."
위와 같은 자연어로 설명된 문법은 사람은 이해할수 있지만 컴퓨터는 이해할 수 없다. 그리고 이러한 "변수명"이 정확히 무엇을 의미하는지 모호함 (숫자로 시작할 수 있나? 특수문자는?) "여러 변수"가 최대 몇 개까지 가능한지 불명확하기 때문에 명확성이 필요하다.
위 자연어를 컴퓨터가 이해할수 있게 명확한 표기가 필요한데 BNF를 사용하면 아래와 같이 나타낼수있다:
<variable_declaration> ::= var <variable_list>
<variable_list> ::= <variable> | <variable_list> , <variable>
<variable> ::= <identifier> : <type>
<identifier> ::= <letter> {<letter> | <digit>}*
<letter> ::= A | B | C | ... | Z | a | b | c | ... | z
<digit> ::= 0 | 1 | 2 | ... | 9
<type> ::= integer | real | boolean | string
- 변수명이 문자로 시작하고 그 뒤에 문자나 숫자가 올 수 있다는 것이 명확함
- 재귀적 정의를 통해 무한히 많은 변수 선언이 가능함을 표현
- 파싱시 위와 같은 표기법을 바탕으로 문법 구문 검사가 진행된다
AST
위에서 파싱된 토큰은 AST로 변환된후 컴파일러에게 전달되어 opcode로 변환된다. 그럼 이미 토큰화하여 파싱까지 했는데 왜 굳이 또 AST로 변환해야 하는걸까?
위에서 토큰화된 값을 다시 보고 문제점이 무엇인지 살펴보자.
T_OPEN_TAG (<?php)
T_VARIABLE ($result)
T_ASSIGN (=)
T_LNUMBER (5)
T_PLUS (+)
T_LNUMBER (2)
T_MUL (*)
T_LNUMBER (3)
T_SEMICOLON (;)
- 연산자 우선순위 정보가 없음
- 2 * 3을 먼저 계산해야 하는지 알 수 없음
- 단순 나열된 토큰만으로는 의미 파악이 어려움
위와 같은 이유로 AST가 필요하며 구조와 장점은 아래와 같다.
할당(=)
/\
/ \
변수($x) 더하기(+)
/\
/ \
숫자(5) 곱하기(*)
/\
/ \
숫자(2) 숫자(3)
- 곱셈이 덧셈보다 하위 노드에 있어 먼저 계산됨
- 연산자 우선순위가 트리 구조로 명확히 표현
- 코드의 의미가 구조적으로 표현됨.
- 이는 컴파일러의 다양한 분석과 최적화를 가능하게하며 다른 언어로 컨버팅시에도 효율적이다
이러한 이점이 있기에 AST는 컴파일 과정에서 필수적인 중간 단계가 된다.
opcode
생성된 AST를 컴퓨터가 이해하고 실행할수 있게 컴파일러가 기계어로 변환해주며 이때 생성되는것이 opcode이다.
그리고 흔히 할수있는 오해가 인터프리터 언어는 매번 실행마다 컴파일을 해야하는 것인데 이는 맞기도 하며 아니기도 하다.
왜냐하면 매 실행마다 스크립트 파일을 컴파일하여 기계어로 바꾼다는것은 불필요한 오버헤드이기 때문인데 이에 대한 해결책으로 나온게 Opcache이다.
opcache란
매 요청마다 PHP 파일을 컴파일 하지않고 한번 컴파일 한 기계어를 캐싱하여 그 다음 요청부턴 불필요한 토큰화, 파싱, 컴파일 작업을 하지 않고 메모리에 캐싱해놓은 기계어를 불러와 바로 실행할수 있게 해주는 기능이다.
opcache 에 의한 성능 향상
opcache 주요 옵션
opcache.enable = 1 // HTTP 트래픽에 대한 opcode 캐시 활성화
opcache.enable_cli = 0 // CLI에서의 opcode 캐시 활성화 여부
opcache.memory_consumption = 128 // 공유 메모리 크기(MB)
opcache.max_accelerated_files = 10000 // 캐시할 수 있는 최대 파일 수
opcache.interned_strings_buffer = 8 // 바이트코드 저장용 메모리(MB)
opcache.validate_timestamps = 1 // 파일 변경 확인 여부
opcache.revalidate_freq = 0 // 검증 주기(초)
주의사항
메모리 설정 관련:
- memory_consumption과 max_accelerated_files 값이 너무 낮으면 지속적인 재컴파일 발생
- 너무 높게 설정하면 서버 메모리 낭비 가능성
- 프로젝트 규모에 맞는 적절한 값 설정 필요
캐시 검증 관련:
- validate_timestamps를 비활성화하면 수동으로만 캐시 리셋 가능
- 개발 환경에서는 validate_timestamps = 1 권장
- 프로덕션 환경에서는 성능을 위해 비활성화 고려
결론
성능 최적화
- opcache는 PHP 성능 향상을 위한 핵심 기능
- 적절한 설정을 통해 최적의 성능 확보 가능
- 프로젝트 특성에 맞는 설정 조정 필요
배포 전략
- 배포 프로세스에 opcache 리셋 로직 포함 권장
- preload를 활용한 사전 컴파일 고려
- 프레임워크별 최적화 방안 검토 (Symfony, Laravel, Drupal)
모니터링
- opcache 상태 정기적 모니터링 필요
- 메모리 사용량 및 캐시 히트율 확인
참고자료
- https://support.engineyard.com/hc/en-us/articles/7598905184146-PHP-Performance-I-Everything-You-Need-to-Know-About-OpCode-Caches
- https://on.com2us.com/tech/php-opcode-%EC%BA%90%EC%8B%9C/
- https://www.cloudways.com/blog/integrate-php-opcache/
- https://blog.blackfire.io/boosting-php-performance-mastering-opcache-optimization-with-blackfire.html
- https://www.ibexa.co/blog/how-much-of-a-performance-boost-can-you-expect-for-a-symfony-5-app-with-php-opcache-preloading
- https://dev.to/mrsuh/how-php-engine-builds-ast-1nc4
- http://php.find-info.ru/php/016/ch20lev1sec1.html
- https://www.phpinternalsbook.com/php7/zend_engine.html
'IT > php' 카테고리의 다른 글
[PHP] 단일 서버를 사용하는 경우 효과적인 캐싱 방법 (0) | 2024.12.02 |
---|---|
[PHP] ?: 와 ?? 연산자의 차이점 (0) | 2024.09.12 |
[PHP] 자식 클래스에서 상속 받은 const 값 선언 강제 하는법 (0) | 2024.02.07 |
[PHP] Carbon 사용시 주의점 (0) | 2023.02.02 |