도입
개발자가 작성하는 C++, java같은 고급 언어 코드는 사람이 이해하기 쉽도록 설계된 소스 코드입니다.
하지만 CPU는 이런 소스 코드를 직접 실행하지 못합니다.
결국 프로그램은 기계어, 목적 코드, 또는 바이트코드 같은 실행 가능한 형태로 바뀌어야 하고, 이 변환 과정을 담당하는 도구가 바로 컴파일러(compiler)입니다.
즉, 컴파일러는 단순한 번역기가 아닙니다. 소스 코드를 읽어 문법과 의미를 검사하고, 내부 표현으로 바꾸고, 최적화를 수행하고, 최종적으로 특정 플랫폼이 실행 가능한 형태로 재구성하는 프로그램 변환 엔진입니다. 운영체제, CPU 아키텍처, ISA, 어셈블리어, 링커, 로더와도 매우 밀접하게 연결됩니다.
필요성
컴파일러는 단순히 개발자가 작성한 코드를 실행 가능하게 만들어 주는 도구를 넘어, 언어의 문법과 의미를 실제 하드웨어 세계로 연결하는 핵심 계층입니다. 컴파일러를 이해하면 왜 같은 소스 코드라도 타깃 CPU에 따라 바이너리가 달라지는지, 왜 최적화 옵션이 성능에 영향을 주는지, 왜 어떤 오류는 실행 전에 잡히고 어떤 오류는 실행 중에만 드러나는지가 훨씬 선명해집니다.
- 프로그래밍 언어와 시스템 소프트웨어 학습
- 성능 최적화와 코드 생성 결과 분석
- 크로스 컴파일과 플랫폼 이식성 이해
- 정적 분석과 타입 시스템 이해
- JIT, VM, 바이트코드 구조 이해
- 디버깅과 빌드 시스템 구조 이해
정의
가장 흔한 정의는 “고급 언어를 기계어로 번역하는 프로그램”이지만, 조금 더 정확히 말하면 컴파일러는 원본 프로그램의 의미를 보존하면서 다른 표현으로 변환하는 프로그램 변환기입니다. 따라서 출력이 항상 최종 기계어일 필요는 없고, 바이트코드, IR(중간 표현), 어셈블리어일 수도 있습니다.
| 입력 | 출력 | 예시 |
|---|---|---|
| 고급 언어 소스 | 기계어 / 목적 코드 | C → x86-64 object code |
| 고급 언어 소스 | 어셈블리어 | C → assembly listing |
| 고급 언어 소스 | 바이트코드 / IR | Java → JVM bytecode |
큰 그림
소스 코드
↓
분석(문자열 → 토큰 → 문법 구조 → 의미 확인)
↓
중간 표현(IR)
↓
최적화
↓
코드 생성
↓
어셈블리 / 목적 코드 / 바이트코드
즉, 컴파일러는 단순 치환 도구가 아니라 분석기 + 검증기 + 최적화기 + 코드 생성기의 성격을 동시에 가집니다.
주요 단계
| 단계 | 무엇을 하는가 | 핵심 결과 |
|---|---|---|
| 어휘 분석 | 문자열을 토큰 단위로 분해 | token stream |
| 구문 분석 | 문법에 맞는 구조인지 검사 | parse tree / AST |
| 의미 분석 | 타입, 선언, 스코프, 의미 규칙 검사 | annotated AST |
| IR 생성 | 언어 독립적 내부 표현 생성 | intermediate representation |
| 최적화 | 동일 의미를 유지하며 더 효율적으로 개선 | optimized IR |
| 코드 생성 | 타깃 ISA에 맞는 코드 생성 | assembly / object code |
1. 어휘 분석
컴파일러는 먼저 소스 파일을 문자 단위로 읽습니다. 하지만 문자를 그대로 처리하면 너무 비효율적이기 때문에, int, main, (, ), + 같은 의미 단위로 끊어 토큰(token)을 만듭니다. 이 역할을 하는 부분이 렉서(lexer) 또는 스캐너(scanner)입니다.
[소스]
int sum = a + 10;
[토큰 예시]
INT / IDENT(sum) / ASSIGN / IDENT(a) / PLUS / NUMBER(10) / SEMICOLON
2. 구문 분석
토큰만 있다고 해서 코드 구조가 바로 이해되는 것은 아닙니다. 구문 분석 단계에서는 이 토큰들이 언어 문법에 맞게 배열되어 있는지 검사하고, 식과 문장과 블록과 함수 같은 구조를 트리로 만듭니다. 이 결과가 보통 파스 트리(parse tree) 또는 더 단순화한 AST(Abstract Syntax Tree)입니다.
3. 의미 분석
문법이 맞는다고 해서 코드가 항상 올바른 것은 아닙니다. 예를 들어 선언되지 않은 변수를 쓰거나, 정수에 문자열을 더하거나, 반환형이 맞지 않는 함수를 정의하는 경우는 구문상 맞더라도 의미상 틀릴 수 있습니다. 이런 문제를 검사하는 단계가 의미 분석(semantic analysis)입니다.
- 변수와 함수가 선언되었는지
- 타입이 서로 호환되는지
- 스코프 규칙을 어기지 않았는지
- 반환값과 매개변수 규칙이 맞는지
- 접근 제한, const, mutability 규칙 등이 맞는지
4. 중간 표현(IR)
IR(Intermediate Representation)은 언어별 문법 차이와 하드웨어별 차이를 완충해 주는 내부 코드 형태입니다. 고급 언어의 복잡한 문법을 더 단순한 연산 구조로 바꿔 두면, 이후 최적화와 코드 생성이 훨씬 쉬워집니다.
소스 코드
↓
AST
↓
IR
↓
최적화
↓
타깃 코드 생성
LLVM IR처럼 널리 알려진 중간 표현은 언어 프런트엔드와 다양한 타깃 백엔드를 유연하게 연결하는 핵심 기반이 되기도 합니다.
5. 최적화
컴파일러는 단순히 맞는 코드만 만들지 않고, 더 효율적인 코드를 만들기 위해 다양한 최적화를 적용합니다. 같은 결과를 내는 연산이라도 덜 비싼 형태로 바꾸거나, 불필요한 계산을 제거하거나, 메모리 접근을 줄이거나, 분기를 단순화할 수 있습니다.
- 상수 폴딩(constant folding)
- 죽은 코드 제거(dead code elimination)
- 공통 부분식 제거(common subexpression elimination)
- 루프 최적화(loop optimization)
- 인라이닝(inlining)
- 레지스터 할당 개선(register allocation)
6. 코드 생성
최종 단계에서는 컴파일러가 타깃 CPU 아키텍처에 맞는 명령어를 선택하고, 레지스터를 배치하고, 메모리 주소 계산과 호출 규약을 맞추어 실제 코드로 변환합니다. 이 결과는 어셈블리어일 수도 있고, 바로 목적 코드일 수도 있습니다.
IR
↓
명령 선택
↓
레지스터 할당
↓
스케줄링 / 배치
↓
assembly / object code
즉, 코드 생성은 추상적인 연산을 실제 CPU 세계의 명령어, 레지스터, 스택, 메모리 접근으로 구체화하는 단계입니다.
인터프리터와의 차이
| 구분 | 컴파일러 | 인터프리터 |
|---|---|---|
| 기본 동작 | 미리 다른 코드 형태로 번역 | 실행하면서 읽고 처리 |
| 출력물 | 목적 코드, 실행 파일, 바이트코드 등 | 대개 별도 최종 산출물 없음 |
| 오류 발견 시점 | 실행 전 다수 검출 가능 | 실행 중 드러나는 경우 많음 |
| 실행 성격 | 사전 번역 중심 | 즉시 해석 중심 |
다만 실제 언어 구현은 이 둘이 명확히 갈라지지 않는 경우가 많습니다. 바이트코드 + VM, JIT 컴파일처럼 컴파일과 해석이 혼합된 실행 모델도 매우 흔합니다.
링커와의 차이
초보자가 자주 헷갈리는 부분이 바로 컴파일러와 링커의 차이입니다. 컴파일러는 보통 한 소스 단위를 분석해 목적 코드로 만들고, 링커는 여러 목적 파일과 라이브러리를 연결해 심볼을 해석하고 최종 실행 파일을 만듭니다.
source.c
↓
컴파일러
↓
object.o
↓
링커
↓
executable
대표 형태
- AOT 컴파일러 → 실행 전에 미리 최종 코드 생성
- JIT 컴파일러 → 실행 중 자주 쓰이는 코드를 즉석에서 최적화해 기계어 생성
- 크로스 컴파일러 → 현재 머신이 아닌 다른 타깃 플랫폼용 코드 생성
- 소스-투-소스 컴파일러 → 한 고급 언어를 다른 고급 언어로 변환
- 바이트코드 컴파일러 → VM이 실행할 중간 코드 생성
즉, 컴파일러는 반드시 “고급 언어 → 네이티브 실행 파일”로만 동작하는 것이 아니라, 목적과 플랫폼에 따라 다양한 변환 경로를 가질 수 있습니다.
자주 하는 오해
- 컴파일러는 그냥 언어를 바꿔 쓰는 번역기라고 생각함 → 실제로는 분석, 검증, 최적화, 코드 생성까지 수행합니다.
- 컴파일되면 오류가 전혀 없다고 생각함 → 컴파일 타임 오류와 런타임 오류는 다릅니다.
- 최적화 옵션만 높이면 무조건 빨라진다고 생각함 → 경우에 따라 코드 크기, 디버깅성, 예측 가능성이 달라질 수 있습니다.
- 모든 컴파일러는 기계어를 바로 만든다고 생각함 → 바이트코드나 IR을 만드는 경우도 많습니다.
- 컴파일러와 링커는 같은 것이라고 생각함 → 역할이 분명히 다릅니다.
- 고급 언어 성능은 언어 자체만으로 결정된다고 생각함 → 컴파일러 품질과 최적화 수준도 매우 큽니다.
공부 루틴
- 고급 언어 → 기계어의 큰 흐름부터 먼저 이해한다.
- 어휘 분석, 구문 분석, 의미 분석 세 단계를 구분한다.
- AST와 IR가 왜 필요한지 이해한다.
- 간단한 예제로 최적화 전후를 비교해 본다.
- 코드 생성이 ISA와 어떻게 연결되는지 본다.
- 링커, 로더, 인터프리터, JIT와의 차이까지 함께 정리한다.
디버깅과 분석 포인트
요약
- ✅ 컴파일러는 소스 코드를 목적 코드, 바이트코드, IR 같은 다른 프로그램 형태로 변환한다.
- ✅ 단순 번역뿐 아니라 문법 검사, 의미 분석, 최적화, 코드 생성까지 수행한다.
- ✅ 일반적으로 어휘 분석, 구문 분석, 의미 분석, IR 생성, 최적화, 코드 생성의 단계로 설명된다.
- ✅ 최적화는 프로그램 의미를 유지한 채 더 빠르거나 더 작은 코드로 개선하는 과정이다.
- ✅ 컴파일러는 인터프리터, 링커, 로더와 밀접하지만 역할은 서로 다르다.
- ✅ 컴파일러를 이해하면 언어 구현, 성능, 플랫폼 호환성, 시스템 소프트웨어 구조가 훨씬 선명해진다.