10년의 결실, JDK 28에 도착하는 프로젝트 발할라
자바 생태계의 가장 중요한 변화 중 하나인 '프로젝트 발할라(Project Valhalla)'가 드디어 JDK 28에 프리뷰 기능으로 통합될 예정입니다. 이는 객체 지향의 편리함을 유지하면서도 기본형(primitive) 수준의 메모리 효율과 성능을 내기 위한 자바의 장기적인 노력의 결실입니다. 개발자들은 기존 코드의 호환성을 깨지 않고도 메모리와 CPU 캐시를 훨씬 더 효율적으로 활용하는 최적화된 코드를 작성할 수 있게 됩니다.
원문 제목: 10년의 노력이 JDK 28에 도착하다, 프로젝트 발할라 설명 출처: JVM Weekly vol. 180
새로운 JVM Weekly가 돌아왔습니다... 마침내 JDK에 발할라(Valhalla)가 포함되면서, 사실상 자바 세계의 라그나로크(종말, 즉 대변혁)가 찾아온 듯합니다. 하지만 상황은 조금 복잡합니다. Artur Skowronski, 2026년 6월 18일
6월 15일, 오라클 엔지니어 Lois Foltan은 많은 업계 관계자들이 이제는 이루어지지 않을 거라 믿었던 것을 확정 지었습니다. 바로 'JEP 401: 값 클래스와 객체(Value Classes and Objects)'가 메인 OpenJDK 저장소에 통합되며, JDK 28을 목표로 하고 있다는 소식입니다.
이번 변경 사항은 매우 방대하여, 통합 작업이 진행되는 동안 나머지 커미터들에게는 큰 규모의 커밋을 보류해 달라고 요청했습니다. 이 풀 리퀘스트(PR) 하나만 해도 무려 1,816개의 파일에 걸쳐 약 19만 7천 줄의 코드가 추가되었습니다.
하지만 샴페인을 터뜨리기 전에 알아둘 것이 있습니다. 이것은 기본적으로 비활성화된 '프리뷰(preview)' 기능이며, Brian Goetz가 사람들의 기대를 신속히 식혔듯이 이는 '발할라의 첫 번째 부분'일 뿐이라는 점입니다. Goetz는 날카로운 관찰을 덧붙였습니다. '그들은 절대 출시하지 못할 거야'라고 말하던 무리가 이제 곧바로 '하지만 가장 중요한 부분은 출시하지 않았잖아'라고 부드럽게 화제를 전환할 것이라는 점입니다. (사실 수년간 커뮤니티에는 프로젝트가 출시되기도 전에 우리가 먼저 북유럽 신화의 사후세계인 발할라에 가게 될 거라는 농담이 돌아다니곤 했습니다.) 아무튼 비판자를 만들려면 그만한 일을 해낸 증거이기도 합니다.
그래서 지금이 전체적인 이야기를 전달하기에 완벽한 시점입니다. 이번 호는 여러분이 발할라에 관한 작업을 지금까지 전혀 따라가지 않았다는 가정하에 쓰인 거대한 심층 분석 기사입니다. 2014년의 문제 인식부터 시작해, 아이디어의 발전 과정(상당수는 결국 폐기되었습니다), 그리고 JDK 28에서 우리가 정확히 무엇을 손에 쥐게 될 것인지까지 다룹니다. 커피를 한 잔 타세요. 저는 이 칼럼을 오랫동안 갈무리해 두었다가 바로 이 기회를 위해 꺼내든 것입니다.
- 서론 - 대체 이게 무슨 일인가?
발할라가 처음부터 품고 있는 슬로건은 이렇습니다. "클래스처럼 코딩하고, int처럼 동작한다."
이 한 문장이 프로젝트의 핵심 목적을 완벽히 요약합니다. 우리는 메서드와 생성자의 유효성 검사, 그리고 합리적인 필드 이름을 갖춘 일반적이고 가독성 좋은 클래스를 작성하길 원하지만, 동시에 JVM이 이를 기본형(primitive)만큼이나 효율적으로 다뤄주길 바라는 것입니다.
왜 이것이 문제가 되는지 이해하려면 자바의 근간으로 돌아가야 합니다. 이 언어에서는 8개의 기본형(int, long, double, boolean 등)을 제외한 모든 것은 '참조형(reference type)'입니다. Point p = new Point(1, 2)라고 작성할 때, 변수 p는 Point가 아닙니다. 변수 p는 포인터, 즉 코트를 맡길 때 받는 번호표 같은 것입니다. 힙(Heap) 어딘가에 객체가 존재하고, 당신은 그 주소가 적힌 종이 쪽지를 들고 있는 셈입니다. 필드를 읽을 때마다 JVM은 이 '코트 보관실'로 가야 하며, 포인터를 통한 우회 참조(pointer indirection)를 수행해야 합니다.
객체 하나만 볼 때는 아무 문제가 아닙니다. 하지만 규모가 커지면 문제가 시작됩니다. 힙에 있는 모든 객체는 각자의 헤더를 갖습니다. (10바이트 남짓의 메타데이터로, JVM이 이 객체의 타입이 무엇인지, 그리고 누군가 이 객체에 동기화를 시도하고 있는지 등을 파악하게 해줍니다.) 덧붙여, 이것은 최근 '프로젝트 릴리푸트(Project Lilliput)'가 객체 헤더 크기를 줄여 해결하려고 했던 바로 그 문제이기도 합니다.
하지만 헤더 크기가 전부는 아닙니다. 모든 객체는 메모리에 할당(allocate)되어야 하고, 결국 가비지 컬렉션(GC)의 대상이 되어야 합니다. 그리고 객체들이 힙 전체에 흩어져 있기 때문에, 백만 개의 Point 객체를 담는 배열은 실제로 거대한 창고 전체에 흩어져 있는 백만 개의 상자를 가리키는 백만 장의 종이 번호표와 다를 바 없습니다.
Brian Goetz는 그의 'State of Valhalla' 문서에서 이러한 메모리 배치 방식을 '푹신한(fluffy)', 즉 부풀어 오르고 비대하다고 불렀습니다. 우리가 꿈꾸는 것은 데이터가 나란히 놓인 조밀한(dense) 메모리 구조입니다.
왜 조밀함이 중요할까요? 하드웨어가 자바보다 더 빠르게 변화했기 때문입니다. 1995년만 해도 메모리 접근 비용은 CPU 연산 비용과 거의 비슷했습니다. 하지만 오늘날 CPU는 주 메모리보다 두 자릿수(약 100배) 더 빠르며, 이 격차는 전적으로 캐시(cache)로 메워집니다. 프로세서는 '캐시 라인(보통 64바이트)'이라고 불리는 덩어리 단위로 메모리를 읽습니다. 만약 데이터가 조밀하고 질서 있게 놓여 있다면, 이런 하나의 덩어리가 읽힐 때 유용한 수많은 값들을 한 번에 가져오게 됩니다. 하지만 포인터를 따라 여기저기 뛰어다녀야 한다면, 메모리에 접근할 때마다 캐시 미스(cache miss)의 위험을 감수해야 하는 것과 같습니다.