파이썬은 내부적으로 CPython으로 구성되어 있어, C언어의 컴파일 과정을 살펴보면 그 구조를 파악하기 쉽다.
우선 C언어의 컴파일 과정은 다음과 같다:
- 소스 코드를 Preprocessor가 #이 붙은 구문들에 따라 치환하고 붙여넣어 준다.
- 그렇게 만들어진 .i로 끝나는 새로운 C언어 파일을 컴파일러가 받는다.
- 컴파일러가 그 파일을 어셈블리어로 번역한다.
- 이후 어셈블러가 어셈블리 코드를 Object file로 번역하고, 링커가 이 목적 파일들과 라이브러리를 엮어 최종 실행 파일을 생성한다.
이때 "왜 바로 기계어로 번역하지 않고 굳이 사람이 읽을 수 있는 어셈블리어로 한 번 더 번역할까?" 궁금해질 수 있다. 이는 컴파일러의 구조를 더 자세히 알아보면 자연스레 답을 얻을 수 있다.
3번 과정을 담당하는 컴파일러에 대해 살펴보자. 여기서 웹과 비슷한 개념들이 등장하므로 연관 지어 생각하면 이해가 쉽다. 컴파일러는 Frontend와 Backend로 나뉜다.

전처리된 파일이 들어오면 먼저 Frontend 작업이 실행된다:
-
Lexical Analysis -> C언어 스크립트의 구문들을 단위로 자른다. 예를 들어 [키워드 int], [식별자 a], [연산자 +] 등으로 나누는 과정이며, React가 DOM을 만들기 위해 HTML을 파싱하는 개념과 동일하다.
-
Syntax Analysis -> 잘린 것들을 토대로 문법에 맞는지 검사하며 AST(Abstract Syntax Tree)를 만든다. HTML과 마찬가지로 우리가 쓰는 스크립트도 연산자의 우선순위나 scope가 있기에 DOM 같은 트리 형태를 구성할 수 있다.
이렇게 AST를 만든 후, 컴파일러의 Backend 작업이 실행된다:
-
Intermediate Representation (IR) -> AST를 순회하면서 CPU에 종속되지 않는 중간 단계의 코드인 IR을 생성한다. 여기서 의미 없는 반복문이나 쓰여지지 않는 변수를 지우는 등의 최적화를 수행한다.
-
Code Generation -> 최적화된 IR을 바탕으로 타깃 CPU에 맞는 어셈블리 언어로 변환하여 출력한다.
즉, 기계어로 바로 번역하지 않고 어셈블리 언어를 거치는 이유는 컴파일러가 최적화한 결과를 디버깅하기 위함도 있지만, 타겟 CPU에 대한 기계어 매핑이 이미 어셈블리 언어에 의해 만들어져 있기 때문이다. 최적화 과정은 어셈블리어가 기계어로 바뀌기 전 컴파일러 단에서 거쳐주기에 성능상 문제 되지 않으며, 이후 어셈블러는 그저 어셈블리어와 기계어를 매핑해주기만 하면 된다.
그럼 특수한 인터프리터가 있다는 파이썬은 어떻게 작동하는 걸까? C의 컴파일러와 비교하며 파악해 보자.
-
컴파일러의 Frontend 작업을 수행 후 AST를 만드는 과정은 C 컴파일러와 동일하다.
-
이후 Backend의 IR 단계를 거친다. -> 이때 C언어와 비교하면 상당히 간소화된 형태로 최적화된다. 구체적으로 말해, CFG(Control Flow Graph)를 구축하여 Dead Code를 없애거나 상수 연산을 미리 수행하는 등 가벼운 최적화 후 1차원 배열로 만든다.
-
파이썬의 Virtual Machine만 이해할 수 있는 자체 어셈블리 언어인 bytecode로 변환한다.
-
이 bytecode를 명령어 단위로 python.exe가 가지고 있던 기계어 switch-case 루프에 던져 실행한다.
이때 4번의 과정을 흔히 인터프리터라고 부른다.
지금은 컴파일러에 집중하느라 python.exe에 대해 깊게 다루지 않았지만, 실제로 터미널에 파이썬 파일을 실행하라는 명령어(python3 script.py)를 입력하면 OS가 python.exe를 메모리에 불러와 CPU에서 실행하기 시작한다. 이후 python.exe가 script.py를 bytecode로 변환하고, Virtual Machine 내부에서 Evaluation Loop가 실행되어 명령어를 처리한다.
결과적으로 C언어가 어셈블러와 링커를 거쳐 완성된 기계어 파일을 CPU가 직접 실행해 결과를 내는 것과 뚜렷하게 대조되는 것이 파이썬만의 특징이다.