Skip to content
Published on

컴파일러와 인터프리터: 코드가 실행되는 원리 — 개발자가 알아야 할 언어 처리의 모든 것

Authors

1. 왜 컴파일 과정을 이해해야 하는가

"코드를 작성하면 실행된다"는 단순한 사실 뒤에는 수십 년간 축적된 언어 처리 기술이 숨어 있습니다. 컴파일러와 인터프리터의 동작 원리를 이해하면 다음과 같은 실질적 이점을 얻습니다.

성능 직관 — V8의 Hidden Class가 왜 중요한지, Python이 왜 느린지, JVM Warm-up이 무엇인지 이해할 수 있습니다.

디버깅 능력 — "SyntaxError: Unexpected token"이 파서 단계에서 발생하는 에러라는 것을 알면 디버깅이 빨라집니다.

도구 활용 — Babel, TypeScript, ESLint, Prettier 모두 컴파일러 기술(렉서, 파서, AST)을 사용합니다. 원리를 알면 커스텀 플러그인도 만들 수 있습니다.

면접 대비 — FAANG 및 대형 IT 기업 면접에서 자주 등장하는 주제입니다.


2. 컴파일레이션 파이프라인 전체 조감도

소스 코드가 실행 가능한 프로그램이 되기까지의 과정을 단계별로 살펴보겠습니다.

소스코드 → [렉서] → 토큰 → [파서]AST[의미 분석]IR[최적화][코드 생성] → 실행
단계입력출력핵심 역할
렉싱(Lexing)소스 코드 문자열토큰 스트림문자열을 의미 단위로 분리
파싱(Parsing)토큰 스트림AST문법 구조를 트리로 변환
의미 분석AST타입 체크된 AST타입 검사, 스코프 분석
IR 생성AST중간 표현(IR)플랫폼 독립적 중간 코드
최적화IR최적화된 IR상수 폴딩, 인라이닝 등
코드 생성최적화된 IR기계어/바이트코드타겟 플랫폼 코드 생성

이 파이프라인은 GCC, LLVM, V8, JVM 등 거의 모든 언어 처리 시스템에서 공통적으로 사용됩니다. 각 단계를 깊이 살펴보겠습니다.


3. 렉서(Lexer) / 토크나이저(Tokenizer)

렉서는 소스 코드 문자열을 토큰(Token) 이라는 의미 단위로 분리합니다. 공백과 주석을 제거하고, 키워드·식별자·리터럴·연산자를 구분합니다.

토큰의 종류

# 입력: "let x = 42 + y;"
# 출력 토큰:
# [LET, "let"] [IDENT, "x"] [ASSIGN, "="] [NUMBER, "42"]
# [PLUS, "+"] [IDENT, "y"] [SEMICOLON, ";"]

간단한 Python 렉서 구현

import re
from enum import Enum, auto
from dataclasses import dataclass
from typing import List

class TokenType(Enum):
    NUMBER = auto()
    PLUS = auto()
    MINUS = auto()
    STAR = auto()
    SLASH = auto()
    LPAREN = auto()
    RPAREN = auto()
    IDENT = auto()
    ASSIGN = auto()
    LET = auto()
    EOF = auto()

@dataclass
class Token:
    type: TokenType
    value: str
    line: int
    col: int

KEYWORDS = {"let": TokenType.LET}

TOKEN_PATTERNS = [
    (r'\d+(\.\d+)?', TokenType.NUMBER),
    (r'[a-zA-Z_]\w*', TokenType.IDENT),
    (r'\+', TokenType.PLUS),
    (r'-', TokenType.MINUS),
    (r'\*', TokenType.STAR),
    (r'/', TokenType.SLASH),
    (r'\(', TokenType.LPAREN),
    (r'\)', TokenType.RPAREN),
    (r'=', TokenType.ASSIGN),
]

def tokenize(source: str) -> List[Token]:
    tokens = []
    pos = 0
    line = 1
    col = 1

    while pos < len(source):
        # 공백 건너뛰기
        if source[pos] in ' \t':
            pos += 1
            col += 1
            continue
        if source[pos] == '\n':
            pos += 1
            line += 1
            col = 1
            continue

        matched = False
        for pattern, token_type in TOKEN_PATTERNS:
            regex = re.compile(pattern)
            match = regex.match(source, pos)
            if match:
                value = match.group()
                # 키워드 확인
                actual_type = KEYWORDS.get(value, token_type)
                tokens.append(Token(actual_type, value, line, col))
                pos = match.end()
                col += len(value)
                matched = True
                break

        if not matched:
            raise SyntaxError(
                f"Unexpected char '{source[pos]}' at {line}:{col}"
            )

    tokens.append(Token(TokenType.EOF, "", line, col))
    return tokens

# 테스트
source = "let x = 42 + y"
for tok in tokenize(source):
    print(f"{tok.type.name:8} | {tok.value}")

출력 결과:

LET      | let
IDENT    | x
ASSIGN   | =
NUMBER   | 42
PLUS     | +
IDENT    | y
EOF      |

실제 언어의 렉서는 문자열 리터럴, 주석, 유니코드 처리 등 훨씬 복잡하지만, 핵심 원리는 동일합니다.


4. 파서(Parser)와 AST(추상 구문 트리)

파서는 토큰 스트림을 받아 AST(Abstract Syntax Tree) 를 생성합니다. AST는 코드의 논리적 구조를 트리 형태로 표현한 것입니다.

재귀 하강 파서(Recursive Descent Parser)

가장 직관적인 파싱 기법으로, 각 문법 규칙을 하나의 함수로 구현합니다.

@dataclass
class NumberNode:
    value: float

@dataclass
class BinaryOpNode:
    left: any
    op: str
    right: any

@dataclass
class AssignNode:
    name: str
    value: any

class Parser:
    def __init__(self, tokens: List[Token]):
        self.tokens = tokens
        self.pos = 0

    def current(self) -> Token:
        return self.tokens[self.pos]

    def eat(self, token_type: TokenType) -> Token:
        token = self.current()
        if token.type != token_type:
            raise SyntaxError(
                f"Expected {token_type}, got {token.type}"
            )
        self.pos += 1
        return token

    def parse_expression(self):
        left = self.parse_term()
        while self.current().type in (TokenType.PLUS, TokenType.MINUS):
            op = self.eat(self.current().type).value
            right = self.parse_term()
            left = BinaryOpNode(left, op, right)
        return left

    def parse_term(self):
        left = self.parse_factor()
        while self.current().type in (TokenType.STAR, TokenType.SLASH):
            op = self.eat(self.current().type).value
            right = self.parse_factor()
            left = BinaryOpNode(left, op, right)
        return left

    def parse_factor(self):
        token = self.current()
        if token.type == TokenType.NUMBER:
            self.eat(TokenType.NUMBER)
            return NumberNode(float(token.value))
        elif token.type == TokenType.LPAREN:
            self.eat(TokenType.LPAREN)
            node = self.parse_expression()
            self.eat(TokenType.RPAREN)
            return node
        elif token.type == TokenType.IDENT:
            self.eat(TokenType.IDENT)
            return token.value
        raise SyntaxError(f"Unexpected token: {token}")

AST 시각화

let x = 42 + y * 2의 AST를 시각화하면:

    AssignNode
    ├── name: "x"
    └── value: BinaryOpNode(+)
              ├── NumberNode(42)
              └── BinaryOpNode(*)
                        ├── "y"
                        └── NumberNode(2)

Pratt Parsing (연산자 우선순위 파서)

Pratt 파서는 연산자 우선순위를 우아하게 처리합니다. 각 토큰에 바인딩 파워(binding power)를 할당합니다.

PRECEDENCE = {
    TokenType.PLUS: 10,
    TokenType.MINUS: 10,
    TokenType.STAR: 20,
    TokenType.SLASH: 20,
}

def pratt_parse(parser, min_bp=0):
    # prefix (숫자, 식별자)
    left = parser.parse_factor()

    while True:
        op = parser.current()
        bp = PRECEDENCE.get(op.type, 0)
        if bp <= min_bp:
            break
        parser.eat(op.type)
        right = pratt_parse(parser, bp)
        left = BinaryOpNode(left, op.value, right)

    return left

Pratt 파싱은 Rust 컴파일러, ESLint, TypeScript 파서 등 많은 프로덕션 파서에서 사용됩니다.


5. 컴파일러 vs 인터프리터 vs JIT

비교표

특성순수 컴파일러순수 인터프리터JIT 컴파일러
대표 언어C, C++, Rust, GoShell script, 초기 BASICJavaScript(V8), Java(HotSpot)
실행 전 단계전체 컴파일 필요없음바이트코드 컴파일
실행 속도매우 빠름느림빠름 (Warm-up 후)
시작 시간느림 (컴파일 시간)빠름중간
메모리 사용낮음중간높음 (컴파일러 포함)
디버깅어려움쉬움중간
최적화 수준정적 분석 기반없음런타임 프로파일링 기반

하이브리드 접근의 현실

현대 언어 구현은 대부분 하이브리드 접근을 사용합니다:

  • JavaScript (V8): 파싱 → 바이트코드(Ignition) → JIT(TurboFan)
  • Python (CPython): 파싱 → 바이트코드 → 인터프리터 (PyPy는 JIT 추가)
  • Java (HotSpot): javac 컴파일 → 바이트코드 → 인터프리터 + C1/C2 JIT
  • C# (.NET): Roslyn 컴파일 → IL → JIT (RyuJIT)

순수한 컴파일러나 순수한 인터프리터는 현대에서는 드뭅니다.


6. V8 엔진: JavaScript의 실행 원리

Google의 V8 엔진은 Chrome과 Node.js의 핵심입니다. 어떻게 JavaScript를 빠르게 실행하는지 살펴보겠습니다.

V8 파이프라인

JavaScript 소스
  파서 → AST
  Ignition (바이트코드 인터프리터)
     (프로파일링 데이터 수집)
  TurboFan (최적화 JIT 컴파일러)
  최적화된 기계어
     (Deoptimization — 가정 깨지면 되돌림)
  Ignition으로 복귀

Hidden Classes (히든 클래스)

V8은 JavaScript 객체에 Hidden Class(Shape) 를 부여하여 프로퍼티 접근을 최적화합니다.

// 좋은 패턴 — 같은 Hidden Class 공유
function Point(x, y) {
  this.x = x;  // Hidden Class C0 → C1
  this.y = y;  // Hidden Class C1 → C2
}
const p1 = new Point(1, 2);  // Shape: C2
const p2 = new Point(3, 4);  // Shape: C2 (동일!)

// 나쁜 패턴 — Hidden Class 불일치
const a = {};
a.x = 1;
a.y = 2;  // Shape: S1

const b = {};
b.y = 2;  // 순서 다름!
b.x = 1;  // Shape: S2 (다른 Shape!)

프로퍼티를 항상 같은 순서로 추가하면 V8이 같은 Hidden Class를 재사용하여 성능이 향상됩니다.

Inline Caches (인라인 캐시)

V8은 프로퍼티 접근 위치마다 캐시를 유지합니다.

function getX(obj) {
  return obj.x;  // Inline Cache가 여기에 생성
}

// Monomorphic (1개 Shape) — 가장 빠름
getX(p1);  // Shape C2로 캐시
getX(p2);  // Shape C2 — 캐시 히트!

// Polymorphic (2-4개 Shape) — 약간 느림
// Megamorphic (5개 이상 Shape) — 느림, 일반 경로 사용

V8 최적화 팁 요약

  1. 객체 프로퍼티는 항상 같은 순서로 초기화
  2. 함수에 전달하는 인자 타입을 일관되게 유지
  3. 배열의 타입을 혼합하지 않기 (숫자 배열에 문자열 넣지 않기)
  4. delete obj.prop 대신 obj.prop = undefined 사용
  5. try-catch 블록을 최소화하고 별도 함수로 분리

7. CPython: Python의 실행 원리

CPython 바이트코드

CPython은 소스 코드를 바이트코드로 컴파일한 뒤 가상 머신에서 실행합니다.

import dis

def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 바이트코드 확인
dis.dis(fibonacci)

출력 (간략화):

  2           0 LOAD_FAST                0 (n)
              2 LOAD_CONST               1 (1)
              4 COMPARE_OP               1 (<=)
              6 POP_JUMP_IF_FALSE       12

  3           8 LOAD_FAST                0 (n)
             10 RETURN_VALUE

  4     >>   12 LOAD_GLOBAL              0 (fibonacci)
             14 LOAD_FAST                0 (n)
             16 LOAD_CONST               2 (1)
             18 BINARY_SUBTRACT
             20 CALL_FUNCTION            1
             ...

GIL (Global Interpreter Lock)

CPython의 GIL은 한 번에 하나의 스레드만 파이썬 바이트코드를 실행할 수 있게 합니다.

# GIL 때문에 CPU-bound 작업에서 멀티스레딩이 비효율적
import threading
import time

def cpu_bound(n):
    count = 0
    for i in range(n):
        count += i
    return count

# 싱글 스레드
start = time.time()
cpu_bound(50_000_000)
cpu_bound(50_000_000)
print(f"Single: {time.time() - start:.2f}s")

# 멀티 스레드 (GIL 때문에 더 느릴 수 있음)
start = time.time()
t1 = threading.Thread(target=cpu_bound, args=(50_000_000,))
t2 = threading.Thread(target=cpu_bound, args=(50_000_000,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"Multi:  {time.time() - start:.2f}s")

# 해결책: multiprocessing 사용
from multiprocessing import Pool
start = time.time()
with Pool(2) as p:
    p.map(cpu_bound, [50_000_000, 50_000_000])
print(f"Multi-process: {time.time() - start:.2f}s")

PyPy: Python의 JIT 대안

PyPy는 RPython으로 작성된 Python 인터프리터로, JIT 컴파일을 지원합니다.

CPython:  fibonacci(35)~4.5PyPy:     fibonacci(35)~0.3 (15배 빠름)

PyPy가 빠른 이유:

  • 트레이싱 JIT — 자주 실행되는 루프를 기계어로 컴파일
  • 가드(Guard) — 타입 가정이 깨지면 인터프리터로 복귀
  • GIL은 여전히 존재 — 하지만 STM(Software Transactional Memory) 실험 중

8. JVM: Java의 실행 원리

javac에서 바이트코드까지

// Hello.java
public class Hello {
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 0; i < 1000; i++) {
            sum += i;
        }
        System.out.println(sum);
    }
}
# 컴파일
javac Hello.java

# 바이트코드 확인
javap -c Hello.class
public static void main(java.lang.String[]);
  Code:
     0: iconst_0
     1: istore_1        // sum = 0
     2: iconst_0
     3: istore_2        // i = 0
     4: iload_2
     5: sipush  1000
     8: if_icmpge  21   // if i >= 1000 goto 21
    11: iload_1
    12: iload_2
    13: iadd
    14: istore_1        // sum += i
    15: iinc    2, 1    // i++
    18: goto    4
    21: ...             // println

HotSpot JIT: C1과 C2 컴파일러

바이트코드 실행 (인터프리터)
     (메서드 실행 횟수 카운트)
  C1 컴파일러 (빠른 컴파일, 기본 최적화)
     (더 많이 실행되면)
  C2 컴파일러 (느린 컴파일, 공격적 최적화)
  최적화된 기계어

C1과 C2의 차이:

특성C1 (Client)C2 (Server)
컴파일 속도빠름느림
최적화 수준기본공격적
인라이닝제한적적극적
루프 최적화기본언롤링, 벡터화
사용 시점초기핫 메서드

GraalVM: 다언어 런타임

GraalVM은 Java 바이트코드뿐만 아니라 JavaScript, Python, Ruby, R, LLVM 기반 언어(C/C++, Rust)까지 실행할 수 있는 범용 가상 머신입니다.

# GraalVM Native Image — AOT 컴파일
native-image -jar myapp.jar -o myapp

# 결과: 기동 시간 수십 밀리초의 네이티브 바이너리
# JVM: ~2초 시작 → GraalVM Native: ~20ms 시작

GraalVM Native Image의 특징:

  • AOT(Ahead-of-Time) 컴파일 — 런타임 JIT 없이 네이티브 코드 생성
  • 빠른 시작 시간 — 서버리스, CLI 도구에 적합
  • 적은 메모리 사용 — JVM 런타임 불필요
  • 제약 — 리플렉션, 동적 클래스 로딩 등 일부 기능 제한

9. LLVM: 모듈형 컴파일러 프레임워크

LLVM은 현대 컴파일러의 표준 인프라입니다. Clang(C/C++), rustc(Rust), swiftc(Swift) 등이 LLVM을 백엔드로 사용합니다.

LLVM 아키텍처

프론트엔드           미들엔드              백엔드
(언어별)            (공통)               (타겟별)
                                    ┌→ x86_64
Clang ─┐         ┌→ 최적화 패스 ─┤→ ARM
       ├→ LLVM IR ┤              └→ WASM
rustc ─┤         └→ 분석 패스
swiftc ┘

LLVM IR 예시

; 함수: add(a, b) = a + b
define i32 @add(i32 %a, i32 %b) {
entry:
  %result = add i32 %a, %b
  ret i32 %result
}

; 함수: factorial(n)
define i64 @factorial(i64 %n) {
entry:
  %cmp = icmp sle i64 %n, 1
  br i1 %cmp, label %base, label %recurse

base:
  ret i64 1

recurse:
  %n_minus_1 = sub i64 %n, 1
  %sub_result = call i64 @factorial(i64 %n_minus_1)
  %result = mul i64 %n, %sub_result
  ret i64 %result
}

주요 최적화 패스

# LLVM 최적화 패스 적용
opt -O2 input.ll -o optimized.ll

# 주요 최적화 패스:
# -mem2reg     : 메모리 접근을 레지스터로 승격
# -inline      : 함수 인라이닝
# -gvn         : Global Value Numbering (중복 계산 제거)
# -licm        : Loop Invariant Code Motion (루프 밖으로 이동)
# -loop-unroll : 루프 언롤링
# -sccp        : Sparse Conditional Constant Propagation
# -dce         : Dead Code Elimination

상수 폴딩 예시

// 최적화 전
int x = 3 + 4;
int y = x * 2;

// 상수 폴딩 후
int x = 7;
int y = 14;

LLVM이 중요한 이유:

  • 모듈성 — 프론트엔드와 백엔드를 독립적으로 개발 가능
  • 공유 최적화 — 모든 언어가 같은 최적화 패스 혜택
  • 새 언어 개발 용이 — LLVM IR만 생성하면 다양한 플랫폼 지원

10. 가비지 컬렉션(GC) 알고리즘

Mark-and-Sweep

가장 기본적인 GC 알고리즘입니다.

1. Mark 단계: 루트에서 도달 가능한 모든 객체를 마킹
2. Sweep 단계: 마킹되지 않은 객체의 메모리 해제

루트(Root):
  - 전역 변수
  - 스택의 지역 변수
  - 레지스터

[A][B][C]
      [D]

[E][F]  (루트에서 도달 불가 → 수집 대상)

Generational GC

"대부분의 객체는 금방 죽는다"는 세대 가설(Generational Hypothesis) 에 기반합니다.

Young Generation (Minor GC — 자주, 빠르게)
  ├── Eden Space (새 객체 할당)
  ├── Survivor 0
  └── Survivor 1

Old Generation (Major GC — 드물게, 느리게)
  └── 오래 살아남은 객체들

GC 과정:
1. 새 객체 → Eden에 할당
2. Eden이 가득 차면 Minor GC
3. 살아남은 객체 → Survivor로 이동
4. N번 살아남으면 → Old Generation으로 승격
5. Old Generation이 가득 차면 Major GC

현대 GC 알고리즘 비교

GC사용처특징중단 시간 목표
G1 GCJVM (기본)리전 기반, 예측 가능200ms 이하
ZGCJVM (최신)동시 수행, 포인터 컬러링1ms 이하
ShenandoahJVM (Red Hat)동시 수행, 브룩스 포인터10ms 이하
OrinocoV8 (JS)세대별, 증분, 동시 마킹최소화
참조 카운팅CPython, Swift즉시 해제, 순환 참조 주의없음 (분산)
소유권 시스템RustGC 없음, 컴파일 타임 메모리 관리없음

ZGC의 핵심 아이디어

전통적 GC:       Stop-the-WorldMarkCompactResume
                 (긴 중단 시간)

ZGC:             애플리케이션 실행과 GC가 동시에 수행
                 (포인터 컬러링 + 로드 배리어로 일관성 유지)

포인터 컬러링:
  64비트 포인터의 상위 비트를 GC 메타데이터로 사용
  [metadata bits][actual address bits]

  - Marked0 비트: 첫 번째 마킹 사이클
  - Marked1 비트: 두 번째 마킹 사이클
  - Remapped 비트: 객체가 재배치됨
  - Finalizable 비트: 파이널라이저 보류 중

11. 빌드 도구도 컴파일러다

프론트엔드 개발자가 매일 사용하는 도구들도 컴파일러 기술을 활용합니다.

Babel: JavaScript 트랜스파일러

// 입력: ES2022+
const greet = (name) => `Hello, ${name}!`;
const items = [1, 2, 3];
const doubled = items.map(x => x * 2);

// Babel 변환 후: ES5
"use strict";
var greet = function greet(name) {
  return "Hello, " + name + "!";
};
var items = [1, 2, 3];
var doubled = items.map(function (x) {
  return x * 2;
});

TypeScript 컴파일러 (tsc)

TypeScript 소스
  파서 → AST
  바인더(Binder) → 심볼 테이블
  타입 체커(Type Checker) → 타입 에러 보고
  이미터(Emitter)JavaScript + .d.ts + .map

차세대 번들러 비교

도구언어속도특징
WebpackJS기준성숙한 에코시스템, 플러그인 풍부
esbuildGo10-100x 빠름병렬 처리, 적은 플러그인
SWCRust20-70x 빠름Babel 호환, Next.js 기본
TurbopackRust증분 빌드 최적화Vercel, 대규모 프로젝트
ViteJS (esbuild/Rollup)개발 시 매우 빠름ESM 기반 dev server
RspackRustWebpack 호환, 빠름Webpack API 호환
# esbuild 벤치마크 예시
# 10,000개 모듈 번들링:
# Webpack:   40초
# esbuild:    0.4초

esbuild, SWC가 빠른 이유:

  1. 시스템 언어(Go/Rust) — GC 오버헤드 없음, 네이티브 속도
  2. 병렬 처리 — 멀티코어 적극 활용
  3. 최소한의 AST 변환 — 불필요한 중간 단계 제거
  4. 제로카피 아키텍처 — 메모리 복사 최소화

12. 면접 질문 10선

Q1. 컴파일러와 인터프리터의 차이점은?

컴파일러는 전체 소스 코드를 한 번에 기계어로 변환하고, 인터프리터는 코드를 한 줄씩 실행합니다. 현대 언어 대부분은 하이브리드 방식(바이트코드 컴파일 + JIT)을 사용합니다.

Q2. AST란 무엇이고 어디에 사용되나요?

AST(Abstract Syntax Tree)는 소스 코드의 구조를 트리로 표현한 자료구조입니다. 컴파일러, 린터(ESLint), 포매터(Prettier), 리팩토링 도구, IDE 자동완성 등에서 사용됩니다.

Q3. JIT 컴파일의 장단점은?

장점: 런타임 프로파일링을 통한 적극적 최적화, AOT보다 나은 성능 가능. 단점: Warm-up 시간 필요, 메모리 사용 증가, 컴파일러 복잡도 증가.

Q4. V8의 Hidden Class란?

V8이 동적 타입인 JavaScript 객체에 내부적으로 부여하는 구조 정보입니다. 같은 Shape의 객체들은 프로퍼티 접근이 빨라집니다.

Q5. Python의 GIL이란?

Global Interpreter Lock으로, CPython에서 한 번에 하나의 스레드만 바이트코드를 실행할 수 있게 하는 뮤텍스입니다. CPU-bound 작업에서 멀티스레딩의 이점이 없어지는 원인입니다.

Q6. Generational GC의 원리는?

"대부분의 객체는 금방 죽는다"는 세대 가설에 기반합니다. Young Generation을 자주 수집하여 효율을 높이고, Old Generation은 드물게 수집합니다.

Q7. LLVM이 현대 컴파일러에서 중요한 이유는?

모듈형 아키텍처로 프론트엔드(언어별)와 백엔드(플랫폼별)를 분리합니다. 새 언어는 IR 생성만 하면 모든 플랫폼과 최적화를 자동으로 얻습니다.

Q8. esbuild가 Webpack보다 빠른 이유는?

Go로 작성되어 네이티브 속도와 병렬 처리가 가능하며, GC 오버헤드가 없고, 최소한의 AST 변환만 수행합니다.

Q9. GraalVM Native Image의 장단점은?

장점: 빠른 시작 시간(수십 ms), 적은 메모리. 단점: 빌드 시간 증가, 리플렉션/동적 기능 제한, 피크 성능은 JIT보다 낮을 수 있음.

Q10. ReDoS란?

Regular Expression Denial of Service의 약자로, 비효율적 정규표현식이 지수적 백트래킹을 일으켜 CPU를 과도하게 사용하는 공격입니다. 정규표현식 엔진도 일종의 인터프리터입니다.


13. 퀴즈

Q1. 렉서(Lexer)의 출력은 무엇인가요?

토큰 스트림(Token Stream)입니다. 소스 코드 문자열을 키워드, 식별자, 리터럴, 연산자 등의 의미 단위인 토큰으로 분리합니다.

Q2. V8에서 TurboFan이 최적화한 코드가 무효화되는 상황은?

Deoptimization이 발생합니다. TurboFan이 가정한 타입 정보가 런타임에 깨지는 경우(예: 항상 정수였던 변수에 문자열이 들어오는 경우) 최적화된 코드를 버리고 Ignition 바이트코드로 복귀합니다.

Q3. JVM에서 C1과 C2 컴파일러의 차이는?

C1(Client Compiler)은 빠르게 컴파일하여 기본 최적화를 적용합니다. C2(Server Compiler)는 시간이 걸리지만 인라이닝, 루프 언롤링, 벡터화 등 공격적 최적화를 수행합니다. Tiered Compilation에서 C1이 먼저, 핫 메서드에 C2가 나중에 적용됩니다.

Q4. LLVM IR이 SSA 형식을 사용하는 이유는?

SSA(Static Single Assignment)는 각 변수가 정확히 한 번만 할당되는 형식입니다. 이는 데이터 흐름 분석을 단순화하고, 상수 전파, 죽은 코드 제거 등의 최적화를 용이하게 합니다.

Q5. ZGC의 포인터 컬러링이란?

64비트 포인터의 상위 비트를 GC 메타데이터(마킹 상태, 재배치 여부 등)로 사용하는 기법입니다. 이를 통해 GC가 애플리케이션과 동시에 실행되면서도 객체 참조의 일관성을 유지할 수 있습니다. 1ms 미만의 일시 정지를 달성합니다.


14. 참고 자료

  1. Crafting Interpreters (Robert Nystrom) — 인터프리터 구현의 바이블
  2. Compilers: Principles, Techniques, and Tools (Dragon Book) — 컴파일러 이론의 고전
  3. Engineering a Compiler (Cooper, Torczon) — 현대적 컴파일러 엔지니어링
  4. V8 Bloghttps://v8.dev/blog — V8 엔진 내부 동작 설명
  5. Inside the V8 Engine (Marja Holtta) — V8 파이프라인 상세 설명
  6. CPython Internals (Anthony Shaw) — CPython 소스 코드 해설서
  7. The Architecture of Open Source Applications: LLVM — LLVM 아키텍처 설명
  8. LLVM Language Reference Manualhttps://llvm.org/docs/LangRef.html
  9. Understanding the JVM (주판, 중국어 원서) — JVM 내부 구조 상세
  10. ZGC: Scalable Low-Latency Garbage Collector — Oracle 문서
  11. GraalVM Documentationhttps://www.graalvm.org/docs/
  12. esbuild Architecturehttps://esbuild.github.io/faq/
  13. SWC: Speedy Web Compilerhttps://swc.rs/
  14. Modern Parser Generator (Laurence Tratt) — 파서 생성기 비교
  15. Tiered Compilation in JVMhttps://docs.oracle.com/en/java/
  16. PyPy Documentationhttps://doc.pypy.org/