TECH 으로 돌아가기
TECH HACKER NEWS 오늘 9분 읽기 32 READS

JEP 544: 자바 애플리케이션을 미리 컴파일해 두는 AOT 캐시의 완성

자바 애플리케이션이 HotSpot JVM 위에서 실행될 때는 세 단계를 거친다. 먼저 main 메서드가 호출되고 필요한 클래스가 그때그때 로드·링크·초기화되는 시작(startup) 단계, 워크로드가 자리를 잡으며 클래스 로딩이 잦아드는 워밍업(warmup) 단계, 그리고 모든 핫 메서드가 최적화된 네이티브 코드로 컴파일되어 컴파일러가 유휴 상태가 되는 최고 성능(peak performance) 단계다. 문제는 이 최고 성능에 도달하기까지 걸리는 시간이다. 초기에는 느린 바이트코드 인터프리터로 실행하면서 메서드 호출과 루프 반복 횟수를 세어 프로파일을 수집하고, 자주 불리는 지점을 C1 컴파일러로 가볍게 컴파일한 뒤, 프로파일이 충분히 쌓이면 가장 뜨거운 메서드를 C2 컴파일러로 고도로 최적화한다. JEP 544는 이 과정에서 발생하는 시작·워밍업 지연을 줄이기 위해, 최적화된 네이티브 코드를 훈련 실행 단계에서 미리 만들어 두는 방식을 제안한다.

프로젝트 레이든의 마지막 조각

JEP 544는 갑자기 등장한 아이디어가 아니라 프로젝트 레이든(Project Leyden)이 단계적으로 쌓아 온 결과물의 마지막 조각이다. 레이든의 핵심 명제는 단순하다. 시작과 워밍업을 개선하는 열쇠는 JVM이 실행 중에 한꺼번에 처리하던 작업(코드 실행, 클래스 로딩·링킹·초기화, 프로파일링, 컴파일)의 일부를 실행 이전으로 앞당기는 데 있다는 것이다. 그 수단이 바로 훈련 실행에서 결과를 만들어 AOT 캐시에 저장하고, 이후 프로덕션 실행에서 즉시 재사용하는 방식이다. JDK 24의 JEP 483은 클래스 로딩·링킹 결과를 캐시에 저장해 시작 시간을 줄였고, JDK 25의 JEP 515는 메서드 실행 프로파일을 저장해 C2 컴파일러가 프로덕션 실행 시작 시점부터 곧바로 동작하도록 만들어 워밍업을 개선했다. JEP 544는 여기서 한 걸음 더 나아가 최적화된 네이티브 코드 자체를 캐시에 담는다.

정적 컴파일과 무엇이 다른가

자바를 통째로 미리 네이티브 코드로 바꾸자는 정적 컴파일 논의는 오래전부터 있었다. 정적 컴파일된 애플리케이션은 워밍업 없이 곧바로 최고 성능에서 시작하고 실행 중 인터프리터·프로파일링·컴파일이 필요 없다는 장점이 있다. 그러나 동적 컴파일에는 정적 컴파일이 흉내 내기 어려운 세 가지 강점이 있다. 첫째는 민첩성이다. 워크로드가 바뀌어 핫스팟이 달라지면 HotSpot은 기존 네이티브 코드를 폐기(deoptimize)하고 새로 뜨거워진 메서드를 다시 컴파일(reoptimize)해 성능을 유지한다. 정적 컴파일은 본질적으로 단 하나의 핫스팟 집합에만 최적화될 수 있다. 둘째는 이식성이다. 동적 컴파일은 실행 시점의 환경에 맞는 코드를 생성하므로 다른 프로세서, 다른 OS, 다른 JDK 버전으로 옮겨도 애플리케이션을 고칠 필요가 없다. 셋째는 동적 클래스 로딩, 동적 링키지, 동적 디스패치, 리플렉션 같은 자바 플랫폼 고유의 동적 특성과의 호환성이다. 정적 컴파일러는 이런 기능 앞에서 폐쇄 세계 가정(closed-world assumption)이나 리플렉션 대상 클래스를 미리 지정하게 하는 등 개발자에게 부담을 지우는 제약을 강요해 왔다.

JEP 544가 노리는 지점이 바로 여기다. AOT로 미리 컴파일한 코드를 쓰되, 정적 컴파일이 되지는 않는다. 목표에서 명시적으로 밝히듯 AOT 전용 모드는 제공하지 않는다. 한 번의 실행 안에서 AOT 코드와 JIT 코드가 함께 쓰이며 필요에 따라 자동으로 전환된다. AOT 코드와 JIT 코드는 모두 C1·C2라는 같은 컴파일러가 만들기 때문에 완전히 상호운용되며 공존할 수 있다. 프로덕션 실행 중 어떤 메서드의 최적화 코드가 요청되었을 때 캐시에 맞는 AOT 코드가 있으면 즉시 제공되고, 없거나 호환되지 않거나 나중에 폐기되면 기존 인터프리터와 JIT 메커니즘으로 자연스럽게 되돌아간다.

워크플로와 실무적 의미

실무자 관점에서 반가운 점은 도입 부담이 거의 없다는 것이다. 애플리케이션, 라이브러리, 프레임워크의 코드를 바꿀 필요가 없고, AOT 캐시 사용을 요청하는 것 외에 HotSpot 설정을 손댈 필요도 없다. 새로운 AOT 워크플로를 도입하는 대신 기존 캐시 생성 워크플로를 확장했을 뿐이다. AOTCacheOutput 옵션으로 훈련 실행을 해서 app.aot 같은 캐시 파일을 만드는 절차는 이전 릴리스와 동일하다. 달라진 것은 그 파일이 이제 사전 링크된 클래스와 프로파일 데이터뿐 아니라 선별된 핫 메서드의 AOT 코드까지 담는다는 점이다. 프로덕션에서는 그 캐시를 지정해 실행하기만 하면 되고, AOT 코드 생성·사용에 별도 옵션은 필요 없다. HotSpot이 기본적으로 AOT 코드를 만들어 캐시에 저장한다. Serial, Parallel, G1, ZGC 가비지 컬렉터도 계속 지원한다.

성능 측정 결과도 제시됐다. JIT 컴파일러가 애플리케이션과 CPU를 다투는 마이크로서비스 환경을 모사하기 위해 2코어 리눅스/x64 시스템에서 인기 프레임워크로 만든 다섯 개 벤치마크를 돌렸을 때, AOT 코드 없이 캐시만 써도 시작 시간이 약 50~70% 줄었고 AOT 코드를 더하면 약 65~80% 단축됐다. 같은 소스 50개 파일을 스무 번 반복 컴파일하는 javac 벤치마크에서는 첫 반복(시작 시간)이 AOT 코드 없는 캐시로 약 30%, AOT 코드를 더해 총 75%가량 개선됐고, AOT 코드를 담은 곡선은 네 번째 반복 만에 정상 상태에 근접해 워밍업 구간까지 눈에 띄게 줄었다.

알아둘 한계와 전제

다만 몇 가지 전제는 분명히 이해하고 써야 한다. 크로스 컴파일은 목표가 아니다. 훈련 실행에서 컴파일된 코드는 동일한 CPU 아키텍처에서, 동일한 기능 집합을 갖춘 환경에서 실행되어야 한다. 현재 지원 대상은 AArch64와 x64 두 아키텍처이며, HotSpot이 지원하는 모든 아키텍처를 다루는 것은 이 단계의 목표가 아니고 이후 정상적인 포팅 작업으로 확대될 것으로 본다. 또한 훈련 실행과 프로덕션 실행이 다를 수 있으므로 AOT 코드와 JIT 코드가 달라지는 경우도 생긴다. 대표적인 예가 클래스 초기화 순서 차이다. 다른 클래스의 정적 필드에 접근하거나 정적 메서드를 호출하는 메서드는 해당 클래스가 초기화되었는지 보장해야 하는데, 이 순서는 워크로드가 다르면 훈련 실행과 프로덕션 실행에서 달라질 수 있다. 이를 위해 C2로 AOT 코드를 만들 때 HotSpot은 두 버전을 컴파일한다. 참조 클래스 초기화를 보장하는 코드를 넣은 느린 버전과, 그 코드가 없어 더 잘 최적화된 빠른 버전이다. 실행 초기에는 느린 버전을 쓰다가 참조 클래스가 모두 초기화되면 빠른 버전으로 전환한다. AOT에서 JIT로 넘어가는 이 전환 전체가 애플리케이션에게는 보이지 않도록 설계된다는 점이, 정적 컴파일의 이점 일부를 가져오면서도 동적 컴파일의 민첩성·이식성·호환성을 지키려는 JEP 544의 기본 태도를 잘 보여준다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://openjdk.org/jeps/544
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...