요약
Java record는 이미 많은 개발자가 쓰고 있습니다. DTO나 읽기 전용 데이터 carrier를 만들 때 보일러플레이트를 크게 줄여주기 때문입니다. 그런데 Project Valhalla의 value class는 record와 같은 문법 편의 기능이 아닙니다. 둘 다 “값을 담는 타입”처럼 보이지만, JVM이 바라보는 의미는 다릅니다.
핵심은 identity입니다. 일반 record는 여전히 identity가 있는 객체이고, value class는 identity가 없는 값 객체를 지향합니다. 이 차이가 ==, synchronization, 메모리 layout, 최적화 가능성까지 이어집니다.
목차
이 글에서 다루는 내용
record는 무엇을 해결했나
Java record는 데이터를 담는 클래스를 간결하게 만들기 위해 들어왔습니다. 예전에는 작은 데이터 타입 하나에도 필드, 생성자, getter, equals, hashCode, toString을 계속 작성해야 했습니다.
public record Point(int x, int y) {}
이렇게 쓰면 컴파일러가 기본적인 데이터 carrier 코드를 만들어줍니다. 그래서 API 응답 DTO, 내부 command 객체, 간단한 설정 값, 테스트 데이터 등에 잘 맞습니다.
하지만 record는 여전히 일반 객체입니다. heap에 객체가 만들어지고, 객체 identity가 존재합니다. 같은 값을 가진 record 두 개가 equals()로는 같아도 ==로는 같은 객체가 아닐 수 있습니다.
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println(a.equals(b)); // true
System.out.println(a == b); // false
record는 “코드를 짧게 만드는 기능”에 가깝습니다. 객체 모델 자체를 크게 바꾸지는 않습니다.
value class는 무엇을 바꾸려 하나
Project Valhalla의 value class는 문제를 더 아래에서 봅니다. Java가 객체지향 추상화를 유지하면서도 primitive에 가까운 성능 특성을 얻을 수 있게 하는 것이 목표입니다.
JEP 401의 예시는 이런 형태입니다.
value record Point(int x, int y) {}
Point p = new Point(17, 3);
Objects.hasIdentity(p); // false
new Point(17, 3) == p; // true
여기서 중요한 부분은 Objects.hasIdentity(p)가 false라는 점입니다. 이 객체는 “어느 heap 위치에 존재하는 특정 객체”라는 의미보다, x=17, y=3이라는 값 자체가 중요합니다.
이 차이 때문에 JVM은 value object를 더 자유롭게 최적화할 수 있습니다. 필요하다면 객체를 heap에 따로 두지 않고, 필드나 배열 내부에 더 compact하게 배치할 가능성이 생깁니다.
record와 value class의 가장 큰 차이: identity
record와 value class의 차이를 표로 정리하면 이렇게 볼 수 있습니다.
record:
- identity가 있는 일반 객체
- 문법과 보일러플레이트를 줄이는 데 초점
- equals/hashCode/toString 자동 생성
- heap object로 다뤄지는 일반적인 객체 모델
value class:
- identity가 없는 값 객체
- JVM object model과 성능 모델 개선에 초점
- 값이 같으면 같은 값으로 볼 수 있음
- flattening/scalarization 같은 최적화 가능성
이 차이는 생각보다 큽니다. identity가 있으면 객체는 “복사해도 되는 값”이라기보다 “특정 위치에 존재하는 개체”에 가깝습니다. identity가 없으면 JVM은 그 값을 더 단순한 데이터 묶음처럼 다룰 수 있습니다.
그래서 value class는 synchronization 대상이 될 수 없고, identity 기반 캐시나 자료구조와도 맞지 않을 수 있습니다.
성능 관점에서 왜 value class가 중요한가
Java 코드에서 작은 객체는 정말 자주 만들어집니다.
Money
Point
Range
UserId
OrderId
EventTime
MetricPoint
이런 타입은 도메인 의미를 잘 표현합니다. 그런데 성능이 중요한 코드에서는 객체가 많아질수록 allocation, reference chasing, GC 부담이 문제가 됩니다.
기존 객체 배열은 보통 이런 구조입니다.
Point[]
-> reference
-> heap object Point
-> x, y
value class가 안정적으로 자리 잡으면, JVM은 더 compact한 layout을 시도할 수 있습니다.
Point[]
-> x, y 값이 더 직접적으로 배치될 가능성
이건 단순히 객체 생성 속도만의 문제가 아닙니다. CPU cache locality, GC 부담, 메모리 사용량까지 연결됩니다. Go나 Rust가 성능 면에서 강하게 느껴지는 이유 중 하나도 데이터 layout을 더 명확하게 다룰 수 있기 때문입니다. Valhalla는 Java가 이 약점을 줄이려는 시도입니다.
실무에서는 어디에 쓰면 좋을까
value class가 안정화되면 가장 먼저 떠오르는 곳은 도메인 value object입니다.
value class Money {
private long cents;
public Money(long cents) {
this.cents = cents;
}
public long cents() {
return cents;
}
}
long cents만 쓰면 빠를 수는 있지만 의미가 약합니다. Money라는 타입을 쓰면 코드가 훨씬 안전하고 읽기 좋습니다. 문제는 성능이었습니다. value class는 이 간극을 줄일 수 있습니다.
다음과 같은 영역도 후보입니다.
- 좌표, 범위, 시간 구간
- 주문 ID, 사용자 ID, 추적 ID
- 이벤트/로그/메트릭의 작은 값 타입
- 벡터 검색이나 랭킹에서 쓰는 작은 수치 데이터
물론 모든 타입을 value class로 바꾸자는 이야기는 아닙니다. identity가 필요한 객체, 생명주기가 중요한 객체, mutable state가 필요한 객체는 일반 class가 맞습니다.
주의할 점
value class는 record의 상위 호환이 아닙니다. record를 쓰던 자리에 무조건 value class를 넣으면 된다는 식으로 보면 위험합니다.
주의할 점은 이렇습니다.
- identity가 없으므로
synchronized대상으로 적합하지 않습니다. ==의 의미가 일반 객체와 다르게 느껴질 수 있습니다.- mutable object처럼 설계하면 안 됩니다.
- identity 기반 캐시, lock, lifecycle 관리 객체에는 맞지 않습니다.
- JEP 401은 Preview 단계이므로 실제 사양은 바뀔 수 있습니다.
실무에서는 “작고, 불변이고, 값 자체가 의미이며, identity가 필요 없는 타입”부터 검토하는 것이 좋습니다.
결론
record와 value class는 겉으로는 비슷해 보입니다. 둘 다 데이터를 표현하는 타입처럼 보이기 때문입니다. 하지만 record는 Java 코드를 간결하게 만드는 기능이고, value class는 Java의 객체 모델과 성능 모델을 바꾸려는 기능에 가깝습니다.
Java를 오래 쓰다 보면 도메인 모델을 잘 만들고 싶지만 성능 때문에 primitive로 내려가고 싶은 순간이 있습니다. value class는 이 타협을 줄여줄 수 있는 가능성입니다.
아직은 Preview 단계라 실제 프로젝트에 바로 적용하기보다 계속 지켜보고 테스트해야 합니다. 그래도 Java가 객체지향적인 표현력을 유지하면서 성능상의 오래된 약점을 줄이려 한다는 점에서, Project Valhalla는 충분히 기대해볼 만한 변화입니다.