컴파일러란 무엇인가요?
컴파일러는 프로그래밍 언어로 작성한 소스 코드를 프로그램이 실행되기 전에 기계어나 바이트코드 같은 더 낮은 수준의 형태로 번역하는 프로그램입니다. 번역된 프로그램은 원래 소스 없이도 실행할 수 있어요.
업데이트: 2026년 9월 24일
- 1print("Hello")
- 2total = 2 + 3
- 3print(total)
- 4print("Done")
한 번 컴파일하면 프로그램을 원하는 만큼 실행할 수 있어요. 실행 횟수는 늘어나지만 컴파일 횟수는 그대로예요. 오타를 켜면 컴파일러가 프로그램 전체를 거부해서 아무것도 출력되지 않아요.
다음 C 프로그램을 hello.c로 저장해 보세요.
#include <stdio.h>
int main(void) {
int price = 4;
printf("Total: %d\n", price * 3);
return 0;
}
그리고 gcc hello.c -o hello를 입력하세요. 잠깐 아무 일도 없는 것 같다가, 소스 옆에 hello라는 새 파일이 생겨요. 이 파일이 컴파일러의 결과물, 즉 프로세서가 실행할 수 있는 기계어예요. hello.c를 지워도 ./hello는 그대로 동작해요. 프로그램에 필요한 모든 것이 이미 번역되어 있기 때문이에요.
gcc hello.c -o hello
./hello
Total: 12
컴파일러는 단계별로 이렇게 동작해요
컴파일러는 소스 코드를 일반 텍스트로 읽고 여러 단계를 거쳐 처리해요. total = price * 3 한 줄을 예로 들어 볼게요.
- 어휘 분석. 텍스트를 언어의 "단어"인 토큰으로 나눠요. 이름
total, 연산자=, 이름price, 연산자*, 숫자3이에요. 공백과 주석은 여기서 버려져요. - 구문 분석(파싱). 토큰을 구조가 드러나는 구문 트리로 배치해요. 값이
price와3의 곱셈인 할당문이라는 구조예요. 언어의 문법을 어긴 코드는 여기서 구문 오류로 멈춰요. - 의미 분석. 컴파일러가 의미를 검사해요.
price가 선언되어 있는지, 타입이 서로 맞는지, 호출하는 함수가 존재하는지 확인해요. - 최적화. 하는 일은 바꾸지 않으면서 더 빠르게 실행되거나 메모리를 덜 쓰도록 프로그램을 고쳐 써요.
x * 2는 시프트 명령 하나가 될 수 있고, 절대 실행될 수 없는 코드는 제거돼요. - 코드 생성. 컴파일러가 대상에 맞는 결과물을 써요. x86-64나 ARM의 기계어일 수도 있고, 가상 머신용 바이트코드일 수도 있어요.
C와 C++에는 마지막 단계인 링킹이 있어요. 컴파일된 파일들을 라이브러리 코드(C 표준 라이브러리의 printf 같은 것)와 합쳐 실행 파일 하나로 만들어요. gcc가 링커를 대신 실행해 주기 때문에 명령 하나처럼 느껴지는 거예요.
파이썬은 앞의 두 단계를 표준 모듈로 제공해서, 실제 입력에 대해 동작하는 모습을 직접 볼 수 있어요.
NAME 'total'
OP '='
NAME 'price'
OP '*'
NUMBER '3'
Assign(
targets=[
Name(id='total', ctx=Store())],
value=BinOp(
left=Name(id='price', ctx=Load()),
op=Mult(),
right=Constant(value=3)))
컴파일러의 나머지 부분은 이 트리를 바탕으로 일해요. 트리는 더 이상 공백이나 줄바꿈을 신경 쓰지 않고 구조만 담고 있어요. price에 3을 곱한 결과를 total에 할당한다는 구조예요.
컴파일 타임: 프로그램이 실행되기 전에 찾는 오류
컴파일러는 프로그램의 어떤 부분도 실행하기 전에 전체를 읽기 때문에 실수를 일찍 거부할 수 있어요. 아래 예제에서 source 안의 print 세 줄은 첫 줄조차 실행되지 않아요. 실행이 시작되기 전에 파이썬 컴파일러가 텍스트를 거부하기 때문이에요.
Rejected before running anything:
'(' was never closed (line 3)
정적 타입 언어의 컴파일러는 문법보다 더 많은 것을 잡아내요. bad.c라는 파일에 있는 이 C 코드 한 줄은 문법상으로는 올바르지만, 정수 변수에 텍스트를 저장하고 있어요.
int count = "five";
GCC 14 이상은 오류를 내고 멈춰요.
bad.c:4:17: error: initialization of 'int' from 'char *' makes integer from pointer without a cast [-Wint-conversion]
파이썬에서 같은 실수를 하면 그 줄이 실행될 때에야 드러나요. 컴파일 타임에 이런 실수를 잡는 것이 대규모 팀이 컴파일되는 정적 타입 언어를 고르는 이유 중 하나예요. 그래도 컴파일러가 모든 것을 잡지는 못해요. 알고 보니 0인 수로 나누거나, 존재하지 않는 파일을 읽는 일은 여전히 실행 중에 실패해요.
컴파일러의 예
| 컴파일러 | 읽는 것 | 만드는 것 |
|---|---|---|
| GCC, Clang | C, C++ | x86-64, ARM 등의 기계어 |
| MSVC (Microsoft Visual C++) | C, C++ | 윈도우 실행 파일 (.exe, .dll) |
javac | Java | JVM 바이트코드 (.class 파일) |
rustc | Rust | 기계어 |
go build | Go | 실행 파일 하나 |
tsc | TypeScript | JavaScript |
두 행을 보면 결과물이 꼭 "기계어"만은 아니라는 걸 알 수 있어요. javac는 자바 런타임 환경의 일부인 자바 가상 머신이 실행하는 바이트코드를 만들어요. tsc는 타입스크립트를 또 다른 고급 언어인 자바스크립트로 바꿔요. 이런 일을 하는 컴파일러를 흔히 트랜스파일러라고 불러요.
컴파일된 기계어는 한 CPU 계열과 한 운영체제에 묶여 있어요. x86-64 윈도우용으로 컴파일한 프로그램은 애플 M 시리즈 칩이 들어간 Mac에서 실행되지 않아요. 대상마다 같은 소스를 다시 컴파일해야 해요.
컴파일러냐 인터프리터냐
인터프리터는 별도의 프로그램 파일을 만들지 않고 소스 코드를 직접 실행해요. 파이썬과 루비는 보통 이렇게 실행돼요. 요즘 언어 도구 대부분은 두 방식을 모두 써요. CPython은 파일을 바이트코드로 컴파일한 다음 인터프리트하고, JVM과 자바스크립트의 V8 엔진은 자주 쓰이는 코드를 프로그램이 실행되는 동안 기계어로 컴파일해요. 전체 비교표는 컴파일러와 인터프리터의 차이 페이지에 있어요.
흔한 오해
- "컴파일되면 제대로 동작한다." 컴파일러는 프로그램이 유효한지를 검사할 뿐, 올바른지는 검사하지 않아요. 논리 실수나 0으로 나누기 같은 실행 중 실패는 그대로 통과해요.
- "파이썬은 컴파일되지 않는다." CPython은 모든 파일을 실행 전에 바이트코드로 컴파일해요.
__pycache__폴더에 있는.pyc파일이 그 바이트코드예요. 프로그램이 불러오는 모듈의 바이트코드를 저장해 두었다가 다음 실행에서 이 단계를 건너뛰어요. - "온라인 컴파일러는 컴파일러다." 브라우저에서 코드를 실행해 주는 사이트는 코드를 서버로 보내고, 서버가 전체 도구 체인(컴파일러, 링커, 그리고 프로그램 자체)을 실행해 출력을 돌려줘요. 컴파일러는 그중 한 부분이에요.
- "컴파일된 코드는 어디서나 실행된다." 빌드 대상인 CPU와 운영체제에서만 실행돼요. 이식성은 다시 컴파일하거나 JVM 같은 가상 머신을 써서 얻어요.
다음으로 읽을 내용
C 문서에서 컴파일러가 실제로 쓰이는 모습을 볼 수 있어요. C를 컴파일하고 실행하는 방법에서 실제 gcc 명령으로 전처리, 컴파일, 어셈블, 링킹을 하나씩 따라가요. 코드를 실행하는 다른 방식을 보려면 컴파일러와 인터프리터의 차이를 읽고, 프로그램이 시작된 뒤 그것을 둘러싼 환경은 런타임 환경에서 확인하세요. 컴파일 언어로 연습하기에는 C 강의가 좋아요.