-
Kotlin의 Value Class로 성능 최적화하기 (Codes like a class, works like an int.)📖 개발 공부/kotlin 2024. 10. 7. 22:21
위 글을 통해 value class를 처음 접하게 되었다. 더 깊이 이해하고 정리하기 위해 이번 글에서 value class에 대해 자세히 다뤄보려고 한다.
Value Class란?
Kotlin의 value class는 코틀린에서 값을 나타내기 위한 wrapper 클래스이다. 이는 primitive 타입의 값을 객체와 같이 다룰 수 있게 해주면서도, wrapper 클래스의 오버헤드 문제를 해결할 수 있다.
기존의 클래스와 Value Class의 차이점
기존에 Kotlin에서는 데이터 처리를 위해 Data Class를 주로 사용했다. Data Class는 내부에 다양한 데이터를 관리하는 데 유용하지만, 메모리 할당 및 해제에서 발생하는 오버헤드가 존재한다.
반면, Value Class는 컴파일 시 해당 클래스의 내용을 인라인 처리하여 불필요한 객체 생성을 줄이고 메모리 사용을 최적화한다. 이러한 특징 덕분에 주로 단일 값을 처리하거나 연산이 빈번히 일어나는 상황에서 유리하다.
다음 코드는 Value Class를 간단하게 사용하는 코드이다.
@JvmInline value class MoneyValue private constructor(val value: Double) { companion object { fun of(value: Double) = MoneyValue(value) } } fun main() { val money = MoneyValue.of(3_000.0) print(money) }
Money 클래스로 Data Class와 Value Class 비교하기
아래 예제는 Money라는 클래스를 data class 와 value class 로 정의한 코드이다.
@JvmInline value class MoneyValue private constructor(val value: Double) { companion object { fun of(value: Double) = MoneyValue(value) } } data class MoneyData(val value: Double) { companion object { fun of(value: Double) = MoneyData(value) } }
두 클래스의 정의만 보았을 때는 큰 차이가 없어 보일 수 있다.
하지만 이를 자바 코드로 변환해보면 차이점이 보인다.
자바 코드로 변환된 결과 보기
fun returnMoneyData(moneyData: MoneyData): MoneyData { return moneyData } fun returnMoneyValue(moneyValue: MoneyValue): MoneyValue { return moneyValue } fun processMoneyData() { (0..10_000_000).forEach { i -> val money = MoneyData(i + 0.0) returnMoneyData(money) } createHeapDump("processMoneyDataIncludingReturnMethod") } fun processMoneyValue() { (0..10_000_000).forEach { i -> val money = MoneyValue(i + 0.0) returnMoneyValue(money) } createHeapDump("processMoneyValueIncludingReturnMethod") }
이를 자바 코드로 변환해보면 다음과 같은 결과를 얻을 수 있다.
@NotNull public static final MoneyData returnMoneyData(@NotNull MoneyData moneyData) { Intrinsics.checkNotNullParameter(moneyData, "moneyData"); return moneyData; } public static final double returnMoneyValue_YjeT3yE/* $FF was: returnMoneyValue-YjeT3yE*/(double moneyValue) { return moneyValue; } public static final void processMoneyData() { Iterable $this$forEach$iv = (Iterable)(new IntRange(0, 10000000)); int $i$f$forEach = false; Iterator var2 = $this$forEach$iv.iterator(); while(var2.hasNext()) { int element$iv = ((IntIterator)var2).nextInt(); int i = element$iv; int var5 = false; MoneyData money = new MoneyData((double)i + 0.0); returnMoneyData(money); } createHeapDump("processMoneyDataIncludingReturnMethod"); } public static final void processMoneyValue() { Iterable $this$forEach$iv = (Iterable)(new IntRange(0, 10000000)); int $i$f$forEach = false; Iterator var2 = $this$forEach$iv.iterator(); while(var2.hasNext()) { int element$iv = ((IntIterator)var2).nextInt(); int i = element$iv; int var5 = false; double money = MoneyValue.constructor-impl((double)i + 0.0); returnMoneyValue-YjeT3yE(money); } createHeapDump("processMoneyValueIncludingReturnMethod"); }
returnMoneyData() 함수에서는 MoneyData 객체를 그대로 반환하는 반면, returnMoneyValue() 함수에서는 MoneyValue 객체가 내부의 기본 값(double)으로 변환되어 전달된다.
즉, value class는 컴파일 시 내부 값을 직접 사용하게 되어 객체를 생성하는 오버헤드를 피할 수 있다.
public static final double returnMoneyValue_YjeT3yE/* $FF was: returnMoneyValue-YjeT3yE*/(double moneyValue) { return moneyValue; }
맹글링(mangling)
여기서 value class에 적용된 함수 이름의 임의 변형은 맹글링(mangling)이라고 부른다. 같은 이름의 여러 함수를 구분하기 위해 JVM 컴파일러가 함수 이름에 임의의 코드(-[a-zA-Z_]{7})를 추가하는 작업이다.Kotlin value class의 특징 간단 요약
- 반드시 @JvmInline 어노테이션과 함께 사용해야 한다.
- 자동 생성 메서드는 equals(), toString(), hashCode()가 전부이다.
- 불변(val) 프로퍼티 1개만 가질 수 있다.
Value Class는 단일 속성만을 가질 수 있다. 따라서 여러 속성을 가진 데이터를 표현하는 데는 적합하지 않다. - 컴파일 타임에 "===" 비교(동일성 비교)를 허용하지 않는다.
성능 테스트: data class vs value class
두 함수(processMoneyData와 processMoneyValue)를 각각 실행해보면서 메모리 사용량을 비교해보았다.
fun main() { // processMoneyData() processMoneyValue() } fun processMoneyData() { (0..10_000_000).forEach { i -> val money = MoneyData(i + 0.0) returnMoneyData(money) } createHeapDump("processMoneyDataIncludingReturnMethod") } fun processMoneyValue() { (0..10_000_000).forEach { i -> val money = MoneyValue(i + 0.0) returnMoneyValue(money) } createHeapDump("processMoneyValueIncludingReturnMethod") }
processMoneyData() 실행 결과
Total Bytes: 18,192,456 Total Classes: 1,953 Total Instances: 353,988 Classloaders: 4 GC Roots: 1,818
processMoneyData() 실행했을 때는 MoneyData 객체가 생성되고, 이 객체가 메모리를 상당히 차지하고 있는 것을 볼 수 있다.
processMoneyValue() 실행 결과
Total Bytes: 13,984,808 Total Classes: 1,954 Total Instances: 177,706 Classloaders: 4 GC Roots: 1,818
MoneyValue의 경우, 객체가 생성되지 않으며 메모리 사용량이 약 23% 줄어든 것을 확인할 수 있었다.
이로써 value class를 사용했을 때 객체 생성 비용을 줄이고, 메모리 측면에서 큰 이점을 가질 수 있음을 알 수 있었다!
"Codes like a class, works like an int."
— 이 문장은 value class의 핵심적인 특징을 가장 잘 요약한 문장이다.
나는 도메인 언어를 명확히 표현하고 싶을 때 이 value class를 사용할 것 같다.
위에서 예로 들었던 것처럼, "돈"이라는 개념을 단순히 number 타입으로 표현할 수도 있겠지만, 이를 도메인 언어로 더 명확히 나타내고 싶을 땐 Money 클래스를 만들어낼 것이다. 이때 data class 대신 value class를 활용하면 더 의미 있는 타입 정의와 함께 성능 최적화도 이룰 수 있다!
실제로 실무에서 어떻게 value class를 활용하고 있는지 다른 분들의 경험도 궁금하다! (경험이 있으시다면 댓글로 공유부탁드립니닷 ㅎㅎ 🤩)
🔗 참고 링크
반응형'📖 개발 공부 > kotlin' 카테고리의 다른 글
테스트 코드 개선하기: Kotest 모킹 초기화 및 구조 개선 (0) 2024.10.27 Spring AOP를 Kotlin으로 개선해보기! (feat. 카카오페이 테크블로그) (0) 2024.03.17 Kotest 예제와 함께 테스트 코드 살펴보기 (0) 2024.02.04 도메인 모델에 kotlin의 data class가 적합할까? (0) 2023.12.10 [Kotlin] @Synchronized 어노테이션 동작 방식 (0) 2023.08.03