Project Valhalla: 10년간의 작업이 JDK 28에 어떻게 반영되었는가
(jvm-weekly.com)- JEP 401: Value Classes and Objects이 실제 JDK preview로 들어가는 단계에 도달
- 핵심 목표는 Java 객체를 “클래스처럼 코딩하고 int처럼 동작”하게 만들어 객체 헤더·힙 할당·GC·포인터 간접 참조 비용을 줄이는 것
- JDK 28의 value class는 아직 null 가능한 참조 타입이며, non-null 타입·전문화 제네릭·128비트 인코딩은 포함되지 않고
--enable-preview가 필요함 - JVM은 value object를 스칼라화하거나 필드·배열에 힙 평탄화할 수 있지만, erased generic이나
Object같은 상위 타입에서는 힙 객체로 materialize될 수 있음 - Java 개발자는 identity와 value의 차이를 코드 설계에 반영해야 하며,
==,synchronized, primitive wrapper, 배열 성능, 향후 제네릭 전문화까지 영향이 이어짐
JDK 28에 들어오는 Valhalla의 범위
- 6월 15일 Oracle 엔지니어 Lois Foltan이 JEP 401: Value Classes and Objects의 OpenJDK 메인 저장소 통합과 JDK 28 타깃을 확인함
- 관련 pull request는 1,816개 파일에 걸쳐 19만 7천 줄 이상을 추가함
- 변경 규모가 커서 통합 중 다른 커미터들에게 큰 커밋을 잠시 보류해 달라는 요청이 있었음
- JEP 401은 기본 비활성화된 preview 기능임
- 문법을 사용하려면
--enable-preview가 필요함 - Brian Goetz는 이를 “Valhalla의 첫 번째 부분”이라고 선을 그음
- 문법을 사용하려면
- JDK 28은 2027년 3월 릴리스 예정이며, mainline 통합은 2026년 7월쯤으로 계획됨
Valhalla가 겨냥한 Java 객체 모델의 비용
- Valhalla의 표어는 “codes like a class, works like an int”임
- 메서드, 생성자 검증, 의미 있는 필드 이름을 가진 일반 클래스를 쓰면서도 JVM이 primitive처럼 효율적으로 다룰 수 있게 하는 것이 목표임
- Java에서는 8개 primitive를 제외하면 거의 모든 것이 참조 타입임
Point p = new Point(1, 2)에서p는 point 자체가 아니라 힙 객체를 가리키는 포인터임- 필드를 읽을 때마다 JVM은 포인터를 따라가야 함
- 객체 수가 늘어나면 비용이 급격히 커짐
- 각 객체에는 타입, 동기화 상태 등을 위한 객체 헤더가 있음
- 객체는 힙에 할당되고 이후 GC 대상이 됨
Point백만 개 배열은 실제로 백만 개 포인터와 힙 곳곳의 백만 개 객체로 구성됨
- Brian Goetz의 “State of Valhalla”는 이런 메모리 배치를 fluffy라고 부름
- Valhalla가 원하는 것은 데이터가 나란히 놓이는 dense한 배치임
하드웨어 격차와 escape analysis의 한계
- 밀도 높은 메모리 배치가 중요한 이유는 CPU와 메모리 속도 격차 때문임
- 1995년에는 메모리 접근 비용이 CPU 연산과 비슷했음
- 현재 CPU는 메인 메모리보다 두 자릿수 배 빠르며, 이 차이를 캐시가 메움
- CPU는 보통 64바이트 cache line 단위로 메모리를 읽음
- 데이터가 조밀하고 순서대로 있으면 한 번에 유용한 값을 많이 가져옴
- 포인터를 따라 흩어진 객체에 접근하면 cache miss가 발생할 수 있고, hit보다 훨씬 느릴 수 있음
- JVM의 escape analysis는 일부 객체 할당을 제거할 수 있음
- 객체가 로컬 코드 조각 밖으로 “escape”하지 않는다고 판단되면 힙에 할당하지 않고 필드를 변수나 레지스터로 펼칠 수 있음
- 다만 escape analysis는 예측 가능성이 낮고 취약함
- 객체가 다른 클래스의 필드에 들어가거나 배열에 저장되거나 복잡한 메서드로 전달되거나 JIT가 분석할 수 없는 경계를 넘으면 최적화가 멈출 수 있음
- 작은 리팩터링, JDK 업데이트, 코드 구조 변경만으로 객체가 다시 힙에 올라갈 수 있음
- 성능을 위해 객체를 포기하고
r,g,b같은 raw byte로 직접 인코딩하면 속도는 얻을 수 있지만 안전성·가독성·검증·메서드를 잃음
2014년 시작과 Q World에서 L World로의 전환
- Project Valhalla는 공식적으로 2014년에 시작됨
- James Gosling은 당시 이를 “six PhDs tied into a single knot”라고 표현함
- Java 창시자들은 Java 1.0 시절부터 value type을 원했지만, 1995년에는 문제가 너무 어려워 포기했음
- 초기 목표는 프로그래밍 모델과 현대 하드웨어 성능 특성의 정렬을 회복하는 것이었음
- 사용자가 직접 primitive처럼 flat하고 dense한 타입을 선언하되, 일반 클래스처럼 보이고 동작하게 만드는 방향임
- 초기 prototype은 Q World 방향이었음
- 새 value type을 객체와 근본적으로 다른 존재로 보고, 별도 type descriptor, bytecode, top type을 두는 방식임
- JVM 타입 시스템 전체가 두 가지 변형을 가져야 해 복잡성이 커졌음
- 2019년쯤 등장한 L World가 전환점이 됨
- value type이 일반 reference와 같은 “L carrier”를 공유함
- 팀은 이 통합이 어렵다고 예상했지만, 큰 타협 없이 동작했고 이전 prototype의 여러 문제를 해결함
- L World에서 중요한 분리가 생김
- JVM 모델과 언어 모델이 100% 겹칠 필요는 없음
- JVM에는 L World 모델을 두고, 프로그래머에게는 더 편한 언어 모델을 제공할 수 있음
- 이후 작업은 value class와 전문화 제네릭의 두 단계로 나뉨
이름과 모델의 변화
- Valhalla 용어는 여러 번 바뀌었고, 단순한 명칭 변경이 아니라 모델 변경을 반영함
- 초기 용어는 value types였음
- 당시에는 이 타입들이 정확히 어떤 존재인지 아직 명확하지 않았음
- 2019~2020년쯤 inline classes 모델이 자리 잡음
- 기존 클래스는 identity classes로, 새 클래스는 identity가 없는 inline classes로 구분됨
- inline class는 기본적으로 final이고, 필드는 final이며, 동기화할 수 없는 제약이 잡힘
- 2021년 “State of Valhalla”는 primitive classes와 두 projection 모델을 다룸
- 하나의 타입이 flat하고 null이 불가능한 value variant와 null을 허용하는 reference variant를 갖는 구상이었음
Point.val/Point.ref, 이후Point!/Point?같은 문법도 실험됨
- 이 모델은 강력했지만 인지 부담이 컸음
- 프로그래머가 같은 타입의 두 형태와 변환 시점을 일상적으로 이해해야 했음
- 결국 사용자 모델을 단순화하기 위해 dualism이 축소됨
- 현재 JEP 401은 value class와 value object를 사용함
valuemodifier로 value class를 선언함- 인스턴스는 identity가 없는 value object임
- value class는 여전히 reference type임
- non-nullability는 별도 optional JEP인 Null-Restricted Value Class Types로 분리됨
- JDK 28에는 포함되지 않음
- 이전 “primitive classes” 모델을 설명하는 오래된 글은 현재 OpenJDK 기준과 다를 수 있음
- JEP 401에는 preview인 JEP 402: Enhanced Primitive Boxing도 함께 있음
- primitive와 wrapper 사이 변환을 더 부드럽게 만드는 방향임
- 완성된 형태로 JEP 401과 함께 모두 들어온다고 가정하면 안 됨
JDK 28의 value class 모델
- value class는
valuemodifier로 선언함
value class USDCurrency implements Comparable<USDCurrency> {
private int cents; // implicitly final
public USDCurrency(int dollars, int cents) {
this.cents = dollars * 100 + cents;
}
public USDCurrency plus(USDCurrency that) {
return new USDCurrency(0, this.cents + that.cents);
}
// dollars(), cents(), compareTo(), toString()...
}
- value record도 가능함
- 주요 규칙은 다음과 같음
- 모든 instance field는 암묵적으로 final임
- method는
synchronized일 수 없음 - 클래스는 기본적으로 final임
- value class와 abstract value class로 구성된 계층은 가능함
- identity가 있는 클래스를 상속할 수 없음
- interface 구현은 가능함
- 핵심 특성은 identity가 없음임
- 일반 객체는 같은 내용을 가져도
new Point(1, 2)로 두 번 만들면 서로 다른 객체임 - value object는
int값 4에 “서로 다른 두 개의 4”가 없는 것처럼 identity가 없음
- 일반 객체는 같은 내용을 가져도
==, synchronized, null의 변화
- value object에서
==는 identity 비교가 아니라 substitutability 검사가 됨- 같은 클래스이고 같은 필드 값을 가지는지 재귀적으로 비교함
- primitive field는 bit 단위로 비교하고, object field는 다시
==로 비교함 new USDCurrency(3,95) == new USDCurrency(3,95)는 true가 됨
- 다만
==는 내부 상태를 보기 때문에, “같은 데이터를 표현하는가”에는 보통equals가 더 적합함 - value object에는 동기화할 identity가 없음
- 동기화를 시도하면 IdentityException이 발생함
- identity를 강제 확인해야 할 때는
Objects.requireIdentity와Objects.hasIdentity를 사용할 수 있음
- JDK 28의 value class는 여전히 null 가능함
USDCurrency d = null;은 합법임- null을 금지하는 타입은 별도 future JEP이며 JDK 28에는 없음
- non-nullability는 단순 문법 문제가 아니라 더 큰 value class의 평탄화를 여는 성능 레버가 됨
스칼라화와 힙 평탄화
- JEP 401은 JVM이 value object를 최적화할 수 있는 자유를 줌
- 스칼라화(scalarization) 는 value object reference를 필드 집합으로 분해하는 JIT 기법임
Color포인터를 전달하는 대신r,g,bbyte와 null 여부 flag를 전달할 수 있음- 할당과 GC 비용이 사라질 수 있음
- escape analysis와 비슷하지만 더 예측 가능하고, inline되지 않은 method call 경계를 넘어 적용될 수 있음
- 스칼라화에는 제한이 있음
- 변수 타입이 value class의 supertype인
Object이거나 erased generic parameter이면 보통 동작하지 않음 - 이 경우 객체가 힙에 materialize되어야 함
- 변수 타입이 value class의 supertype인
- 힙 평탄화(heap flattening) 는 value object의 필드 값을 compact bit vector로 인코딩해 필드나 배열 셀에 직접 쓰는 방식임
- 다른 힙 위치를 가리키는 포인터가 필요 없음
- 데이터 밀도와 locality가 생김
- 평탄화된 데이터는 concurrent access에서 tearing을 피하기 위해 atomic하게 읽고 쓸 수 있어야 함
- 일반 플랫폼에서 “충분히 작은” 크기는 null flag를 포함해 64비트 수준일 수 있음
- 작은 value class는 잘 평탄화될 수 있지만, 두 개의
int필드나 하나의double만 있어도 atomic write 크기에 맞지 않아 일반 힙 객체가 될 수 있음
- 향후에는 128비트 인코딩과 null-restricted type이 더 큰 value class의 평탄화를 가능하게 할 수 있음
Boxing, wrapper, 배열에서의 효과
- preview가 켜지면
Integer,Long,Double같은 primitive wrapper class 자체가 value class가 됨- box가 identity를 잃으므로 JVM이 스칼라화하고 평탄화할 수 있음
Integer[]는int[]의 효율에 가까워지고, boxing overhead가 크게 줄어드는 방향임
- JEP 402: Enhanced Primitive Boxing은 primitive와 box 사이 변환을 더 확장함
List<int>같은 표현으로 가는 길을 열지만, 아직 별도의 성숙 중인 작업임
- 배열에서 효과가 가장 잘 드러남
- 기존
Color[]는 백만 개 포인터와 힙에 흩어진 백만 개 객체가 될 수 있음 - value class
Color[]는 연속된 색상 값을 직접 저장하는 contiguous block이 될 수 있음 - CPU는 cache line 단위로 여러 값을 순차적으로 읽을 수 있음
- 기존
Point[] 예시로 보는 전후 차이
- Valhalla 이전의 일반 class 예시는 다음과 같음
final class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
- 이 배열은 백만 개의 포인터를 담음
- 각 포인터는 힙 어딘가의 별도
Point객체를 가리킴 - 각 객체에는 두
int외에도 객체 헤더가 있음 - 순회할 때 포인터를 읽고, 해당 주소로 점프하고, 필드를 읽어야 함
- 각 포인터는 힙 어딘가의 별도
- Valhalla 이후 value class 예시는 다음과 같음
value class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
- 코드 차이는
value한 단어지만, 메모리 배치는 달라짐- JVM은 각 point의 값을 배열 안에 조밀하게 저장할 수 있음
- element마다 헤더가 없고 포인터도 없음
x,y두int기준 8바이트와 가능한 null flag로 연속 배치될 수 있음
- 유지보수성도 유지됨
Point는 여전히 이름, 생성자, 검증, method를 가진 class임int[] xs,int[] ys로 쪼개고 index를 맞추는 방식을 피할 수 있음
전문화 제네릭이 아직 남은 이유
- Java generics는 type erasure로 구현됨
List<String>과List<Integer>는 runtime에서 같은List임- type parameter
T는Object로 erasure됨
- erasure는 Java의 기존 코드베이스를 깨지 않고 generics를 도입하기 위한 의도적 선택이었음
- non-generic class를 generic으로 바꿔도 기존 source file과 compiled class를 깨지 않게 함
- Valhalla와 erasure는 성능상 충돌함
List<Point>에 value object를 넣으면T가Object로 erasure되므로 객체가 힙에 materialize되어야 함Point[]에서 얻은 평탄화 이점이ArrayList<Point>에서는 사라질 수 있음
- 복구 계획은 두 단계임
- Universal Generics: 언어 수준에서 type variable이 value type까지 다룰 수 있게 함
- 여전히 erasure를 사용함
T필드가 기본적으로 null에서 시작하는 문제 때문에 “null pollution” compiler warning이 생길 수 있음- 경고를 해결하면 API가 specialization-ready에 가까워짐
- Specialized Generics: JVM 수준에서 concrete type argument별 전문화된 class layout을 생성함
- 프로젝트 용어로 species와 type restriction이 관련됨
- 이 단계에서야
ArrayList<Point>가 실제 flat memory를 쓸 수 있음
- Universal Generics: 언어 수준에서 type variable이 value type까지 다룰 수 있게 함
- JDK 28에는 full specialized generics가 없음
- 컬렉션, stream, API가 value type 위에서 flat하고 allocation-free가 되는 것은 future release의 작업임
JDK 28에 있는 것과 없는 것
- JDK 28에 들어오는 것은 다음과 같음
value class와value record선언- JDK의 기존 value-based class 중 primitive wrapper 같은 클래스의 value class migration
- 조건을 만족하는 클래스의 스칼라화와 평탄화
- 더 저렴한 boxing
- JDK 28에 없는 것은 다음과 같음
- null-restricted types
- full specialized generics
- 128비트 인코딩
- 완전히 성숙한 JEP 402
- preview feature이므로 syntax와 동작은 release마다 feedback에 따라 바뀔 수 있음
- JDK 28은 LTS가 아님
- 다음 LTS는 2027년 9월의 JDK 29일 가능성이 큼
- 많은 회사는 안정화된 Valhalla를 LTS에서 만날 수 있지만, JDK 28 preview가 실제 코드 feedback loop를 시작함
생태계와 코드에 생길 변화
- 고성능 Java 영역에서 Valhalla는 abstraction을 포기하지 않고 dense data를 다루는 경로가 됨
- 데이터 처리, vector computation, ML, game development, finance, codec 같은 영역이 해당됨
- framework와 library는 value-based class migration을 시작할 수 있음
- identity에 의존하던 코드는 동작 차이를 겪을 수 있음
==가 value object에서 address 비교가 아니라 substitutability 비교가 됨synchronized는 value object에서IdentityException으로 이어짐
Integer가 value class가 되어도 대부분의 경우 binary는 계속 link됨- 새 compilation error는 이런 타입에 동기화하려는 경우임
Integeridentity에 의존한==나synchronized(someInteger)는 영향을 받을 수 있음
- early-access build는 jdk.java.net/valhalla에서 사용할 수 있음
자주 나오는 질문 정리
- value class는 record와 다름
record는 content가 component라는 선택임value는 identity를 포기하는 선택임- 일반 class, record, value class, value record 조합이 모두 가능함
- value object는
==로 비교할 수 있음- 의미는 address 비교가 아니라 substitutability임
- 표현하는 데이터의 동등성에는 보통
equals가 더 적합함
- JDK 28 value class는 null이 가능함
- non-nullable type은 future JEP임
- 더 큰 value class의 평탄화에도 중요함
- 빠른 flat
ArrayList<Point>는 아직 아님- type erasure 때문에 generic collection 안의 객체는 힙에 materialize됨
- JDK 28에서 평탄화가 직접 작동하는 대표 사례는 value type의 field와 array인
Point[]임
- escape analysis가 전부를 대신하지는 못함
- 객체가 field, array, 분석 경계 밖으로 나가면 최적화가 깨질 수 있음
- value object의 스칼라화는 더 예측 가능하고 method-call boundary를 더 멀리 넘을 수 있음
- 전체 Valhalla는 여러 release에 걸쳐 확장됨
- JDK 28은 value class의 첫 preview임
- specialized generics, null-restricted types, 128비트 인코딩은 future release에 걸친 작업임
댓글과 토론
Hacker News 의견들
-
메모리 차이는 근본적인 부분이라고 했지만, 글이 제대로 검수된 건지 의문임
방금 전까지 64비트 초과 표현을 가진 객체는 힙 평탄화가 안 된다고 설명하지 않았나? 예시의Point는 32비트 정수 2개에 널 플래그까지 있으니 최소 65비트임
“널 플래그가 있을 수도 있음”이라는 표현과 뒤따르는 짧은 강조 문장들을 보면, AI가 강조문을 만들다가 딴길로 샌 느낌이고, 중간의"[IMAGE: the same Point[] array in two variants..."블록도 안타깝다- “원소별 헤더 없음. 포인터 없음. 힙을 여기저기 뛰어다니지 않음.” 같은 문장은 AI 문체 냄새가 나고, 그래서 게으른 글쓰기처럼 보임
AI로 글쓰기를 보조하는 건 좋지만, 자기 목소리를 넣지 않으면 읽을 이유가 없음
https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing#... - 주제가 정말 흥미로워 보여서 읽고 싶었고, AI 생성 이미지까지는 봐줄 수 있었음
그런데 몇 문단 지나니 LLM을 거쳤거나 그보다 더 나쁜 방식으로 만들어진 글이라는 게 분명해짐
기술 블로그든 뭐든, 제발 AI가 대신 쓰게 하지 않았으면 함. 아무도 그런 글을 읽고 싶어 하지 않음 - 가능한 값이 18446744073709551616개나 있는데 그중 1개를 null에 할애할 수 없다는 건가? :)
오늘 Rust에는NonZeroU64가 있고,Optional과 조합하면 항목당 64비트만으로 필요한 동작을 얻을 수 있다는 걸 알게 됨
https://doc.rust-lang.org/std/num/type.NonZeroU64.html - AI를 너무 많이 쓴 게 뻔해서 2문단 읽고 그만둠
- “64비트 초과 표현을 가진 객체는 힙 평탄화가 안 된다”는 건 초기 커밋에서의 얘기임
JEP에도 분명하듯 이건 거대한 기능의 첫 번째 산출물일 뿐이고, 최근 Java 기능들이 그렇듯 조각조각 전달되는 중임
당연히 목표는 더 큰 값도 평탄화하는 것이며, 그 메커니즘은 이미 JVM 안에 있음. 남은 건 “찢어짐을 허용한다”는 의도를 언어 차원에서 드러내는 일임
- “원소별 헤더 없음. 포인터 없음. 힙을 여기저기 뛰어다니지 않음.” 같은 문장은 AI 문체 냄새가 나고, 그래서 게으른 글쓰기처럼 보임
-
Valhalla에 실제로 들어간 작업의 노고는 인정하지만, “모델은 강력했지만 정신적으로 무거웠다”는 해석은 동의하기 어려움
변수가 null이 될 수 없다고 말하는 건 정신적으로 부담스러운 구분이 아니며, 특히 모든 게 충분히 표기되어 있다면 더더욱 그렇지 않음
“성능 상한을 희생하더라도 사용자 모델을 단순화한다”는 태도도 오히려 사용자에게 단순화를 제공했을 수 있음
프로그래밍 언어의 타입 시스템은 숫자밖에 처리하지 못하는 CPU 위에서 개발자에게 편리한 보장을 제공하기 위한 것임. 선택적 안전 보장을 “너무 복잡하다”는 이유로 줄일 필요는 없음
심지어 “언어 모델과 JVM 모델이 100% 겹칠 필요는 없다”는 인식까지는 도달해 놓고도 그렇다- Java의 방향을 제대로 잡을 수 있을지 신뢰가 별로 가지 않음
Java 관리 체계는 부족해 보이고, 처음부터 대체로 올바른 결정을 한 .NET 쪽과 특히 대비됨
요즘 Oracle 안에서 Java가 가치나 관심을 갖고 있기는 한지도 의문임. 회사는 이제 데이터센터/컴퓨트 사업에 레거시 활동과 막대한 부채가 붙은 형태처럼 보임
가끔 Oracle에서 아직 수익 나는 부문은 법무팀과 잔디깎이 부문뿐 아닌가 싶을 때도 있음 - 그 불만은 Java 언어가 아니라 블로거를 향한 것 아닌가?
그리고 널 표시자도 올 예정임: https://openjdk.org/jeps/8303099
다만 점진적으로 내놓아야 할 뿐이고, 값 클래스/객체를 도입하는 이번 PR만 해도 이미 20만 줄 규모임 - null 불가 값 타입은 후속 JEP에서 다루기로 한 것뿐임
불가능하다고 말하는 건 아닌 것 같고, 코끼리를 한입에 먹을 수는 없다는 얘기임
그렇다 해도 이 한쪽 다리를 꽤 오래 갉아먹고 있긴 함 - nullable은 철도 지향 프로그래밍에서 다른 적재 상태일 뿐임
2012년부터 해결된 개념이라면 상태의 여러 맛을 언어에 직접 넣을 이유가 없음. 레일은 A로 가거나 B로 가는 것뿐이고, 열차의 적재 상태에 따라 갈림
어떤 개념이 생겼다 사라졌다 하며 언어 전쟁이 벌어진다면, 수요가 있는데 언어가 그 수요를 제대로 처리하지 못하거나 처리하더라도 정신적 과부하를 만든다는 신호임
Value,Errorstates,Null,IoExceptions,WeirdOsStatesNeededToHandleUpstairs같은 것들 말임
https://fsharpforfunandprofit.com/rop/
Monty Python식으로 말하면, 이제 좀 진행하자 - 여기서 말하는 건 null 안전성이 아니라
Integer/int와 비슷한 참조/값 투영에 관한 것임
Valhalla 팀은 각 타입마다 식별성이 있는 투영과 없는 투영을 두는 대신, 값 타입은 아예 식별성을 갖지 않게 만들었고 그래서Integer와int가 동의어가 됨
메모리 배치는 문맥과 최적화 결정에 따라 자동으로 정해짐. 그래서Integer같은 기본 래퍼의==의미도 바뀌었고, 이제 “참조 투영”을 쓰는지 “값 투영”을 쓰는지에 의존하지 않음
선택적 안전 보장을 “정신적으로 부담스럽다”는 이유로 줄인 일이 여기서 벌어진 건 아님
- Java의 방향을 제대로 잡을 수 있을지 신뢰가 별로 가지 않음
-
Java/JVM 관련 HN 댓글에서 반복적으로 보이는 건, 놀랄 만큼 많은 사람이 JVM이나 Java의 과거 이미지는 갖고 있지만 오늘날의 모습은 거의 모른다는 점임
2026년의 JVM은 아주 건강한 포식자임. 흠이 있느냐면 물론 있지만, 기반은 극도로 좋음- HN에서는 JVM에 대한 좋은 평을 얻기 어렵고, 여기서는 유행이 지난 기술 취급을 받음
업무에서는 최신 Java 26과 미리보기 기능, 주로StructuredConcurrency를 쓰는데 훌륭함. 이전 회사들에서 Haskell과 Python을 썼던 입장에서도 전혀 후회하지 않음 - 2000년대 Java가 유행하던 시절 시작된 Java 모놀리스를 유지보수하면서 아직도 Java 8로 굴려야 하는 사람이 많음
최근 몇 년 나온 새 기능들은 개인적으로 알고 있지만, 실제 업무에서 Java는 문자 그대로 과거에 갇혀 있음
- HN에서는 JVM에 대한 좋은 평을 얻기 어렵고, 여기서는 유행이 지난 기술 취급을 받음
-
여기 댓글 상당수는 지금 진행 중인 훌륭한 작업과 앞으로 나올 더 멋진 JEP들에 비해 좀 불공정함
Java를 아이에 비유하면, 처음 몇 년은 사랑 많은 부모(Sun)에게 자라다가 이후 다른 아이들과 함께 차고에 던져지고 사악한 보호자(Oracle)에게 방치된 셈임
JDK 8까지 방치되고 사랑받지 못했으니 기본적으로 따라잡기를 해온 것임
“이제야 구조체나 값 타입 같은 게 생겼다”는 말은 맞지만, 그건 거대하고 관료적이며 적대적인 기업 프로세스 때문에 성장이 저해됐기 때문임. 이제는 자유로워졌고 OpenJDK 가족을 통해 사랑받고 있음
앞으로도 한 번 작성해서 어디서나 배포하는 즐거움을 계속 누릴 것임- Oracle을 좋아하든 말든, 그건 Java 역사에 대한 올바른 묘사가 아님
사랑 많은 부모가 키우다가 재정 문제 때문에 위탁 가정에 맡겼고, 거기서 방치된 것에 가까움
이후 새롭고 사랑 많은 부모인 Oracle이 입양했고, Java는 꽃피우며 건강하고 안정적인 성인이 됨
OpenJDK를 참조 구현으로 만들며 플랫폼 오픈소스화를 완료한 것도 Oracle이고, 이전에 독점이던 JFR, Mission Control 같은 도구도 오픈소스화했음
언어 팀의 원년 멤버도 많이 유지했는데, 이런 인수에서 꽤 드문 일이며 Java는 언어와 런타임 양쪽에서 크게 개선됨 - Java가 방치된 건 Sun의 마지막 몇 년 동안임
Oracle은 대부분의 하위 호환성을 유지하면서도 전례 없는 속도로 Java를 전진시켰음
.NET은 “처음부터 제대로 했다”고 하지만, 그게 .NET Framework/.NET Core/.NET 분리와 재작성이라면 이 논의 안에서도 말이 안 됨. .NET은 Java를 보고 배울 수 있었는데도 망친 부분이 있음
MySQL도 마찬가지임. 이 사이트에서는 “죽었다”고 했지만, 실제로 아는 사람들에게는 Oracle 아래에서 되살아났음 - “Oracle이 Java를 방치했다”와 “JDK 8까지 방치됐다”는 말은 서로 모순됨
Sun 아래 마지막 Java 버전은 2006년에 나왔고, Oracle은 2010년에 Sun을 샀으며, JDK 7은 2011년, JDK 8은 2014년에 나왔음
팀은 대체로 그대로였고, 가장 큰 차이는 Oracle이 방치를 끝내고 더 많은 자금을 댔다는 점임. 그래서 인수 이후 Java의 속도가 빨라졌음
“따라잡기”라고 하는데, 누구를 따라잡는다는 건지도 애매함. Java만큼 또는 그보다 더 인기 있는 언어는 JS/TS와 Python뿐임
Java가 뒤처졌다고 말하는 사람들은 보통 Java보다 훨씬 못하고 있는 언어와 비교함. 특정 기능을 좋아하는 사람들이 그 기능을 가진 언어가 그 기능 덕분이 아니라 그럼에도 불구하고 부진하다는 걸 놓치곤 함
관리자들은 빠르게 출시되는 걸 좋아하고, 오히려 Sun 시절부터 있던 기술 리더십이 신중하고 천천히 제대로 해야 한다고 주장함
Java가 2003년만큼 인기 있지는 않다는 분위기는 이해하지만, 그 시기는 Java뿐 아니라 전체 소프트웨어 생태계에서도 예외적으로 통합된 시기였고, 그 전후로 그렇게 통합된 적이 없음 - “한 번 작성해서 어디서나 배포”라지만 브라우저, iOS, 임베디드 시스템에는 안 됨
이제 진짜 한 번 작성해 어디서나 배포하는 기술은 WebAssembly임. JVM의 차례는 있었고 졌음 - 비유를 더 이어가면, Java는 차고에 던져졌을 뿐 아니라 Google을 상대로 수십억 달러 양육비 소송을 거는 데 쓰였고, 결국 현금 수취 수단이 된 셈임
그래도 Java가 “성장 저해”됐다고까지 부르지는 않겠음. 선택을 했고, 일부는 합리적이었고 일부는 아니었으며, 그런 선택은 나중에 고치기 매우 어려움
C++만 봐도 C와의 반호환성은 개인적으로 고칠 수 없는 150피트짜리 알바트로스라고 보고, C++11 이후 많은 버전은 그 알바트로스를 조금 더 견딜 만하게 만드는 작업이었음
JVM에서 모든 값 클래스를 기본 타입처럼 단일 L-타입으로 다루는 건 어려운 문제에 대한 꽤 깔끔한 해결책이라고 봄
결국 이 모든 건 Java 2가 하위 호환성을 위해 제네릭을 타입 소거로 구현하기로 한 결정에서 이어졌고, C3는 그 결과를 보고 그 길을 거부했음
- Oracle을 좋아하든 말든, 그건 Java 역사에 대한 올바른 묘사가 아님
-
Java의 값 타입 진화만으로도 기술 스릴러 한 권은 쓸 수 있을 듯함
메일링 리스트를 읽고 관련 영상을 모두 봤는데, 설계를 언제나 Java답게 보이는 무언가로 통합해낸 과정이 정말 인상적임
동시에 값 타입이 무엇을 의미하는지, 어떤 최적화를 어디에서 할 수 있는지 훨씬 더 세밀하게 파고들었음- 문법 변화는
value하나를 추가하는 것뿐임
- 문법 변화는
-
값 클래스에서
==는 사실상memcmp()처럼 동작하게 되는 셈임
이건 좀 아쉬운데, 캡슐화를 깨고 구현 세부사항을 드러내기 때문임
클라이언트 코드는 주어진 값이 내부적으로 어떻게 표현되는지에 따라 분기할 수 있음. 어떤 면에서는 식별성 비교보다 더 나쁜데, 식별성 비교는 최소한 내부 상태를 노출하지는 않기 때문임- 값 타입은 “마법의 블랙박스 유기체”식 객체지향 사고와 아주 거리가 먼 개념임
고전적 객체지향을 새 방식으로 하는 게 아니라, 객체지향 이념에서 태어난 언어가 탈객체지향 세계로 한 걸음 더 나아가는 방식임 - 데이터 덩어리에 내부 상태가 있다면 그 데이터 덩어리 자체가 잘못된 것임
Java 쪽에서도 비교에서 패딩을 제외하거나 패딩 바이트를 0으로 강제하는 정도는 충분히 생각했을 거라고 봄
문자열에도 동작해야 함. 문자열은 분명 계속 힙에 할당될 것이고, 새 “구조체” 안의 포인터를memcmp하는 건 정확히 식별성 비교임 - 값 클래스의 핵심은 상태를 캡슐화하지 않아야 한다는 것, 즉 완전히 투명한 데이터 보관자라는 점임
- Java를 실전에서 써보지 않았다면 이 변화의 진짜 의미를 못 느낄 수 있음. 이건 Java가 드물게 하는 깨지는 변경임
Java는 객체의 식별성 확인과 동등성 확인을 분리함.==는 기본적으로 두 포인터가 같은지 보고, 동등성은equals/hashCode같은 인터페이스에 기반한 주관적 개념임
그래서new Integer(1000) == new Integer(1000)은 예전에는false였지만 이제true가 되고,new Integer(1000).equals(new Integer(1000))는true이며,new Integer(10) == new Long(10)은 예전에는false였지만 이제 컴파일 오류가 됨
예전 Java에서는 특정 값 이하의 정수가 정규화된 타입으로 대체되곤 했고, 아마 128 근처였던 걸로 기억함. 그래서 10과 1000의 차이가 생겼음
이제는 위 비교들이 암묵적으로 언박싱되는 것 같음.Integer/Long비교가 예전에는false였고 이제 컴파일 오류가 된 걸 보면 언박싱이 개입되는 게 분명함
변수로는 여전히 예전 동작을 얻을 수 있을지도 모름
어쨌든 값 클래스가 식별성을 잃으면==는 포인터 동등성에서 비트 단위 동등성으로 바뀜. 이런 여러 구석 사례를 해결하길 바라지만, 기술적으로는 깨지는 변경임
- 값 타입은 “마법의 블랙박스 유기체”식 객체지향 사고와 아주 거리가 먼 개념임
-
값 클래스의 취지는 이해하지만 구현은 결함이 있음
다음 코드가 무엇을 출력할까?Point a = new Point(10, 10); Point b = a; a.x = 100; System.out.println(b.x);
지금까지 답은 명확했지만, 값 클래스가 추가되면 답은Point가 값 클래스인지 참조 클래스인지에 따라 달라짐. 그래서 이 설계는 가독성을 해침
이는 균일성 원칙 위반임. Weinberg의 『The Psychology of Computer Programming』에서 균일성은 사용자가 비슷해 보이는 것은 비슷하게 동작하고, 다르게 보이는 것은 다르게 동작하리라 기대한다는 심리적 원칙이라고 설명함
프로그래밍 언어가 사용 지점에서 거의 똑같아 보이는 두 구문에 의미상 다른 동작을 허용하면, 독자의 인지 부담이 커짐. 대입, 동등성, 식별성, 변경이 일반 참조 객체처럼 동작하는지 값처럼 동작하는지 알기 위해 타입 선언을 확인하거나 도구에 의존해야 함
선언 시점뿐 아니라 사용 시점에도value키워드를 요구했다면 고칠 수 있었음. 예를 들어value Point a = new Point(10, 10);처럼 쓰는 식임- JEP 401의 목표를 보면 이는 불가능함. 값 클래스는 개발자가 불변 데이터를 위한 프로그래밍 모델을 선택할 수 있게 하려는 것이기 때문임
https://openjdk.org/jeps/401 - 글을 읽어보면 3번째 줄은 문법 오류임. 값 타입의 모든 필드는
final임
물론 저 네 줄만 보고는 그런 일이 생긴다는 걸 알 방법이 없지만, 이 문제는 지금도 있음.Point가 레코드여도 같은 일이 벌어짐 a.x = 100;문장은 유효하지 않을 것임. 레코드 타입은 불변이기 때문임
그러므로 걱정하는 상황은 불가능해야 함- 그래도 좀 흐릿함
value Point a = new Point(10, 10); value Point b copy= a; a.x uniq= 100; System.out.println(b.x);처럼 쓰면 복제/복사가 일어난다는 점과 필드 변경이 다른 객체의 필드에 영향을 주지 않는다는 점이 훨씬 분명해짐
- JEP 401의 목표를 보면 이는 불가능함. 값 클래스는 개발자가 불변 데이터를 위한 프로그래밍 모델을 선택할 수 있게 하려는 것이기 때문임
-
Java 세계에서는 .NET의 존재를 인정하는 게 실례라는 건 알지만, 이게 .NET 구조체와 어떻게 다른지 궁금함
값 타입, 제네릭 특수화, 박싱을 대충 훑어보면 같은 선택을 한 것처럼 보임- C#에는 실제로 함정이 꽤 많고, Java는 이를 명시적으로 만들려는 목표가 있음
C#이 낮은 수준 관점에서 C를 대체로 복사했다면, Java 쪽은 높은 수준에서 접근해 어떤 제약이 어떤 이점을 주는지 자세히 분석했음
다른 언어에서는 구조체/클래스 분류가 이분법적이지만, Java는 기반 도메인의 의미를 반영해 더 세밀한 제어를 허용함
그리고 구조체에는 특히 병렬 문맥에서 다양한 발총이 있음이 드러남 - 글에 그 부분을 다룬 섹션이 있음
개인적으로 C/C#의 구조체는 변경 가능하고 복사로 전달되지만, 값 클래스는 변경할 수 없고 값으로 전달된다고 봄
Java에서 스택 할당은 할 수 없다고 생각함 - 기능적으로는 다르지 않고, Java가 이제는 오래된 관행을 따라잡는 것뿐임
“C#의 구조체는 식별성과 변경을 가지므로 대입이나 전달 시 복사 의미를 정확히 정의해야 하고, 그래서 프로그래머에게 더 무거운 모델과 런타임에 더 적은 자유를 준다”는 식의 거짓 이분법은 설명하는 내용과 잘 맞지 않음
Java 클래스 참조 의미의 식별성은 없겠지만, 특정 주소의 고유한 메모리 구조라는 의미에서는 당연히 여전히 식별성이 있음. 이건 Java 용어를 두고 말꼬리를 잡는 것에 가까움
- C#에는 실제로 함정이 꽤 많고, Java는 이를 명시적으로 만들려는 목표가 있음
-
각주 6의 “C#의 struct와 어떻게 다른가”는 부정확함
글에 AI 생성 이미지가 잔뜩 들어 있는 걸 보면, 글쓰기나 최소한 조사 과정에도 환각이 잔뜩 섞였다고 봐야 하나 싶음 -
글이 좀 흐릿하고 극적이지만, 다행히 원문 문서들은 꽤 읽기 쉬움
최상위 페이지: https://openjdk.org/projects/jdk/28/spec/
JEP 상태: https://bugs.openjdk.org/secure/Dashboard.jspa?selectPageId=...
C#, Swift, Java, Rust의 관련 발전을 누가 추적해주면 좋겠음. 모두 하드웨어를 따라잡기 위해 경쟁해왔고 서로 영향을 주고받고 있다고 봄
개인적으로는 이 변화들이 FFI 메모리 공유에 어떤 영향을 줄지가 걱정됨