컴파일러의 번역 과정과 최적화 기술
컴파일러가 소스 코드를 목적 코드로 바꾸는 과정, 전단부와 후단부의 역할, 최적화와 LLVM·JIT·AOT 방식을 정리한다.
2026-08-14 · 최초 발행 2026-01-16
소스 코드가 실행 파일이 되기까지
컴파일러는 사람이 작성한 원시 프로그램을 컴퓨터가 실행할 수 있는 목적 프로그램으로 옮기는 언어 번역 시스템이다. 고급 언어를 하드웨어가 해석할 수 있는 저급 언어로 바꾸는 데 그치지 않고, 오류를 찾아내고 플랫폼 독립적인 중간 표현을 만들며 실행 성능을 높이는 최적화도 수행한다.
일반적으로 소스 코드 전체를 번역해 실행 파일을 만든다는 점에서 한 줄씩 순차적으로 실행하는 인터프리터와 구별된다.
전체 번역과 순차 실행의 차이
| 구분 | 컴파일러 | 인터프리터 |
|---|---|---|
| 번역 단위 | 전체 프로그램 | 한 줄씩 순차 실행 |
| 실행 속도 | 빠름 | 상대적으로 느림 |
| 오류 검출 | 컴파일 시 전체 검출 | 실행 중 순차 검출 |
| 메모리 사용 | 목적 코드 저장 필요 | 별도 저장 불필요 |
| 개발 편의성 | 수정 시 재컴파일 필요 | 즉시 수정 및 실행 가능 |
| 대표 언어 | C, C++, Rust, Go | Python, JavaScript, Ruby |
두 모델이 완전히 분리되는 것은 아니다. 실행 시점에 바이트코드를 기계어로 바꾸는 JIT(Just-In-Time) 컴파일처럼 두 방식의 특성을 결합한 접근도 널리 쓰인다.
분석 결과를 목적 코드로 넘기는 구조
컴파일러의 전단부(Front-end)는 소스 코드의 형태와 의미를 분석해 중간 표현(IR)을 만든다. 후단부(Back-end)는 이 중간 표현을 최적화한 뒤 목적 기계에 맞는 코드로 변환한다.
전단부의 시작점은 어휘 분석이다. 소스 코드를 토큰(Token) 단위로 분리하고 식별자, 키워드, 연산자, 상수를 인식한다. 이 과정에서는 공백과 주석을 제거하고 심볼 테이블을 초기화하며, 정규 표현식과 유한 오토마타를 기반으로 구현할 수 있다.
구문 분석은 토큰 스트림이 문법 규칙에 맞는지 확인한다. 파스 트리(Parse Tree) 또는 추상 구문 트리(AST)를 만들며, 문맥 자유 문법(CFG)을 바탕으로 LL 파서, LR 파서, LALR 파서 등을 사용한다.
의미 분석 단계에서는 타입 검사와 타입 변환을 처리하고, 변수 선언과 범위를 검증한다. 함수 호출 인자가 맞는지도 확인하며, 심볼 테이블을 완성해 의미적 오류를 검출한다.
이후 중간 코드 생성기는 기계 독립적인 IR을 만든다. 3-주소 코드와 정적 단일 할당(SSA) 형식 등이 여기에 사용되며, 플랫폼과 무관한 최적화를 적용할 기반이 된다.
후단부는 IR을 목적 기계가 이해할 수 있는 코드로 바꾼다. 코드 최적화에서는 기본 블록 내부의 지역 최적화, 함수 전체를 대상으로 하는 전역 최적화, 반복문 성능을 다루는 루프 최적화를 수행할 수 있다. 인라인 확장, 죽은 코드 제거, 상수 전파도 이 단계의 대상이다.
코드 생성 단계에서는 목적 기계의 명령어 집합에 맞춰 코드를 만들고, 레지스터 할당과 명령어 스케줄링을 수행한다. 메모리 주소를 할당하고 재배치 정보도 생성한다.
실행 경로를 줄이는 최적화
기계 독립적 최적화는 특정 하드웨어에 묶이지 않은 중간 표현 수준에서 적용된다.
| 최적화 기법 | 설명 | 효과 |
|---|---|---|
| 상수 폴딩 | 컴파일 시 상수 연산 미리 계산 | 실행 시간 단축 |
| 상수 전파 | 상수 값을 사용 위치에 직접 대입 | 레지스터 절약 |
| 죽은 코드 제거 | 실행되지 않는 코드 삭제 | 코드 크기 감소 |
| 공통 부분식 제거 | 중복 연산 결과 재사용 | 연산 횟수 감소 |
| 루프 불변 코드 이동 | 루프 밖으로 불변 연산 이동 | 반복 연산 제거 |
목적 기계의 특성을 반영하는 최적화도 별도로 필요하다. 레지스터 할당은 변수를 레지스터에 효율적으로 배치하고, 명령어 스케줄링은 파이프라인을 고려해 명령어 순서를 조정한다. 피프홀 최적화는 연속된 명령어 패턴을 더 효율적인 패턴으로 교체하며, 분기 예측 힌트는 조건 분기의 예측 성능을 높이는 데 쓰인다.
LLVM과 실행 시점에 따른 컴파일 방식
LLVM(Low Level Virtual Machine)은 여러 언어와 플랫폼을 지원하는 모듈형 컴파일러 인프라다. 언어별 전단부가 LLVM IR을 공유하고, IR은 여러 목적 기계용 후단부로 전달된다.
JIT 컴파일은 프로그램 실행 시점에 바이트코드를 기계어로 변환한다. 런타임 정보를 바탕으로 최적화할 수 있고 플랫폼 독립성을 유지한다. Java HotSpot VM, JavaScript V8 엔진, .NET CLR이 활용 사례이며, 핫스팟 분석·적응적 최적화·탈최적화가 관련 기술이다.
AOT 컴파일은 실행 전에 네이티브 코드로 변환하는 방식이다. 빠른 시작 시간과 예측 가능한 성능이 장점이며, GraalVM Native Image와 Android ART에서 활용된다. 반면 동적 최적화는 불가능하고 바이너리 크기가 증가하는 트레이드오프가 있다.
언어별 컴파일러의 성격
| 컴파일러 | 대상 언어 | 특징 | 최적화 수준 |
|---|---|---|---|
| GCC | C, C++, Fortran | 오픈소스, 다양한 플랫폼 | 높음 |
| Clang/LLVM | C, C++, Objective-C | 모듈화, 빠른 컴파일 | 높음 |
| MSVC | C, C++ | Windows 최적화 | 높음 |
| rustc | Rust | 메모리 안전성 보장 | 높음 |
| Go | Go | 빠른 컴파일, 정적 링킹 | 중간 |
| javac | Java | 바이트코드 생성 | JVM 의존 |
컴파일러를 만들거나 확장할 때는 어휘 분석기 생성 도구인 Lex/Flex, 구문 분석기 생성 도구인 Yacc/Bison, 범용 파서 생성기인 ANTLR을 활용할 수 있다. LLVM은 컴파일러 백엔드 프레임워크로, MLIR은 다중 수준 중간 표현 프레임워크로 쓰인다.