---
title: "자바 Valhalla가 도메인 원시 타입의 성능 핑계를 지워가고 있음"
published: 2026-05-19T20:31:56.000Z
canonical: https://jeff.news/article/2939
---
# 자바 Valhalla가 도메인 원시 타입의 성능 핑계를 지워가고 있음

글은 Project Valhalla의 value class가 자바에서 도메인 제약을 타입으로 표현할 때 생기던 성능 비용을 크게 줄인다고 설명한다. PositiveInt 같은 래퍼 타입이 기존엔 객체 헤더와 포인터 추적으로 비쌌지만, Valhalla에서는 정적 타입이 맞을 때 배열 슬롯이나 객체 필드에 평평하게 저장될 수 있다. 다만 제네릭 박싱, 프레임워크 어댑터, == 의미 변화 같은 현실적인 주의점은 아직 남아 있다.

## 도메인 타입은 좋지만, 기존 자바에선 비쌌음

- 글의 출발점은 “컴파일러를 보안팀처럼 쓰자”는 아이디어임
  - 예를 들어 int를 그대로 쓰지 않고 PositiveInt로 감싸면, 음수 같은 잘못된 상태를 타입 차원에서 막을 수 있음
  - Price, Probability, Port, Email 같은 값도 마찬가지로 비즈니스 제약을 타입에 넣을 수 있음

- 문제는 기존 자바 래퍼 클래스가 너무 비싸다는 거였음
  - HotSpot에서 `class PositiveInt { final int v; }` 같은 객체는 16바이트를 먹음
  - 구조상 12바이트 객체 헤더와 4바이트 int가 붙고, 배열에는 값이 아니라 4바이트 참조가 들어감
  - `-XX:+UseCompactObjectHeaders`를 쓰면 헤더가 8바이트까지 줄지만, 그래도 “int 하나 감싸자고 객체 하나”라는 구조는 그대로임

- 그래서 실무 룰은 꽤 현실적이었음
  - API 경계나 생성 시점에서는 타입을 정교하게 만들 수 있음
  - 하지만 수백만 이벤트를 도는 핫 루프에서는 raw int, raw float, raw String으로 돌아가는 경우가 많았음
  - GC 추적, 캐시 미스, 포인터 추적 비용이 타입 안정성보다 먼저 발목을 잡았기 때문임

## Valhalla의 value class가 바꾸는 지점

- Project Valhalla는 이 비용 구조를 바꾸려는 자바/JVM 프로젝트임
  - 글의 실험은 JEP 401을 구현한 `openjdk 27-jep401ea3` EA 빌드에서 진행됨
  - 핵심은 새 `value` 키워드로 identity 없는 클래스를 만들 수 있다는 점임

- value class는 “클래스처럼 코딩하지만, 원시값처럼 저장될 수 있는” 타입에 가까움
  - 생성자도 있고, 검증 로직도 있고, 메서드도 가질 수 있음
  - 대신 객체 정체성(identity)이 없으니 JVM이 레지스터, 객체 필드, 배열 슬롯에 값을 바로 펼쳐 넣을 수 있음
  - 즉 PositiveInt라는 타입 보장은 유지하면서도 객체 헤더와 포인터 추적을 피할 수 있음

> [!IMPORTANT]
> 글의 핵심 수치는 이거임. value class 배열은 bare primitive와 같은 크기와 배치를 가질 수 있고, identity class는 같은 값을 담아도 대략 4배 비쌀 수 있음.

- 단, “정적 타입이 무엇이냐”가 매우 중요함
  - 변수가 `PositiveInt`처럼 구체 value class 타입이면 JVM이 평평한 배치를 선택할 수 있음
  - 반대로 `RefinedInt` 참조, 인터페이스 파라미터, 컬렉션 원소처럼 추상 타입으로 들고 있으면 힙 할당이 강제될 수 있음
  - 이 차이를 모르고 쓰면 “Valhalla 썼는데 왜 느리지?”가 나올 수 있음

## 숫자로 보면 왜 의미가 큰지 보임

- 글은 10개짜리 배열 기준으로 메모리 풋프린트를 비교함
  - 환경은 64비트 HotSpot, compressed oops 사용 조건임
  - value class는 bare primitive와 같은 크기, 같은 레이아웃, 같은 캐시 동작을 보였다고 설명함
  - identity class는 객체 헤더와 참조 때문에 약 4배 비용이 발생함

- 멀티 필드 타입에서도 효과가 이어짐
  - 예를 들어 Coordinate가 Latitude와 Longitude를 들고 있고, 둘 다 double 기반 value class라면 JVM은 두 double을 슬롯 안에 이어서 넣을 수 있음
  - `Coordinate[10]` 배열은 160바이트의 연속된 double 데이터처럼 다뤄질 수 있음
  - 기존 identity class 방식이면 배열에는 참조 10개가 있고, 실제 객체 10개는 힙 여기저기에 흩어짐

- 문자열 기반 타입은 이득이 제한적임
  - Email, HostName, Slug 같은 타입은 래퍼 객체 헤더는 줄일 수 있음
  - 하지만 내부적으로 String 참조를 들고 있으니 문자열 자체에 대한 indirection은 남음
  - 그러니까 모든 도메인 타입이 int처럼 완전히 싸지는 건 아님

## 아직 밟으면 아픈 지뢰도 있음

- `==` 의미가 바뀌는 건 마이그레이션 때 꽤 중요함
  - identity class에서 `==`는 참조 비교임
  - value class에서 `==`는 필드 값 기준의 substitutability 비교에 가까움
  - 기존 코드가 참조 동일성에 기대고 있었다면 value class 전환만으로 조용히 의미가 바뀔 수 있음

- null 처리도 달라짐
  - value class는 null이 될 수 없음
  - JEP 401의 동반 흐름으로 null-restricted reference가 논의되고 있고, 글에서는 `PositiveInt!`와 `PositiveInt?` 같은 문법이 언급됨
  - 문법은 아직 EA 빌드에서 유동적이지만, 핫패스에서는 null-check가 사라지는 효과가 이미 보인다고 설명함

> [!WARNING]
> 기존 identity class를 value class로 바꾸는 건 단순 성능 튜닝이 아님. `==`, null, 제네릭 박싱, 프레임워크 직렬화까지 같이 점검해야 함.

- 제네릭은 아직 큰 한계임
  - `List<PositiveInt>`와 `Optional<PositiveInt>`는 현재 박싱됨
  - 타입 소거 때문에 각 요소가 힙으로 올라가고, generic specialization은 아직 출시되지 않음
  - 성능 민감 코드에서는 당분간 `PositiveInt[]` 같은 typed array와 value-typed field가 핵심임

- 프레임워크 통합도 공짜가 아님
  - Jackson, JPA, Bean Validation은 기본적으로 primitive와 String을 기대함
  - 각 value type마다 얇은 어댑터나 deserializer가 필요함
  - 글에서 소개한 라이브러리는 Jackson과 JPA 어댑터를 포함하지만, 모듈 등록은 사용자가 해야 함

## 결론은 꽤 큼

- Valhalla가 안정화되면 자바 도메인 모델링의 타협점이 바뀔 수 있음
  - 지금까지는 “경계에서는 타입을 정교하게, 핫패스에서는 raw primitive”가 현실적인 선택이었음
  - value class가 제대로 들어오면 “컴파일러가 보장하는 도메인 타입”을 성능 민감 코드에도 넣을 수 있음
  - 글의 표현대로라면 “클래스처럼 코딩하고 int처럼 동작하는” 방향에 가까워지는 중임

- 다만 아직은 프리뷰임
  - Java 27 EA의 Valhalla preview 기반 실험이라 운영 코드에 바로 박을 단계는 아님
  - 그래도 자바 백엔드에서 타입 안정성과 성능을 동시에 챙기려는 팀이라면 지금부터 설계 감각을 업데이트할 만함

---

## 기술 맥락

- 여기서 중요한 선택은 raw primitive를 계속 쓸지, PositiveInt 같은 도메인 원시 타입을 쓸지예요. 예전에는 타입 안정성을 얻는 대신 객체 할당과 캐시 미스를 감수해야 해서, 핫패스에서는 원시 타입으로 도망가는 게 현실적이었거든요.

- Valhalla의 value class가 흥미로운 이유는 이 선택지를 JVM 레벨에서 다시 열어주기 때문이에요. 객체처럼 생성자 검증과 메서드를 갖지만, identity를 포기하는 대신 배열 슬롯이나 객체 필드에 값을 바로 넣을 수 있어요.

- 특히 이벤트 스트림처럼 수백만 개의 값이 오가는 코드에서는 포인터 하나 더 따라가는 비용이 꽤 커요. 글에서 말한 것처럼 identity class는 객체 헤더와 참조 때문에 int 하나보다 훨씬 비싸고, value class는 특정 조건에서 primitive와 같은 레이아웃을 얻을 수 있어요.

- 하지만 “value class면 무조건 빠르다”는 식으로 보면 안 돼요. 정적 타입이 구체 value class여야 하고, `List<PositiveInt>` 같은 제네릭 컬렉션은 아직 박싱돼요. 그래서 성능 민감한 구간에서는 배열과 필드 설계까지 같이 봐야 해요.

- 프레임워크 경계도 실무에서 꽤 중요해요. Jackson이나 JPA는 이런 새 타입을 자동으로 이해하지 못하니 deserializer나 adapter를 붙여야 해요. 도메인 모델을 더 안전하게 만들 수는 있지만, 입출력 계층에서는 여전히 접착 코드가 필요해요.

## 핵심 포인트

- value class는 객체처럼 동작하지만 JVM이 원시값처럼 평평하게 배치할 수 있음
- PositiveInt 같은 도메인 타입이 int와 같은 메모리 배치를 가질 수 있어 핫패스에서도 쓸 여지가 생김
- List나 Optional 같은 제네릭은 아직 박싱되므로 성능 민감 코드에서는 배열과 값 타입 필드가 중요함
- 기존 identity class를 value class로 바꾸면 == 의미가 참조 비교에서 값 비교로 바뀔 수 있음

## 인사이트

이 글의 포인트는 “타입 안정성 vs 성능”이라는 오래된 자바 백엔드 트레이드오프가 JVM 레벨에서 바뀌고 있다는 점이다. 아직 프리뷰라서 당장 운영 코드에 박을 단계는 아니지만, 자바 도메인 모델링 방식은 꽤 크게 흔들릴 수 있음.
