ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

컴파일러 (Compiler)

도입

사람이 작성한 고급 언어 코드를 기계가 이해하고 실행할 수 있는 형태로 변환하는 핵심 시스템 소프트웨어다

개발자가 작성하는 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)입니다.

실전 팁
우리가 흔히 보는 문법 오류(syntax error)는 대부분 이 단계에서 검출됩니다. 괄호가 맞지 않거나, 문장 구조가 언어 규칙에 맞지 않으면 여기서 컴파일이 멈춥니다.

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. 코드 생성

코드 생성은 최적화된 내부 표현을 특정 ISA와 플랫폼이 이해할 수 있는 실제 실행 코드로 바꾸는 단계다

최종 단계에서는 컴파일러가 타깃 CPU 아키텍처에 맞는 명령어를 선택하고, 레지스터를 배치하고, 메모리 주소 계산과 호출 규약을 맞추어 실제 코드로 변환합니다. 이 결과는 어셈블리어일 수도 있고, 바로 목적 코드일 수도 있습니다.

IR
   ↓
명령 선택
   ↓
레지스터 할당
   ↓
스케줄링 / 배치
   ↓
assembly / object code

즉, 코드 생성은 추상적인 연산을 실제 CPU 세계의 명령어, 레지스터, 스택, 메모리 접근으로 구체화하는 단계입니다.

인터프리터와의 차이

컴파일러는 보통 실행 전에 코드를 다른 형태로 번역하고, 인터프리터는 보통 실행 시점에 코드를 읽으며 동작을 수행한다
구분 컴파일러 인터프리터
기본 동작 미리 다른 코드 형태로 번역 실행하면서 읽고 처리
출력물 목적 코드, 실행 파일, 바이트코드 등 대개 별도 최종 산출물 없음
오류 발견 시점 실행 전 다수 검출 가능 실행 중 드러나는 경우 많음
실행 성격 사전 번역 중심 즉시 해석 중심

다만 실제 언어 구현은 이 둘이 명확히 갈라지지 않는 경우가 많습니다. 바이트코드 + VM, JIT 컴파일처럼 컴파일과 해석이 혼합된 실행 모델도 매우 흔합니다.

링커와의 차이

컴파일러가 소스 파일을 목적 코드로 바꾸는 역할이라면, 링커는 여러 목적 코드와 라이브러리를 결합해 최종 실행 파일을 만드는 역할을 맡는다

초보자가 자주 헷갈리는 부분이 바로 컴파일러와 링커의 차이입니다. 컴파일러는 보통 한 소스 단위를 분석해 목적 코드로 만들고, 링커는 여러 목적 파일과 라이브러리를 연결해 심볼을 해석하고 최종 실행 파일을 만듭니다.

source.c
   ↓
컴파일러
   ↓
object.o
   ↓
링커
   ↓
executable

대표 형태

컴파일러는 항상 같은 방식으로 동작하지 않으며, 타깃 환경과 실행 모델에 따라 여러 형태로 나뉜다
  • AOT 컴파일러 → 실행 전에 미리 최종 코드 생성
  • JIT 컴파일러 → 실행 중 자주 쓰이는 코드를 즉석에서 최적화해 기계어 생성
  • 크로스 컴파일러 → 현재 머신이 아닌 다른 타깃 플랫폼용 코드 생성
  • 소스-투-소스 컴파일러 → 한 고급 언어를 다른 고급 언어로 변환
  • 바이트코드 컴파일러 → VM이 실행할 중간 코드 생성

즉, 컴파일러는 반드시 “고급 언어 → 네이티브 실행 파일”로만 동작하는 것이 아니라, 목적과 플랫폼에 따라 다양한 변환 경로를 가질 수 있습니다.

자주 하는 오해

컴파일러를 처음 배울 때는 단순 번역기로만 보거나, 최적화를 만능처럼 생각하는 등 여러 오해가 생기기 쉽다
  • 컴파일러는 그냥 언어를 바꿔 쓰는 번역기라고 생각함 → 실제로는 분석, 검증, 최적화, 코드 생성까지 수행합니다.
  • 컴파일되면 오류가 전혀 없다고 생각함 → 컴파일 타임 오류와 런타임 오류는 다릅니다.
  • 최적화 옵션만 높이면 무조건 빨라진다고 생각함 → 경우에 따라 코드 크기, 디버깅성, 예측 가능성이 달라질 수 있습니다.
  • 모든 컴파일러는 기계어를 바로 만든다고 생각함 → 바이트코드나 IR을 만드는 경우도 많습니다.
  • 컴파일러와 링커는 같은 것이라고 생각함 → 역할이 분명히 다릅니다.
  • 고급 언어 성능은 언어 자체만으로 결정된다고 생각함 → 컴파일러 품질과 최적화 수준도 매우 큽니다.

공부 루틴

컴파일러를 공부할 때는 이론만 외우기보다, 소스 코드가 토큰과 AST와 IR과 어셈블리로 변하는 흐름을 단계적으로 보는 것이 가장 효과적이다
  1. 고급 언어 → 기계어의 큰 흐름부터 먼저 이해한다.
  2. 어휘 분석, 구문 분석, 의미 분석 세 단계를 구분한다.
  3. AST와 IR가 왜 필요한지 이해한다.
  4. 간단한 예제로 최적화 전후를 비교해 본다.
  5. 코드 생성이 ISA와 어떻게 연결되는지 본다.
  6. 링커, 로더, 인터프리터, JIT와의 차이까지 함께 정리한다.

디버깅과 분석 포인트

컴파일 관련 문제를 볼 때는 단순히 에러 메시지만 읽기보다, 어느 단계에서 문제가 생겼는지 구분하는 것이 훨씬 중요하다
1
문제가 문법 오류인지, 의미 오류인지, 링크 오류인지 먼저 구분한다.
2
타깃 플랫폼이 맞는지, 즉 ISA와 ABI가 일치하는지 확인한다.
3
최적화가 켜져 있다면 디버깅 시 소스와 실제 코드 대응이 어긋날 수 있음을 본다.
4
성능 이슈라면 어셈블리 출력과 최적화 결과를 같이 확인한다.
5
바이너리 실행 문제가 있으면 컴파일러뿐 아니라 링커와 로더 단계도 함께 살펴본다.

요약

컴파일러는 고급 언어 코드를 분석하고 검증하고 최적화해 실행 가능한 형태로 변환하는 핵심 시스템 소프트웨어이며, 소프트웨어와 하드웨어를 연결하는 가장 중요한 계층 중 하나다
  • ✅ 컴파일러는 소스 코드를 목적 코드, 바이트코드, IR 같은 다른 프로그램 형태로 변환한다.
  • ✅ 단순 번역뿐 아니라 문법 검사, 의미 분석, 최적화, 코드 생성까지 수행한다.
  • ✅ 일반적으로 어휘 분석, 구문 분석, 의미 분석, IR 생성, 최적화, 코드 생성의 단계로 설명된다.
  • ✅ 최적화는 프로그램 의미를 유지한 채 더 빠르거나 더 작은 코드로 개선하는 과정이다.
  • ✅ 컴파일러는 인터프리터, 링커, 로더와 밀접하지만 역할은 서로 다르다.
  • ✅ 컴파일러를 이해하면 언어 구현, 성능, 플랫폼 호환성, 시스템 소프트웨어 구조가 훨씬 선명해진다.
728x90