고급 언어로 작성된 소스 코드는 CPU가 곧바로 실행할 수 있는 기계 명령이 아닙니다. 컴파일러는 문자로 된 소스 코드를 토큰과 문법 구조로 분석하고, 프로그램의 의미가 올바른지 검사한 뒤 중간 표현과 목적 코드를 생성합니다.
이 장에서는 컴파일러 세부 과정을 전처리·어휘 분석·구문 분석·의미 분석·중간 코드 생성·코드 최적화·코드 생성으로 구분합니다. 또한 하향식·상향식 파싱의 차이, 파스 트리와 추상 구문 트리, 심벌 테이블, 링크와 실행 코드 생성, 디버깅까지 시험에 필요한 기준으로 연결합니다.
이 장의 핵심 내용
- 전처리는 헤더 포함·매크로 확장·조건부 컴파일 등을 본격적인 컴파일 전에 처리합니다.
- 어휘 분석기는 문자의 연속을 식별자·키워드·리터럴·연산자 등의 토큰으로 변환합니다.
- 구문 분석기는 토큰의 배열이 문법에 맞는지 검사하고 파스 트리 또는 추상 구문 트리를 구성합니다.
- 하향식 파싱은 시작 기호에서 입력 문자열을 향해 전개하며 좌단 유도와 연결됩니다.
- 상향식 파싱은 입력 문자열에서 시작 기호를 향해 축약하며 우단 유도의 역과 연결됩니다.
- 의미 분석은 자료형·선언·사용 범위·함수 인수·식별자 사용이 의미상 타당한지 검사합니다.
- 심벌 테이블은 식별자의 이름·종류·자료형·범위·저장 위치 등의 정보를 여러 단계에서 공유합니다.
- 중간 코드는 특정 기계에 덜 의존하는 내부 표현으로 최적화와 코드 생성을 연결합니다.
- 코드 최적화는 프로그램의 의미를 유지하면서 실행 시간·코드 크기·자원 사용을 개선합니다.
- 코드 생성은 중간 표현을 타깃 기계의 어셈블리 또는 기계 코드로 변환합니다.
- 목적 파일과 라이브러리를 실행 파일로 결합하는 링크는 일반적으로 컴파일러 핵심 분석 단계와 구분합니다.
- 디버거는 실행 중인 프로그램의 상태를 관찰하여 런타임 오류와 논리 오류를 찾는 도구입니다.
컴파일러 세부 과정은 어떻게 구성되는가?
컴파일러는 일반적으로 프런트엔드에서 소스 언어를 분석하고, 중간 표현을 생성한 뒤, 최적화와 백엔드 코드 생성을 통해 타깃 기계용 코드를 만듭니다. 전처리와 링크는 전체 빌드 흐름에는 포함되지만 엄밀하게는 컴파일러 핵심 분석 단계와 별도의 도구가 담당할 수 있습니다.
| 구분 | 단계 | 입력 | 핵심 처리 | 대표 출력 |
|---|---|---|---|---|
| 사전 처리 | 전처리(선행 처리) | 원시 코드·전처리 지시문 | 헤더 포함·매크로 확장·조건부 선택 | 확장된 원시 코드 |
| 프런트엔드 | 어휘 분석 | 문자열 | 문자를 토큰으로 분류 | 토큰의 연속 |
| 프런트엔드 | 구문 분석 | 토큰 | 문법 구조 검사·트리 구성 | 파스 트리·AST |
| 프런트엔드 | 의미 분석 | 구문 트리·심벌 정보 | 자료형·선언·범위·호출 규칙 검사 | 의미가 부여된 트리 |
| 중간 단계 | 중간 코드 생성 | 의미 분석 결과 | 기계 독립적 내부 표현으로 변환 | IR·중간 코드 |
| 중간 단계 | 코드 최적화 | 중간 코드 | 의미를 보존하며 성능·크기 개선 | 최적화된 중간 코드 |
| 백엔드 | 코드 생성 | 최적화된 IR | 명령 선택·레지스터 할당·명령 배치 | 어셈블리·목적 코드 |
| 빌드 후속 | 링크 | 목적 파일·라이브러리 | 심벌 해결·재배치·결합 | 실행 파일·로드 모듈 |

전처리기는 컴파일 전에 무엇을 처리하는가?
전처리기(Preprocessor, 선행 처리)는 컴파일러의 어휘·구문 분석이 시작되기 전에 소스 텍스트를 확장하거나 선택합니다. C 계열 언어에서는 파일 포함, 매크로 치환, 조건부 컴파일이 대표적입니다.
전처리기는 원시 프로그램을 기계어 목적 프로그램으로 번역하지 않습니다. 파일 포함, 매크로 확장, 조건부 컴파일 등의 사전 처리를 수행하여 컴파일러가 분석할 확장된 원시 코드를 생성합니다.
#include <stdio.h>
#define MAX_COUNT 100
#if MAX_COUNT >= 100
const char *mode = "large";
#else
const char *mode = "small";
#endif
| 전처리 기능 | 설명 | 예 |
|---|---|---|
| 파일 포함 | 다른 파일의 내용을 현재 위치에 포함 | #include |
| 매크로 확장 | 매크로 이름을 정의된 토큰열로 치환 | #define |
| 조건부 컴파일 | 조건에 따라 컴파일할 코드 영역을 선택 | #if·#else |
| 주석 처리 | 시험에서 주석 제거를 전처리 기능으로 분류 | 실제 처리 단계는 언어·도구 구현에 따라 다를 수 있음 |
시험 기준: 전처리기의 대표 기능으로 주석 제거·상수 정의 치환·매크로 확장이 출제될 수 있습니다. 실제 컴파일러에서는 주석 처리와 상수 계산의 정확한 단계가 구현에 따라 달라질 수 있습니다.
어휘 분석은 소스 코드를 어떻게 토큰으로 나누는가?
어휘 분석(Lexical Analysis)은 소스 프로그램의 문자 흐름을 읽고 문법적으로 의미 있는 최소 단위인 토큰(Token)으로 분류하는 단계입니다. 어휘 분석기는 스캐너(Scanner) 또는 렉서(Lexer)라고도 하며, 인식한 토큰을 구문 분석기에 전달합니다.
int total = price * count + 100;
| 문자열 조각 Lexeme | 토큰 종류 | 구문 분석기에 전달되는 의미 |
|---|---|---|
int | 키워드 | 정수형 선언 |
total | 식별자 | 변수 이름 |
= | 대입 연산자 | 대입 연산 |
price | 식별자 | 변수 이름 |
* | 산술 연산자 | 곱셈 |
count | 식별자 | 변수 이름 |
+ | 산술 연산자 | 덧셈 |
100 | 정수 리터럴 | 상수 값 |
; | 구분자 | 문장 종료 |
어휘 오류
언어가 허용하지 않는 문자나 끝나지 않은 문자열 리터럴처럼 토큰 자체를 구성할 수 없는 경우입니다.
어휘 분석의 출력
토큰 종류와 필요하면 식별자·리터럴 값 또는 심벌 테이블을 가리키는 부가 정보를 전달합니다.
구문 분석기는 토큰을 어떻게 문장 구조로 만드는가?
구문 분석(Syntax Analysis)은 토큰의 순서가 언어의 문맥 자유 문법에 맞는지 검사하고 계층적인 문장 구조(ex. 파스 트리)를 만드는 단계입니다. 이 작업을 파싱(Parsing)이라고 하며, 구문 분석기 또는 파서(Parser)가 수행합니다.
<대입문> ::= id "=" <표현식> ";"
<표현식> ::= <표현식> "+" <항> | <항>
<항> ::= id | number
이 문법에서 id = id + number;는 대입문 규칙과 표현식 규칙에 따라 유도할 수 있으므로 구문상 올바른 문장입니다. 반면 id = + number;는 덧셈 연산자 앞에 필요한 표현식이 없으므로 구문 오류입니다.
| 분석 결과 | 특징 | 사용 목적 |
|---|---|---|
| 파스 트리 | 문법 규칙에 등장하는 단말·비단말 기호를 비교적 자세히 표현 | 입력이 문법에서 어떻게 유도되는지 확인 |
| 추상 구문 트리 AST | 괄호·세미콜론 등 불필요한 문법 세부사항을 줄이고 핵심 연산 구조를 표현 | 의미 분석·중간 코드 생성·최적화에 활용 |
구문 분석의 역할: 토큰이 존재하는지만 확인하는 것이 아니라 토큰이 문법적으로 허용된 순서와 계층으로 결합되는지 판단합니다.
하향식 파싱과 상향식 파싱은 무엇이 다른가?
| 비교 항목 | 하향식 파싱 Top-Down | 상향식 파싱 Bottom-Up |
|---|---|---|
| 출발점 | 문법의 시작 기호·트리의 루트 | 입력 토큰·트리의 잎 |
| 진행 방향 | 루트에서 잎을 향해 생성 규칙을 전개 | 잎에서 루트를 향해 핸들을 축약 |
| 유도 관계 | 일반적으로 좌단 유도 | 우단 유도의 역 |
| 핵심 동작 | 예측하고 규칙을 선택하여 확장 | Shift로 입력을 쌓고 Reduce로 규칙의 오른쪽을 왼쪽으로 축약 |
| 대표 파서 | 재귀 하강(Recursive Descent Parser), 예측 파서(Predictive Parser), LL 파서 | Shift-Reduce, LR, SLR, LALR 파서 |
| 문법 주의 | 전통적 LL·예측 파서는 좌재귀 제거가 필요하며 좌인수분해가 필요할 수 있음 | 좌재귀 문법도 처리할 수 있으며 더 넓은 문법을 다룸 |
하향식 파싱의 흐름
<대입문>
⇒ id = <표현식> ;
⇒ id = <표현식> + <항> ;
⇒ id = <항> + <항> ;
⇒ id = id + <항> ;
⇒ id = id + number ;
상향식 파싱의 흐름
id = id + number ;
number → <항>
id → <항> → <표현식>
<표현식> + <항> → <표현식>
id = <표현식> ; → <대입문>
표현 보완: 하향식 파싱을 ‘루트에서 단말 노드 방향’으로, 상향식 파싱을 ‘단말 노드에서 루트 방향’으로 기억하면 됩니다. 엄밀한 유도 기준은 하향식이 좌단 유도, 상향식이 우단 유도의 역입니다.
파스 트리와 추상 구문 트리는 어떻게 다른가?
트리는 하나의 루트 노드에서 하위 노드가 계층적으로 연결되는 비선형 자료 구조입니다. 컴파일러는 문법 전체의 유도 관계를 나타낼 때 파스 트리를 사용하고, 이후 분석과 변환에는 불필요한 노드를 줄인 추상 구문 트리를 주로 사용합니다.

구분: 파스 트리는 문법 규칙을 자세히 보존하고, 추상 구문 트리는 프로그램의 계산 의미를 중심으로 단순화합니다.
의미 분석은 어떤 오류를 검사하는가?
의미 분석(Semantic Analysis)은 파서가 생성한 구문 트리와 심벌 테이블을 이용하여 자료형, 선언, 범위, 함수 호출, 연산의 적합성을 검사하는 단계입니다. 의미 분석을 통과한 트리는 다음 단계인 중간 코드 생성 단계로 전달됩니다.
| 검사 항목 | 올바른 예 | 오류 예 |
|---|---|---|
| 선언 확인 | 선언한 변수 사용 | 선언하지 않은 변수 사용 |
| 자료형 검사 | 정수 변수에 정수값 대입 | 정수 변수에 호환되지 않는 객체 대입 |
| 연산자 적합성 | 숫자끼리 산술 연산 | 지원하지 않는 자료형 사이의 연산 |
| 함수 호출 | 매개변수 개수·자료형 일치 | 인수 개수 또는 자료형 불일치 |
| 사용 범위 | 유효한 블록 안에서 식별자 사용 | 범위를 벗어난 지역 변수 참조 |
| 반환 규칙 | 함수 반환형에 맞는 값 반환 | 반환형과 호환되지 않는 값 반환 |
int count = 10;
count = "ten"; // 구문은 만들 수 있지만 자료형 규칙에 어긋나는 의미 오류
중요한 보완: 매크로 확장은 일반적으로 의미 분석기의 기능이 아니라 전처리기의 기능입니다. 의미 분석은 자료형·선언·범위·심벌 정보와 같은 의미 규칙을 검사합니다.
심벌 테이블과 중간 코드는 왜 필요한가?
심벌 테이블(Symbol Table)은 소스 코드의 식별자 정보를 저장하는 자료 구조이며 어휘 분석부터 의미 분석·코드 생성까지 여러 단계가 함께 사용합니다. 의미 분석을 통과한 프로그램은 최적화와 타깃 코드 생성을 쉽게 하기 위해 중간 표현(Intermediate Representation, IR)으로 변환됩니다.
| 구분 | 저장·표현 내용 | 주요 사용 단계 |
|---|---|---|
| 심벌 테이블 | 식별자 이름·종류·자료형·범위·저장 위치·함수 매개변수 | 어휘 분석·구문 분석·의미 분석·코드 생성 |
| 중간 표현 IR | 연산과 제어 흐름을 기계에 덜 의존적인 형식으로 표현 | 최적화·타깃 코드 생성 |
원시 식: total = price * count + 100
3주소 형식의 중간 코드 예:
t1 = price * count
t2 = t1 + 100
total = t2
코드 최적화는 프로그램을 어떻게 개선하는가?
코드 최적화(Code Optimization)는 프로그램의 관찰 가능한 의미를 바꾸지 않으면서 실행 시간, 코드 크기, 기억장치 사용량 또는 전력 사용을 개선하는 단계입니다. 모든 지표를 동시에 개선하는 것은 아니며 최적화 목표와 타깃 환경에 따라 결과가 달라집니다.
| 최적화 기법 | 의미 | 예 |
|---|---|---|
| 상수 접기 Constant Folding | 컴파일 시 계산할 수 있는 상수식을 미리 계산 | 3 * 4 → 12 |
| 죽은 코드 제거 Dead Code Elimination | 결과에 영향을 주지 않는 코드를 제거 | 사용되지 않는 계산 삭제 |
| 공통 부분식 제거 | 동일한 계산을 반복하지 않고 재사용 | a*b의 중복 계산 제거 |
| 강도 감소 Strength Reduction | 비용이 큰 연산을 더 저렴한 연산으로 변경 | 조건에 따라 곱셈을 덧셈·시프트로 변경 |
| 루프 최적화 | 반복문의 불필요한 계산과 메모리 접근을 줄임 | 루프 불변 코드 이동 |
| 인라인 확장 | 함수 호출을 함수 본문으로 치환하여 호출 비용을 줄임 | 작은 함수의 호출 제거 |
최적화 원칙: 코드의 의미를 유지해야 하며, 최적화된 코드가 항상 실행 시간과 메모리 사용량을 모두 줄이는 것은 아닙니다.
코드 생성은 목적 코드를 어떻게 만드는가?
코드 생성(Code Generation)은 중간 표현을 타깃 명령어 집합에 맞는 어셈블리 또는 기계 코드로 변환하는 단계입니다. 백엔드는 명령 선택, 레지스터 할당, 명령 스케줄링과 주소 지정 방식을 결정합니다.
| 코드 생성 작업 | 설명 |
|---|---|
| 명령 선택 | IR의 연산을 타깃 CPU 명령으로 대응 |
| 레지스터 할당 | 값을 저장할 레지스터를 배정하고 부족하면 메모리로 이동 |
| 명령 스케줄링 | CPU 파이프라인과 의존성을 고려해 명령 순서를 조정 |
| 주소 지정 | 변수·배열·메모리에 접근하는 주소 계산 방식을 선택 |
| 목적 코드 출력 | 어셈블리 파일 또는 재배치 가능한 목적 파일을 생성 |
코드 생성과 링크: 코드 생성은 개별 번역 단위의 목적 코드를 만들고, 링커는 여러 목적 파일과 라이브러리를 결합하여 실행 파일 또는 로드 모듈을 만듭니다.
디버깅은 어떤 오류를 찾는 과정인가?
디버깅(Debugging)은 프로그램의 결함을 재현하고 원인을 추적하여 수정하는 과정입니다. 디버거(Debugger)는 중단점, 단계 실행, 변수·메모리·호출 스택 관찰 등의 기능으로 실행 중인 프로그램 내부 상태를 확인하게 합니다.
| 오류 종류 | 발견 시점·도구 | 예 |
|---|---|---|
| 어휘·구문 오류 | 컴파일러의 어휘·구문 분석 | 잘못된 문자, 괄호·세미콜론 누락 |
| 의미 오류 | 컴파일러의 의미 분석 | 자료형 불일치, 선언되지 않은 이름 |
| 링크 오류 | 링커 | 정의되지 않은 외부 심벌 |
| 런타임 오류 | 실행 환경·디버거 | 0으로 나누기, 잘못된 메모리 접근 |
| 논리 오류 | 시험·로그·디버거 | 실행은 되지만 계산 결과가 잘못됨 |
디버거의 대표 기능
- 중단점에서 실행 중지
- 한 줄 또는 한 명령씩 단계 실행
- 변수·레지스터·메모리 값 확인
- 함수 호출 스택 추적
- 조건을 만족할 때 실행 중지
디버깅의 기본 순서
- 오류를 재현
- 발생 조건과 범위를 축소
- 가설을 세우고 상태를 관찰
- 원인을 수정
- 회귀 시험으로 다른 기능을 확인
시험에서 자주 혼동되는 개념
어휘 분석과 구문 분석
어휘 분석은 문자를 토큰으로 만들고, 구문 분석은 토큰을 문법 구조로 결합합니다.
구문 오류와 의미 오류
구문 오류는 문장 구조가 문법에 어긋난 경우이고, 의미 오류는 문법은 맞지만 자료형·선언·범위 규칙에 어긋난 경우입니다.
하향식과 상향식
하향식은 루트에서 잎으로 좌단 유도하고, 상향식은 잎에서 루트로 우단 유도의 역을 수행합니다.
LL과 LR
LL은 대표적인 하향식 파서이고, LR은 Shift-Reduce 방식의 대표적인 상향식 파서입니다.
의미 분석과 중간 코드 생성
의미 분석은 프로그램의 의미 규칙을 검사하고, 중간 코드 생성은 검사된 트리를 IR로 변환합니다.
코드 생성과 링크
코드 생성은 목적 코드를 만들고, 링크는 목적 파일과 라이브러리를 실행 파일로 결합합니다.
전처리기와 의미 분석기
매크로 확장과 조건부 컴파일은 전처리기의 기능이며 자료형·선언·범위 검사는 의미 분석기의 기능입니다.
컴파일 오류와 디버깅
컴파일러가 찾는 오류 외에도 런타임·논리 오류는 실행과 시험 과정에서 디버거를 이용해 추적합니다.
이번 장 핵심 요약
- 컴파일러 세부 과정은 어휘 분석·구문 분석·의미 분석·중간 코드 생성·코드 최적화·코드 생성으로 이어집니다.
- 전처리는 매크로 확장·파일 포함·조건부 컴파일 등을 본격적인 분석 전에 수행합니다.
- 어휘 분석기는 문자 흐름을 토큰으로 분류하여 파서에 전달합니다.
- 렉심은 소스에 실제로 나타난 문자열이고 토큰은 그 문자열의 문법적 종류입니다.
- 구문 분석기는 토큰이 문맥 자유 문법에 맞는지 검사하고 트리를 만듭니다.
- 하향식 파싱은 시작 기호에서 입력을 향해 좌단 유도합니다.
- 전통적인 예측 파서는 좌재귀 제거가 필요하며 좌인수분해가 필요할 수 있습니다.
- 상향식 파싱은 입력에서 시작 기호를 향해 Shift와 Reduce를 수행합니다.
- 상향식 파싱은 우단 유도의 역과 연결됩니다.
- 재귀 하강·예측·LL 파서는 하향식이고 Shift-Reduce·LR 파서는 상향식입니다.
- 파스 트리는 문법 구조를 자세히 보존하고 AST는 핵심 계산 구조를 단순화합니다.
- 의미 분석은 자료형·선언·범위·함수 호출·연산 적합성을 검사합니다.
- 매크로 처리는 의미 분석이 아니라 전처리 기능으로 구분합니다.
- 심벌 테이블은 식별자의 이름·종류·자료형·범위 등의 정보를 저장합니다.
- 중간 표현은 기계 독립적인 형태로 최적화와 코드 생성을 연결합니다.
- 코드 최적화는 프로그램의 의미를 보존하면서 성능이나 코드 크기를 개선합니다.
- 코드 생성은 IR을 타깃의 어셈블리·기계 코드로 변환합니다.
- 링커는 여러 목적 파일과 라이브러리를 결합하여 실행 파일을 만듭니다.
- 디버거는 중단점·단계 실행·상태 관찰을 통해 런타임 오류와 논리 오류를 추적합니다.
복습 문제
사용자가 작성한 소스 코드를 실행하면서 오류를 찾기 위한 프로그램은?
디버거입니다.
원시 프로그램을 문자 단위로 스캔하여 의미 있는 토큰으로 분류하는 단계는?
어휘 분석입니다.
컴파일러의 핵심 단계 순서는?
어휘 분석 → 구문 분석 → 의미 분석 → 중간 코드 생성 → 코드 최적화 → 코드 생성입니다.
토큰의 배열이 문법에 맞는지 검사하는 단계는?
구문 분석입니다.
구문 분석의 대표 출력은?
파스 트리 또는 추상 구문 트리입니다.
루트에서 단말 노드 방향으로 문법 규칙을 전개하는 방식은?
하향식 파싱입니다.
입력 토큰에서 시작하여 루트를 만들어 가는 방식은?
상향식 파싱입니다.
하향식 파싱과 연결되는 유도는?
좌단 유도입니다.
상향식 파싱과 연결되는 유도는?
우단 유도의 역입니다.
재귀 하강 파서와 LL 파서는 어느 방식인가?
하향식 파싱 방식입니다.
Shift-Reduce 파서와 LR 파서는 어느 방식인가?
상향식 파싱 방식입니다.
전통적인 예측 파서에서 제거해야 하는 대표 문법 요소는?
좌재귀입니다.
자료형 불일치와 선언되지 않은 식별자를 검사하는 단계는?
의미 분석입니다.
식별자의 자료형·범위·저장 위치 정보를 관리하는 자료 구조는?
심벌 테이블입니다.
기계에 덜 의존적인 내부 코드 표현은?
중간 코드 또는 중간 표현(IR)입니다.
프로그램의 의미를 유지하며 실행 시간이나 코드 크기를 개선하는 단계는?
코드 최적화입니다.
중간 표현을 타깃 기계 명령으로 변환하는 단계는?
코드 생성입니다.
목적 파일과 라이브러리를 실행 파일로 결합하는 도구는?
링커입니다.
매크로 확장·파일 포함·조건부 컴파일을 담당하는 것은?
전처리기입니다.
실행은 되지만 결과가 잘못되는 오류는?
논리 오류입니다.
참고자료
- 학습 범위 — 사무자동화산업기사 제2과목 프로그래밍 일반, 컴파일러 세부 과정·구문 분석
- Q-Net — 사무자동화산업기사 종목별 상세정보
- Clang Documentation — Compiler User’s Manual
- Clang Documentation — Compiler Stages
- GNU Bison Manual — The Bison Parser
- GNU Bison Manual — Bottom-Up Parser Algorithm
- GNU Bison Manual — Language and Grammar
- LLVM Flang — Overview of Compiler Phases
- LLVM Flang — Semantic Analysis
- LLVM — Target-Independent Code Generator
- GCC — The C Preprocessor
- GNU GDB — Debugging with GDB
이 글은 사무자동화산업기사 제2과목 프로그래밍 일반의 컴파일러 세부 과정, 어휘·구문·의미 분석, 파싱 방식, 중간 코드·최적화·코드 생성과 디버깅을 이해하고 복습할 수 있도록 재구성한 정리입니다. 교재의 단계와 용어는 시험 대비를 위해 단순화된 경우가 있으며, 실제 컴파일러는 전처리·프런트엔드·중간 표현·최적화·백엔드·링크의 경계를 다르게 구성할 수 있으므로 최신 출제기준과 공식 문서를 함께 확인해야 합니다.