2025-09-19 21:58

  • JIT] 컴파일은 프로그램 실행 시점에 코드를 기계어로 번역하여 인터프리터의 유연성과 컴파일러의 성능을 모두 잡는 기술이다.

  • 프로파일러가 자주 사용되는 ‘핫스팟’ 코드를 식별하면, 이를 최적화된 기계어로 컴파일하여 이후 호출 시에는 빠른 속도로 실행되게 한다.

  • 현대의 자바 가상 머신(JVM)이나 자바스크립트 엔진(V8) 등은 JIT 컴파일을 통해 동적 언어의 성능 한계를 극복하고 높은 실행 속도를 달성한다.

JIT 컴파일러 완벽 핸드북 인터프리터와 AOT를 넘어

컴퓨터 프로그래밍의 세계는 끊임없는 속도와의 전쟁이다. 개발자들은 더 빠르고 효율적인 애플리케이션을 만들기 위해 노력하고, 그 중심에는 ‘어떻게 코드를 실행할 것인가’라는 근본적인 질문이 있다. 이 질문에 대한 가장 현대적이고 강력한 답변 중 하나가 바로 JIT(Just-In-Time) 컴파일러다. JIT는 마치 게으른 천재와 같다. 모든 일을 미리 해두지 않고, 필요할 때 꼭 필요한 일만 찾아내어 가장 효율적인 방식으로 처리한다.

이 핸드북은 JIT 컴파일러가 왜 등장했으며, 어떤 구조로 작동하고, 어떻게 현대 프로그래밍 언어의 심장이 되었는지 심도 있게 탐구한다. 단순한 개념 설명을 넘어, 그 내부의 복잡한 메커니즘과 개발자가 알아두면 좋을 미묘한 차이점까지 다룰 것이다.

1. 만들어진 이유: 인터프리터와 컴파일러 사이의 딜레마

JIT의 탄생 배경을 이해하려면 먼저 컴퓨터가 코드를 이해하는 두 가지 전통적인 방식, 즉 인터프리터(Interpreter)와 정적 컴파일러(Static Compiler)를 알아야 한다.

인터프리터: 유연한 동시통역사

인터프리터는 코드를 한 줄씩 읽어 내려가며 즉시 실행하는 방식이다. 파이썬(Python)이나 초기 자바스크립트(JavaScript) 같은 언어가 대표적이다.

  • 장점:

    • 플랫폼 독립성: 소스 코드만 있으면 어디서든 실행 가능하다.

    • 빠른 시작: 별도의 빌드 과정 없이 바로 코드를 실행해 볼 수 있다.

    • 동적 코드 실행: 런타임에 코드를 생성하고 실행하는 eval() 같은 기능이 가능하다.

  • 단점:

    • 느린 실행 속도: 매번 코드를 해석해야 하므로, 특히 반복문 같은 구조에서 심각한 성능 저하를 보인다. 같은 코드를 100번 반복한다면, 100번 모두 해석하는 비효율이 발생한다.

이를 비유하자면, 외국어 연설을 한 문장씩 듣고 바로 통역해주는 ‘동시통역사’와 같다. 청중은 즉시 내용을 이해할 수 있지만, 연설 전체의 흐름이나 자주 반복되는 표현을 최적화할 여유는 없다.

정적 컴파일러 (AOT): 철저한 사전 번역가

정적 컴파일러, 또는 AOT(Ahead-of-Time) 컴파일러는 프로그램 실행 전에 소스 코드 전체를 특정 플랫폼(CPU, OS)에 맞는 기계어로 미리 번역해두는 방식이다. C, C++, Go 같은 언어가 이 방식을 사용한다.

  • 장점:

    • 최고의 실행 속도: 이미 모든 코드가 기계어로 번역되어 있어 CPU가 가장 빠르게 이해하고 실행할 수 있다.

    • 전체 코드 최적화: 프로그램 전체를 분석하여 변수 사용, 함수 호출 관계 등을 최적화할 수 있다.

  • 단점:

    • 플랫폼 종속성: 특정 환경에 맞춰 빌드된 실행 파일은 다른 환경에서 작동하지 않는다. (예: 윈도우용 .exe 파일은 리눅스에서 실행 불가)

    • 느린 빌드 시간: 코드를 수정할 때마다 전체를 다시 컴파일해야 한다.

    • 동적 최적화의 한계: 프로그램이 실제로 어떻게 사용될지, 어떤 데이터가 주로 입력될지 알 수 없으므로 런타임 상황에 맞는 최적화는 불가능하다.

이는 책 한 권을 출간하기 전에 전체 원고를 완벽하게 번역하고 편집하여 출판하는 ‘전문 번역가’에 비유할 수 있다. 한번 출판된 책은 독자들이 매우 빠르게 읽을 수 있지만, 오탈자를 수정하거나 내용을 바꾸려면 재출판이라는 무거운 과정을 거쳐야 한다.

JIT: 두 세계의 장점을 결합한 해결사

JIT 컴파일러는 이 두 방식의 딜레마를 해결하기 위해 등장했다. 처음에는 인터프리터처럼 코드를 실행하다가, 프로그램이 실행되는 동안 실시간 데이터를 수집(프로파일링)하여 자주 사용되는 코드 영역, 즉 ‘핫스팟(Hotspot)‘을 찾아낸다. 그리고 바로 그 ‘핫스팟’만을 골라내어 AOT 컴파일러처럼 기계어로 번역한다.

  • JIT의 핵심 아이디어: “모든 코드를 미리 최적화하는 것은 낭비다. 가장 많이 실행되는 부분만 집중적으로 최적화하자.”

이로써 JIT는 인터프리터의 플랫폼 독립성과 빠른 시작이라는 장점, 그리고 AOT 컴파일러의 빠른 실행 속도라는 장점을 모두 취할 수 있게 되었다. 마치 동시통역사가 연설을 통역하다가 연사가 계속 반복하는 중요한 키워드나 문장을 발견하고, 그것만 따로 완벽하게 번역된 메모를 만들어 두었다가 다음부터는 그 메모를 보고 즉시 말해주는 것과 같다.

2. JIT 컴파일러의 핵심 구조와 작동 원리

JIT 컴파일러는 단순한 ‘런타임 번역기’가 아니다. 내부에는 성능을 극대화하기 위한 정교하고 복잡한 시스템이 유기적으로 작동하고 있다. 현대적인 JIT 컴파일러, 특히 자바의 HotSpot JVM이나 구글의 V8 자바스크립트 엔진에 탑재된 것들은 다음과 같은 요소들로 구성된다.

1단계: 프로파일러 (Profiler) - 뜨거운 곳을 찾는 감시자

JIT의 모든 것은 프로파일링에서 시작된다. 프로파일러는 프로그램이 실행되는 동안 코드의 어떤 부분이 얼마나 자주, 그리고 어떻게 호출되는지에 대한 정보를 수집하는 감시자다.

  • 수집 정보:

    • 메서드 호출 빈도

    • 반복문 실행 횟수

    • 분기문(if)의 실행 경로 (어느 쪽이 더 자주 선택되는가)

    • 사용되는 데이터 타입 정보

이 정보를 바탕으로 프로파일러는 최적화가 필요한 ‘핫스팟’을 식별한다. 예를 들어, 특정 메서드가 수천 번 이상 호출되면 JIT 컴파일의 대상 후보로 선정된다.

2단계: 컴파일 큐 (Compilation Queue) - 최적화 대기열

프로파일러가 핫스팟을 식별하면, 해당 코드(주로 메서드 단위)를 컴파일 큐에 등록한다. 백그라운드에서 실행되는 컴파일러 스레드는 이 큐에서 작업을 가져와 최적화 및 컴파일을 수행한다. 이렇게 함으로써 애플리케이션의 메인 실행 흐름을 방해하지 않고 부드럽게 성능을 향상시킨다.

3단계: 계층적 컴파일 (Tiered Compilation) - 속도와 최적화의 균형

모든 핫스팟이 똑같이 뜨겁지는 않다. 어떤 코드는 적당히 뜨겁고, 어떤 코드는 용암처럼 뜨겁다. 모든 핫스팟에 대해 최고 수준의 최적화를 수행하는 것은 시간이 오래 걸려 오히려 성능에 방해가 될 수 있다. 그래서 현대 JIT는 ‘계층적 컴파일’ 전략을 사용한다.

계층 (Tier)이름 (JVM 예시)역할 및 특징비유
Tier 0Interpreter프로파일링 정보를 수집하며 코드를 한 줄씩 실행. 가장 느리지만 모든 정보 수집의 기반.도보 순찰
Tier 1C1 (Client)빠른 컴파일 속도에 중점을 둔다. 기본적인 최적화만 적용하여 일단 빠르게 기계어 코드를 생성.자전거 순찰
Tier 2C2 (Server)매우 뜨거운 코드를 대상으로 한다. 시간이 오래 걸리더라도 가능한 모든 고급 최적화 기법을 총동원.헬리콥터 감시 및 추적

작동 시나리오:

  1. 모든 코드는 Tier 0 (인터프리터)에서 실행 시작. 프로파일링 데이터 수집.

  2. 메서드가 일정 횟수 이상 호출되면 Tier 1 (C1 컴파일러)가 빠르게 컴파일. 이제부터 이 메서드는 더 빠른 C1 컴파일 코드로 실행된다.

  3. C1으로 컴파일된 코드가 여전히 엄청나게 많이 호출되면, 프로파일러는 이를 ‘진정한 핫스팟’으로 판단하고 Tier 2 (C2 컴파일러)에게 작업을 넘긴다.

  4. C2 컴파일러는 시간을 들여 해당 코드를 최고 수준으로 최적화된 기계어로 컴파일한다.

  5. 컴파일이 완료되면, 해당 메서드를 가리키는 주소가 C2가 생성한 코드로 교체된다. 다음 호출부터는 최고 성능으로 실행된다.

4단계: 고급 최적화 기법

C2와 같은 고급 컴파일러는 AOT 컴파일러가 할 수 없는, 런타임 정보에 기반한 강력한 최적화를 수행한다.

  • 인라이닝 (Inlining): 짧고 자주 호출되는 메서드의 코드를 호출한 쪽 코드에 아예 삽입해버리는 기법. 메서드 호출에 드는 비용을 없애고 추가적인 최적화 기회를 만든다.

  • 탈출 분석 (Escape Analysis): 객체가 메서드 외부로 ‘탈출’하지 않고 내부에서만 사용되는 경우, 굳이 힙(Heap) 메모리에 할당하지 않고 스택(Stack)에 할당하거나 아예 객체 생성을 제거하여 가비지 컬렉션 부담을 줄인다.

  • 가상 호출 제거 (Virtual Call Inlining): 객체 지향 언어에서 특정 인터페이스나 부모 클래스 변수가 실제로는 항상 특정 자식 클래스의 인스턴스만 가리킨다는 사실을 런타임에 파악하면, 비싼 가상 호출을 저렴한 직접 호출로 바꾼다.

5단계: 탈최적화 (Deoptimization) - 안전장치

JIT의 강력한 최적화는 ‘가정’에 기반한다. 예를 들어, “지금까지 이 변수는 항상 Integer 타입이었으니, 앞으로도 그럴 것이다”라고 가정하고 최적화를 수행한다. 하지만 만약 이 가정이 깨지면 어떻게 될까? 예를 들어 동적으로 클래스가 로드되어 갑자기 다른 타입의 객체가 들어온다면?

이때 ‘탈최적화’라는 안전장치가 발동한다. JIT는 최적화된 코드를 버리고 다시 안전한 인터프리터 모드(Tier 0)로 돌아가 코드를 실행한다. 이는 최적화로 인한 오류를 방지하고 프로그램의 안정성을 보장하는 매우 중요한 메커니즘이다. 마치 자율주행 자동차가 예측하지 못한 상황을 만나면 즉시 운전자에게 제어권을 넘기는 것과 같다.

3. JIT 컴파일러 사용법: 보이지 않는 손을 이해하기

사실 대부분의 개발자는 JIT 컴파일러를 ‘직접 사용’하지 않는다. JIT는 언어 런타임(JVM, V8 등)의 일부로써 자동으로 작동하기 때문이다. 하지만 개발자는 JIT의 작동 방식을 이해함으로써 JIT가 더 효율적으로 최적화할 수 있는, 즉 ‘JIT 친화적인(JIT-friendly)’ 코드를 작성할 수 있다.

자바(Java)와 JVM 환경

자바는 JIT 컴파일의 혜택을 가장 잘 보여주는 대표적인 언어다. JVM의 HotSpot 기술은 세계 최고 수준의 JIT 컴파일러를 자랑한다.

  • 워밍업 (Warm-up): 자바 애플리케이션은 시작 직후에는 인터프리터로 실행되다가 JIT 컴파일이 점진적으로 이루어지므로, 최고의 성능에 도달하기까지 약간의 ‘워밍업’ 시간이 필요하다. 서버 애플리케이션을 벤치마킹할 때 초반 몇 분간의 성능 데이터를 제외하는 이유가 바로 이 때문이다.

  • JIT 컴파일 관찰: JVM 옵션을 통해 JIT 컴파일 과정을 직접 눈으로 확인할 수 있다.

    • -XX:+PrintCompilation: JIT 컴파일이 발생할 때마다 관련 정보를 콘솔에 출력한다.

    • -XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation: 컴파일 로그를 hotspot.log 파일로 저장하여 나중에 상세히 분석할 수 있다.

자바스크립트(JavaScript)와 V8 엔진 환경

과거 ‘느린 스크립트 언어’의 대명사였던 자바스크립트는 V8과 같은 강력한 JIT 엔진 덕분에 서버(Node.js)와 프론트엔드 모두에서 고성능 언어로 거듭났다.

  • 히든 클래스 (Hidden Class / Shapes): V8 엔진은 객체의 구조(프로퍼티의 종류와 순서)가 동일한 객체들을 같은 ‘히든 클래스’로 관리한다. 코드 내에서 객체의 구조를 일관되게 유지하면 V8이 더 쉽게 코드를 최적화할 수 있다.

    // JIT에 비친화적인 코드
    function processObject(obj) {
      // 매번 프로퍼티 순서가 달라 히든 클래스가 계속 바뀜
      if (Math.random() > 0.5) {
        obj.a = 1;
        obj.b = 2;
      } else {
        obj.b = 2;
        obj.a = 1;
      }
    }
    
    // JIT 친화적인 코드
    function createObject(a, b) {
      // 항상 같은 순서로 프로퍼티를 초기화
      const obj = {};
      obj.a = a;
      obj.b = b;
      return obj;
    }
    
  • 함수 인자의 타입 일관성: 함수를 호출할 때 항상 동일한 타입의 인자를 전달하면 V8이 해당 함수를 더 공격적으로 최적화할 수 있다.

4. 심화 내용: JIT의 다른 얼굴들과 미래

JIT는 하나의 고정된 기술이 아니라 계속해서 발전하는 분야다. 역사적으로 여러 형태의 JIT가 있었고, 현재는 AOT와 결합하는 하이브리드 방식이 주목받고 있다.

JIT의 종류

  • 메서드 JIT (Method JIT): 가장 일반적인 형태로, 메서드(함수) 전체를 컴파일 단위로 삼는다. 현대의 JVM, V8이 이 방식을 기반으로 한다.

  • 트레이싱 JIT (Tracing JIT): 메서드가 아닌, 실제로 실행되는 코드 경로, 특히 반복문의 경로(‘트레이스’)를 컴파일 단위로 삼는다. 동적인 코드에 강점이 있어 과거 파이어폭스의 자바스크립트 엔진 등에서 사용되었으나, 현재는 메서드 JIT 방식이 주류를 이루고 있다.

JIT vs AOT: 끝나지 않은 경쟁과 협력

항목JIT (Just-In-Time)AOT (Ahead-of-Time)
컴파일 시점프로그램 실행 중 (Runtime)프로그램 실행 전 (Build time)
최대 성능런타임 정보를 활용한 최적화로 더 높은 성능에 도달할 잠재력 있음런타임 정보가 없어 한계가 있지만, 초기 성능은 빠름
시작 시간인터프리터 실행, 프로파일링, 컴파일 등으로 초기에는 느림 (워밍업 필요)이미 컴파일된 코드를 바로 로드하므로 시작이 매우 빠름
메모리 사용량프로파일링 정보, 여러 계층의 컴파일된 코드 등으로 더 많은 메모리 사용필요한 코드만 담고 있으므로 상대적으로 메모리 사용량이 적음
플랫폼 독립성높음 (바이트코드 등 중간 언어 기반)낮음 (특정 OS와 CPU 아키텍처에 종속)
대표 주자Java (JVM), JavaScript (V8), C# (.NET)C, C++, Go, Rust, Swift

미래: 하이브리드 방식의 부상

최근에는 JIT와 AOT의 장점을 모두 취하려는 하이브리드 접근법이 각광받고 있다.

  • Android ART (Android Runtime): 초기에는 AOT 방식으로 앱 설치 시 코드를 컴파일했지만, 현재는 앱 사용 패턴을 프로파일링하여 자주 쓰는 코드만 AOT로 컴파일하고 나머지는 JIT으로 처리하는 하이브리드 방식을 사용한다.

  • GraalVM: 자바와 다른 언어들을 위한 고성능 런타임이다. JIT 컴파일러로도 작동하지만, ‘네이티브 이미지(Native Image)’ 기능을 통해 자바 애플리케이션을 AOT 방식으로 컴파일하여 가벼운 메모리 사용과 빠른 시작 속도를 가진 단일 실행 파일로 만들 수 있다. 이는 서버리스나 클라우드 환경에서 큰 장점을 가진다.

결론: 현대 언어의 숨겨진 엔진

JIT 컴파일러는 단순한 기술을 넘어, 현대 프로그래밍 언어 생태계를 지탱하는 핵심 철학이다. “미리 모든 것을 결정하지 말고, 실제 데이터를 기반으로 최적의 결정을 내린다”는 JIT의 접근 방식은 빠르고 변화무쌍한 소프트웨어 개발 환경에 가장 적합한 해답 중 하나다.

개발자로서 우리는 JIT의 존재를 항상 의식할 필요는 없지만, 그 보이지 않는 손이 어떻게 우리의 코드를 더 빠르게 만들기 위해 노력하는지 이해하는 것은 중요하다. JIT의 원리를 이해하면 우리는 단순히 ‘동작하는’ 코드를 넘어, ‘잘 동작하는’ 코드를 작성하는 지혜를 얻을 수 있을 것이다. JIT는 지금 이 순간에도 우리가 사용하는 수많은 애플리케이션의 뒤에서, 가장 효율적인 경로를 찾아내며 묵묵히 자신의 일을 수행하고 있다.