Skip to content

필사 모드: [컴파일러] 02. 프로그래밍 언어 기초와 컴파일러 기술 응용

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

프로그래밍 언어 기초와 컴파일러 기술 응용

컴파일러를 제대로 이해하려면 프로그래밍 언어 자체에 대한 기초 지식이 필요합니다. 이 글에서는 언어의 발전 과정부터 핵심 개념, 그리고 컴파일러 기술이 컴파일 이외의 분야에 어떻게 활용되는지를 다룹니다.

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)

컴파일러를 제대로 이해하려면 프로그래밍 언어 자체에 대한 기초 지식이 필요합니다. 이 글에서는 언어의 발전 과정부터 핵심 개념, 그리고 컴파일러 기술이 컴파일 이외의 분야에 어떻게...

작성 글자: 0원문 글자: 4,043작성 단락: 0/133