Skip to content
Published on

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

Authors

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

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


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 등

프로그래밍 언어의 기본 개념을 이해하는 것은 컴파일러의 각 단계가 왜 필요한지를 이해하는 데 필수적입니다. 다음 글에서는 간단한 구문 지시 번역기를 직접 만들어 보면서 컴파일러의 핵심 원리를 체험해 보겠습니다.