ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin의 Value Class로 성능 최적화하기 (Codes like a class, works like an int.)
    📖 개발 공부/kotlin 2024. 10. 7. 22:21

     

     

    코틀린, 저는 이렇게 쓰고 있습니다 | 카카오페이 기술 블로그

    코틀린으로 서비스를 개발하며 직접 경험한 코틀린의 매력을 소개합니다.

    tech.kakaopay.com

    위 글을 통해 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의 특징 간단 요약

    1. 반드시 @JvmInline 어노테이션과 함께 사용해야 한다.
    2. 자동 생성 메서드는 equals(), toString(), hashCode()가 전부이다.
    3. 불변(val) 프로퍼티 1개만 가질 수 있다.
      Value Class는 단일 속성만을 가질 수 있다. 따라서 여러 속성을 가진 데이터를 표현하는 데는 적합하지 않다.
    4. 컴파일 타임에 "===" 비교(동일성 비교)를 허용하지 않는다.

     

    성능 테스트: 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를 활용하고 있는지 다른 분들의 경험도 궁금하다! (경험이 있으시다면 댓글로 공유부탁드립니닷 ㅎㅎ 🤩)

     

     

     

     

     

     

     

    🔗 참고 링크

    반응형

    댓글

Designed by Tistory.