[Kotlin] value class

boogi-woogi·2023년 2월 25일
0
post-thumbnail

Level 1의 두번째 미션인 로또 미션을 진행하면서 로또의 숫자를 wrapping한 클래스인 LottoNumber를 구현할 때 value class를 이용한 크루들이 심심치않게 보였다. value class에 대한 궁금증을 해소하기 위해 이 글을 쓰게 되었다.

value class를 사용하는 이유?

Sometimes it is necessary for business logic to create a wrapper around some type. However, it introduces runtime overhead due to additional heap allocations. Moreover, if the wrapped type is primitive, the performance hit is terrible, because primitive types are usually heavily optimized by the runtime, while their wrappers don't get any special treatment.

공식문서

Wrapper는 추가적인 Heap 영역에 할당된다. Wrapping의 대상이 되는 타입이 primitive 타입이라면 런타임 성능에 더 악영향을 미친다. primitive타입은 런타입에 최적화되어 있는데 primitive 타입을 wrapping하는 순간 최적화가 의미가 없어지기 때문이다.


primitive type을 wrapping한 클래스

// int를 wrapping 한 클래스
class Score(val value: Int)
fun main(){
    val perfect = Score(100)
    showScore(perfect)
}

fun showScore(score: Score){
    println(score.value)
}

Decompile to Java

public final class ApplicationKt {
   public static final void main() {
      Score perfect = new Score(100);
      showScore(perfect);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void showScore(@NotNull Score score) {
      Intrinsics.checkNotNullParameter(score, "score");
      int var1 = score.getValue();
      System.out.println(var1);
   }
}

Decompile된 버전을 보면 위에서 언급했던 문제점이 드러난다. runtime에 최적화 된 int(primitive type)를 Wrapping하면서 힙 영역의 추가적인 할당으로 런타임 성능에 악영향을 미칠 것이라는 것을 알 수 있다.


Value class를 사용하면 어떨까?

@JvmInline
value class Score(val value: Int)
fun main(){
    val perfect = Score(100)
		showScore(perfect)
}

fun showScore(score: Score){
		println(score.value)
}

Decompile to Java

public final class ApplicationKt {
   public static final void main() {
      int perfect = Score.constructor-impl(100);
      showScore-VJLaubs(perfect);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void showScore_VJLaubs/* $FF was: showScore-VJLaubs*/(int score) {
      System.out.println(score);
   }
}

추가적인 Score 객체를 생성하는 것이 아니라 constructor-impl을 통해 int 타입의 변수를 생성하는 것을 알 수 있다! (Heap 영역의 추가적인 메모리를 요구하지 않는다.)

Decompile to Java (Score.kt)

public final class Score {
   private final int value;

   public final int getValue() {
      return this.value;
   }

   // $FF: synthetic method
   private Score(int value) {
      this.value = value;
   }

   public static int constructor_impl/* $FF was: constructor-impl*/(int value) {
      return value;
   }
...
}

Score 클래스 코드를 decompile 해보니 int형 변수를 return하는 constructor_impl이라는 static 메소드를 발견할 수 있었다.

@JvmInline
value class Score(val value: Int) {
    init {
        require(value > 0)
    }
}

이렇게 init block에 require를 걸어주면 어떻게 생성이 될까?

Decompile to Java (Score.kt)

public final class Score {
   private final int value;

   public final int getValue() {
      return this.value;
   }

   // $FF: synthetic method
   private Score(int value) {
      this.value = value;
   }

   public static int constructor_impl/* $FF was: constructor-impl*/(int value) {
      boolean var1 = value > 0;
      if (!var1) {
         String var2 = "Failed requirement.";
         throw new IllegalArgumentException(var2.toString());
      } else {
         return value;
      }
   }
...
}

constructor_impl에서 검증이 이루어진 int형 변수를 생성하는 것을 알 수 있다.


data 클래스와의 차이

  • data class는 equals, toString, hashCode, copy, componentN을 자동으로 생성해주는 반면에 value class는 equals, toString, hashCode만 자동으로 생성해준다.
  • data class는 varval를 모두 허용하지만 value class는 val만 허용한다.
  • value class는 하나의 프로퍼티만 허용한다.

결론

런타임에 최적화되어 있는 primitive type을 wrapping 하게되면 그 최적화가 의미가 없어지게 된다. value class를 사용하면 wrapper를 생성할때 추가적인 힙 메모리 영역을 필요로하지 않는다. primitive한 타입 자체로 존재할 수 있게 된다.

profile
https://github.com/boogi-woogi

0개의 댓글