cd ../
Runtime·2025-01-10·6 min read·# entry/001

JIT 컴파일러의 역사와 원리: V8 엔진을 중심으로

Java HotSpot부터 Chrome V8까지, JIT의 등장 배경과 JavaScript 엔진의 최적화 파이프라인(Ignition & TurboFan)을 심층 분석합니다.

JIT(Just-In-Time)의 기원

JIT 컴파일, 즉 프로그램을 실행하는 시점에 코드를 기계어로 번역하는 '동적 번역' 개념은 1960년대부터 존재했습니다. 하지만 당시에는 하드웨어 성능의 한계와 구현의 복잡성으로 인해 상용화되지 못하고 학술적인 개념에 머물러 있었습니다.

이 개념이 세상 밖으로 나오게 된 계기는 JavaHotSpot의 등장 덕분입니다.

Java와 HotSpot의 등장

초기 Java는 'Write Once, Run Anywhere'를 목표로 운영체제에 독립적인 중간 언어인 바이트코드(Bytecode)를 사용했습니다. 하지만 초기 JVM은 이 바이트코드를 한 줄씩 해석하는 인터프리터 방식으로 동작하여 실행 속도가 매우 느리다는 치명적인 단점이 있었습니다.

이 문제를 해결하기 위해 썬 마이크로시스템즈는 1996년, 'HotSpot'이라는 이름의 JIT 컴파일러를 JVM에 도입했습니다.

HotSpot이란?

이름 그대로 '뜨거운 지점(Hot Spot)', 즉 실행 시점에 자주 사용되는 코드를 찾아내어 네이티브 기계어로 컴파일하고 캐싱하는 기술입니다. 이로 인해 인터프리터의 느린 속도를 보완하면서도 전체를 미리 컴파일하는(AOT) 오버헤드를 줄여, Java가 엔터프라이즈 시장을 장악하는 발판이 되었습니다.

JavaScript 성능 전쟁의 서막

초기 JavaScript 역시 순수한 인터프리터 언어로, 복잡한 연산 성능이 매우 낮았습니다. 하지만 웹이 단순 문서 표시를 넘어 동적 애플리케이션 플랫폼으로 진화하면서 성능 향상이 절실해졌습니다.

  • V8 엔진의 충격 (2008): 구글이 크롬(Chrome) 브라우저와 함께 공개한 V8 엔진은 JIT 컴파일을 기본으로 탑재하여 기존 엔진들을 압도하는 성능을 보여주었습니다.
  • 브라우저 성능 전쟁: V8의 등장을 기점으로 Mozilla(SpiderMonkey), Apple(JavaScriptCore), MS(Chakra) 등 브라우저 벤더 간의 치열한 최적화 경쟁이 시작되었고, 이는 JavaScript가 컴파일 언어에 준하는 성능을 갖게 된 계기가 되었습니다.

V8 엔진의 JIT 컴파일 동작 방식

JavaScript 엔진은 코드를 무작정 컴파일하지 않습니다. 인터프리터의 '빠른 시동'컴파일러의 '고속 주행' 장점만을 결합한 영리한 파이프라인을 가지고 있습니다.

V8 엔진의 핵심인 Ignition(인터프리터)TurboFan(최적화 컴파일러)의 협업 과정을 살펴봅시다.

1

Parsing (구문 분석)

엔진은 개발자가 작성한 JavaScript 코드를 읽어 들여 AST(Abstract Syntax Tree, 추상 구문 트리)라는 컴퓨터가 이해하기 쉬운 구조로 변환합니다. 이 과정에서 문법 오류 등을 검출합니다.

2

Ignition: 인터프리터 실행 & 프로파일링

V8의 인터프리터인 Ignition은 AST를 기반으로 바이트코드를 생성하고 즉시 실행합니다.

  • 빠른 시작: 컴파일을 기다릴 필요 없이 코드가 바로 실행됩니다.
  • 프로파일링(Profiling): 코드를 실행하면서 "어떤 함수가 자주 호출되는지", "인자의 타입은 무엇인지" 등의 정보를 수집합니다.
3

TurboFan: 최적화 컴파일

수집된 프로파일링 데이터를 바탕으로 '최적화할 가치가 있는 코드(Hot Code)'가 식별되면, 이를 TurboFan으로 보냅니다.

  • 가정(Speculation) 기반 최적화: "이 함수는 지금까지 숫자만 들어왔으니, 앞으로도 숫자만 다룰 것이다"라고 가정하고, 불필요한 타입 체크를 제거한 초고속 기계어를 생성합니다. 이는 기존 바이트코드보다 수십~수백 배 빠릅니다.
4

Deoptimization: 코드 교체 및 역최적화

최적화된 기계어가 생성되면 기존 바이트코드를 대체합니다. 하지만 만약 "숫자만 들어올 것"이라는 가정이 틀렸다면(예: 갑자기 문자열이 들어옴) 어떻게 될까요?

엔진은 즉시 최적화된 코드를 폐기(Deoptimization)하고, 다시 안전한 바이트코드(Ignition)로 돌아가 실행합니다. 그리고 프로파일링 데이터를 갱신하여 나중에 다시 최적화를 시도합니다.

이처럼 JIT는 "일단 빠르게 시작하고(Ignition), 자주 쓰는 건 최적화하고(TurboFan), 틀리면 원상복구(Deopt)한다"는 실용적인 전략을 취합니다.


최적화 후보 선정의 비밀 (Heuristics)

엔진은 코드를 최적화하는 데 드는 비용(CPU, Memory) 대비 이익(속도 향상)을 끊임없이 계산합니다. 이 '눈치 게임'의 핵심 기준은 빈도(Frequency)입니다.

호출 빈도 및 루프 실행 횟수

엔진 내부에는 각 함수나 코드 블록마다 카운터(Counter)가 존재합니다.

  1. 함수 호출 카운터: 함수가 호출될 때마다 증가합니다.
  2. 루프 실행 카운터: 함수가 한 번만 호출되더라도, 내부의 for, while 루프가 많이 돌면 Hot Loop로 간주합니다.

일정 횟수 이상 실행되어 Warm 상태가 되면 1차 최적화 후보가 되고, 임계값을 넘어 Hot 상태가 되면 TurboFan의 대상이 됩니다.

Q. 실행 중인 함수를 어떻게 중간에 최적화하나요?

On-Stack Replacement (OSR) 기술을 사용합니다.

  1. 함수가 실행될 때마다 내부 카운터가 증가합니다.
  2. 임계값(Threshold)에 도달하면 엔진은 해당 함수에 "최적화 필요" 플래그를 세웁니다.
  3. 엔진은 별도의 백그라운드 스레드를 통해 TurboFan을 깨워 최적화된 기계어를 생성합니다.
  4. 다음 호출부터는(혹은 루프 도중이라도) 스택 프레임을 교체하여 최적화된 코드로 갈아탑니다.

브라우저 vs 서버(Node.js)의 JIT 전략

"카운팅 기반이라면, 사용자와 상호작용이 짧은 브라우저는 컴파일을 안 하고, 오래 켜져 있는 Node.js 서버는 다 컴파일하는 걸까요?"

그렇지 않습니다. 중요한 건 "무엇을, 얼마나 깊게" 컴파일하느냐입니다.

환경별 JIT 전략 차이

🖥️ 브라우저 (Chrome)

  • 모든 코드를 컴파일하지 않습니다. 렌더링용 일회성 함수는 컴파일 비용이 아깝기 때문입니다.
  • 대신 스크롤 이벤트, 애니메이션, 차트 렌더링 등 UX에 직접 영향을 주는 반복적인 코드를 귀신같이 찾아내어 신속하게 최적화합니다.

⚙️ 서버 (Node.js)

  • 서버 시작 시 한 번 실행되는 초기화 코드나 에러 처리 코드는 컴파일하지 않습니다.
  • API 핸들러, 데이터 처리 로직 등 처리량(Throughput)을 좌우하는 '우량주' 코드를 식별하여, CPU 자원을 아낌없이 투자해 가장 깊은 단계의 최적화를 수행합니다.