프로그래밍 언어 기초와 컴파일러 기술 응용
컴파일러를 제대로 이해하려면 프로그래밍 언어 자체에 대한 기초 지식이 필요합니다. 이 글에서는 언어의 발전 과정부터 핵심 개념, 그리고 컴파일러 기술이 컴파일 이외의 분야에 어떻게 활용되는지를 다룹니다.
1. 프로그래밍 언어의 발전
1세대: 기계어 (1940년대)
컴퓨터가 직접 실행할 수 있는 이진 코드입니다. 사람이 직접 작성하기 매우 어렵습니다.
10110000 01100001
2세대: 어셈블리어 (1950년대)
기계어를 기호(mnemonic)로 표현한 저급 언어입니다.
MOV AL, 61h
ADD AL, BL
3세대: 고급 언어 (1960년대~)
Fortran, COBOL, C, Java 등 사람이 이해하기 쉬운 언어가 등장했습니다.
int result = a + b;
4세대 이상
SQL 같은 도메인 특화 언어(DSL)와 Python, JavaScript 같은 스크립트 언어가 포함됩니다.
언어 설계와 컴파일러의 상호 작용
프로그래밍 언어의 설계와 컴파일러 기술은 서로 영향을 주고받습니다.
- **Fortran**: 효율적인 수치 연산 컴파일이 목표였습니다.
- **Java**: 바이트코드와 JIT 컴파일러를 통한 이식성을 강조했습니다.
- **Rust**: 컴파일 타임 소유권 검사로 메모리 안전성을 보장합니다.
2. 정적(Static) vs 동적(Dynamic) 구분
컴파일러 이론에서 **정적**이란 컴파일 시간에 결정되는 것을, **동적**이란 실행 시간에 결정되는 것을 의미합니다.
정적 타입 언어
변수의 타입이 컴파일 시간에 결정됩니다.
int x = 10; // x는 컴파일 시 int로 확정
String s = "hello"; // s는 컴파일 시 String으로 확정
// x = "world"; // 컴파일 에러!
**장점**: 타입 오류를 실행 전에 발견할 수 있으며, 최적화가 용이합니다.
동적 타입 언어
변수의 타입이 실행 시간에 결정됩니다.
x = 10 # x는 현재 int
x = "hello" # x가 str로 변경 (에러 없음)
**장점**: 유연하고 빠른 개발이 가능합니다.
정적/동적 비교
컴파일 시간 실행 시간
(Static) (Dynamic)
타입 결정: Java, C, Rust Python, JS, Ruby
바인딩: 정적 바인딩 동적 바인딩
디스패치: 정적 디스패치 동적 디스패치 (가상 함수)
메모리: 스택 할당 결정 힙 할당 결정
3. 스코프 규칙(Scope Rules)
**스코프(scope)**란 프로그램에서 변수 선언이 유효한 범위를 말합니다.
정적 스코프(Static/Lexical Scope)
변수의 스코프가 **소스 코드의 구조**에 의해 결정됩니다. 대부분의 현대 언어(C, Java, Python 등)가 정적 스코프를 사용합니다.
int x = 10;
void foo() {
printf("%d", x); // 항상 전역 변수 x = 10 참조
}
void bar() {
int x = 20;
foo(); // foo 안에서 x는 여전히 10
}
정적 스코프에서는 함수가 **정의된 위치**에서 변수를 찾습니다.
동적 스코프(Dynamic Scope)
변수의 스코프가 **실행 시간의 호출 순서**에 의해 결정됩니다. 일부 Lisp 방언과 Bash 스크립트가 동적 스코프를 사용합니다.
#!/bin/bash
x=10
foo() {
echo $x # 호출 시점의 x 값을 참조
}
bar() {
local x=20
foo # 동적 스코프이므로 x = 20 출력
}
bar # 출력: 20
동적 스코프에서는 함수가 **호출된 위치**에서 변수를 찾습니다.
블록 구조와 스코프
C 계열 언어에서 블록(`{`...`}`)은 새로운 스코프를 만듭니다.
void example() {
int x = 1;
{
int x = 2; // 내부 블록의 x (외부 x를 가림)
printf("%d\n", x); // 2 출력
}
printf("%d\n", x); // 1 출력 (외부 x)
}
컴파일러는 **스코프 체인(scope chain)**을 통해 변수 이름을 해결합니다. 현재 스코프에서 찾지 못하면 바깥 스코프를 차례로 탐색합니다.
4. 매개변수 전달 방식(Parameter Passing)
함수에 인자를 전달하는 방식은 언어마다 다릅니다.
값에 의한 호출(Call by Value)
인자의 **값을 복사**하여 함수에 전달합니다.
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
// a, b는 복사본이므로 원본에 영향 없음
}
int main() {
int x = 1, y = 2;
swap(x, y);
// x = 1, y = 2 (변경 안 됨)
}
참조에 의한 호출(Call by Reference)
인자의 **메모리 주소**를 전달합니다.
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
// a, b는 원본의 별칭이므로 원본이 변경됨
}
int main() {
int x = 1, y = 2;
swap(x, y);
// x = 2, y = 1 (변경됨)
}
이름에 의한 호출(Call by Name)
인자의 **표현식 자체**를 전달하며, 사용할 때마다 재평가합니다. Algol 60에서 사용했고 현대 언어에서는 거의 사용하지 않지만, Scala의 `=> T` 문법이 유사한 개념입니다.
def condition(test: => Boolean, msg: => String): Unit = {
if (test) println(msg) // msg는 test가 true일 때만 평가됨
}
매개변수 전달 방식 비교
+------------------+----------+----------+------------------+
| 방식 | 복사 비용 | 원본 수정 | 대표 언어 |
+------------------+----------+----------+------------------+
| Call by Value | 있음 | 불가 | C, Java(기본타입) |
| Call by Reference| 없음 | 가능 | C++, C#(ref) |
| Call by Name | 없음 | 가능 | Algol 60, Scala |
+------------------+----------+----------+------------------+
5. 환경(Environment)과 상태(State)
컴파일러 이론에서 중요한 두 가지 매핑이 있습니다.
이름(name) --[환경]--> 저장 위치(location) --[상태]--> 값(value)
예:
"x" --[환경]--> 0x7fff1234 --[상태]--> 42
- **환경(Environment)**: 이름을 저장 위치에 매핑합니다. 변수 선언 시 생성됩니다.
- **상태(State)**: 저장 위치를 값에 매핑합니다. 대입문 실행 시 변경됩니다.
이 구분은 포인터와 참조를 이해하는 데 중요합니다. 같은 이름이 다른 환경에서 다른 위치를 가리킬 수 있고(스코프), 같은 위치의 값은 프로그램 실행 중 바뀔 수 있습니다(상태 변경).
6. 컴파일러 기술의 응용 분야
컴파일러 기술은 프로그래밍 언어 번역 외에도 다양한 분야에서 활용됩니다.
IDE 도구
현대 IDE(IntelliJ, VS Code 등)는 컴파일러 기술을 사용합니다.
- **구문 강조(Syntax Highlighting)**: 어휘 분석 기술을 활용합니다.
- **코드 자동 완성**: 구문 분석과 타입 분석 결과를 이용합니다.
- **리팩토링**: 구문 트리 변환 기술을 적용합니다.
- **실시간 에러 표시**: 증분 파싱(incremental parsing) 기술을 사용합니다.
보안 분석
- **정적 분석(Static Analysis)**: 코드를 실행하지 않고 잠재적 취약점을 탐지합니다.
- **버퍼 오버플로 탐지**: 데이터 흐름 분석을 통해 버퍼 크기 초과를 검사합니다.
- **오염 분석(Taint Analysis)**: 외부 입력이 위험한 함수에 도달하는지 추적합니다.
[보안 정적 분석의 흐름]
소스 코드 --> [구문 분석] --> [데이터 흐름 분석] --> [취약점 보고]
프로그램 검증
컴파일러의 의미 분석 기술을 확장하여 프로그램이 명세를 충족하는지 검증합니다.
- **모델 체킹(Model Checking)**: 상태 공간 탐색을 통한 속성 검증
- **추상 해석(Abstract Interpretation)**: 프로그램의 근사적 의미 분석
- **의존 타입(Dependent Types)**: Agda, Idris 같은 언어에서 타입으로 속성을 증명
기타 응용 분야
- **자연어 처리(NLP)**: 문법 분석에 구문 분석 기법을 활용합니다.
- **하드웨어 합성**: VHDL, Verilog 컴파일러가 하드웨어 회로를 생성합니다.
- **데이터베이스 쿼리**: SQL 파서와 쿼리 최적화에 컴파일러 기술을 사용합니다.
- **도메인 특화 언어(DSL)**: 컴파일러 프론트엔드 기술로 커스텀 언어를 구축합니다.
정리
| 개념 | 핵심 내용 |
| ------------- | ------------------------------------------------------ |
| 정적 vs 동적 | 컴파일 시간에 결정되면 정적, 실행 시간에 결정되면 동적 |
| 정적 스코프 | 코드의 구조(정의 위치)로 변수를 해결 |
| 동적 스코프 | 실행 시간 호출 순서로 변수를 해결 |
| 값 전달 | 값 복사, 원본 불변 |
| 참조 전달 | 주소 전달, 원본 변경 가능 |
| 환경 vs 상태 | 이름을 위치로, 위치를 값으로 매핑 |
| 컴파일러 응용 | IDE, 보안 분석, 프로그램 검증, DSL 등 |
프로그래밍 언어의 기본 개념을 이해하는 것은 컴파일러의 각 단계가 왜 필요한지를 이해하는 데 필수적입니다. 다음 글에서는 간단한 구문 지시 번역기를 직접 만들어 보면서 컴파일러의 핵심 원리를 체험해 보겠습니다.
현재 단락 (1/133)
컴파일러를 제대로 이해하려면 프로그래밍 언어 자체에 대한 기초 지식이 필요합니다. 이 글에서는 언어의 발전 과정부터 핵심 개념, 그리고 컴파일러 기술이 컴파일 이외의 분야에 어떻게...