코틀린의 Value Class
코틀린의 Value Class
value class는 하나의 값을 래핑하면서, 런타임에 불필요한 객체 할당을 피하도록 컴파일러가 최적화하는 특수한 클래스입니다. 소스 코드 수준에서는 타입 안전성과 의미적 명확성을 제공하지만, 컴파일러는 가능한 한 호출 지점에서 래핑된 값을 인라인(inline) 처리하여 래퍼 객체로 인한 오버헤드를 제거합니다. 따라서 동일한 원시 타입(primitive type)을 서로 다른 용도로 구분해야 할 때, 런타임 비용 없이 타입 안전성을 확보할 수 있어 매우 유용합니다. 실무에서도 ID, 금액, 측정값 등 의미가 다른 값을 혼동 없이 다루기 위해 널리 활용되고 있습니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
value class가 어떻게 컴파일되며, 래퍼 객체가 언제 제거되는지 설명할 수 있습니다.value class선언에 적용되는 제약 조건을 파악할 수 있습니다.value class를 사용하더라도 박싱(boxing)이 발생하는 시나리오를 설명할 수 있습니다.- 함수 매개변수와 도메인 모델에서 타입 안전성을 강화하기 위해
value class를 적용할 수 있습니다.
선언 및 컴파일
value class는 @JvmInline 어노테이션과 value 키워드를 사용하여 선언합니다. 주 생성자에 읽기 전용 프로퍼티를 정확히 하나만 정의해야 합니다.
@JvmInline
value class UserId(val id: Long)
@JvmInline
value class OrderId(val id: Long)
소스 코드 수준에서 UserId와 OrderId는 서로 다른 타입입니다. UserId를 받는 함수에 OrderId를 전달하면 컴파일 오류가 발생합니다. 런타임에서는 컴파일러가 이러한 클래스의 사용처를 가능한 한 내부의 Long 값으로 대체하므로, 객체 할당이 완전히 생략됩니다.
fun fetchUser(userId: UserId) {
// 런타임에서 userId는 단순한 Long 값으로 처리됩니다
}
이때 컴파일러는 이름 맹글링(name mangling)된 함수명을 생성합니다. 동일한 기본 타입을 래핑하는 서로 다른 value class를 매개변수로 받는 함수들 간에 시그니처 충돌이 발생하지 않도록 하기 위함입니다. 가령, UserId를 받는 함수와 OrderId를 받는 함수가 JVM 바이트코드 수준에서 동일한 시그니처를 갖게 되는 것을 방지합니다.
제약 조건
value class에는 컴파일러 최적화를 가능하게 하는 몇 가지 제약 사항이 있습니다.
- 주 생성자에
val프로퍼티를 하나만 선언할 수 있습니다. - 사이드 이펙트(side effect)를 발생시키는 로직이 포함된
init블록을 가질 수 없습니다. - 다른 클래스를 상속받을 수 없지만, 인터페이스 구현은 가능합니다.
- 래퍼 객체가 런타임에 존재하지 않을 수 있으므로,
===와 같은 참조적 동등성(referential equality) 기반의 비교 연산을 사용할 수 없습니다.
이러한 제약 조건 덕분에 컴파일러는 프로그램의 동작을 변경하지 않으면서 래퍼 객체를 내부 값으로 안전하게 대체할 수 있습니다. 면접에서 이 제약 조건들을 명확히 설명하실 수 있다면, value class의 설계 의도를 정확히 이해하고 있음을 보여줄 수 있습니다.
박싱이 발생하는 경우
인라인 최적화에도 불구하고, 특정 상황에서는 여전히 박싱(boxing)이 발생합니다. value class를 nullable 타입으로 사용하거나, 제네릭 컨테이너에 저장하거나, Any 타입을 받는 함수에 전달하면, 런타임에서 실제 래퍼 객체를 생성해야 합니다. 기본 원시 타입만으로는 이러한 경우를 직접 표현할 수 없기 때문입니다.