Kotlin 의 value class

JoJo Green·2023년 1월 13일
0
post-thumbnail

value class


  • 코틀린에서 프로퍼티가 하나인 VO(Value Object) 를 만들어 사용할 때 선언할 수 있는 클래스 키워드
    • class 앞에 value 키워드를 선언해주면 된다.
    • 프로퍼티는 단 하나만 가질 수 있다.
    • 프로퍼티는 반드시 val 이어야만 한다.
    • @JvmInline 을 선언해줘야한다.
      • 코틀린의 다른 버전과의 value class 호환을 위해서 존재하는 Annotation
    • 예시
      @JvmInline
      value class Age(val value: Int) {
          infix fun isOrderThan(age: Age): Boolean {
              return value > age.value
          }
      }
  • 일반적인 클래스 형태로도 VO 를 만들어 사용할 수 있지만 value class 로 선언하여 사용하면 메모리를 최적화하여 사용할 수 있다.
    • value class 를 선언한 VO 를 사용하면 바이트 코드 상에서 VO 객체를 생성하지 않고 내부 프로퍼티를 직접 사용하여 VO 객체 생성에 따른 메모리 비용을 최적화 할 수 있다.
  • value class 를 선언하면 내부적으로 equals(), hashcode(), toString() 메서드를 자동 생성해준다.
  • 1.5 버전 이전의 inline class 와 같다.
    • 1.5 버전 부터는 value class 로 이름도 바꾸고 딱 VO 객체 최적화를 위해서 사용하라고 만든 키워드이다.
  • 프로퍼티가 하나인 VO 를 사용하는 경우 런타임 시점의 VO 객체 생성 비용을 최적화하도록 바이트코드를 조작하고 싶다면 value class 를 사용할 수 있다.

예시

  • value class 를 선언한 VO 인 Age 클래스를 사용하는 Person 클래스
    class Person(val name: String, val age: Age) {
        fun introduce() {
            println("Hi, I'm $name my age is ${age.value}")
        }
    }
    
    @JvmInline
    value class Age(val value: Int) {
        init {
            check(value > 0)
        }
        infix fun isOrderThan(age: Age): Boolean {
            return value > age.value
        }
    }
    
    fun main() {
        val age20 = Age(20)
        val age19 = Age(19)
    
        println(age20 isOrderThan age19) // 결과 : ture
    }
    • 나이라는 양의 정수 값을 한번 Wrapping 한 VO 인 Age 클래스를 만들고 Person 이라는 클래스가 프로퍼티로서 사용하고 있다.
    • Age 는 value class 로 구현했다.
  • 위의 코드를 자바로 변환한 결과(중요한 부분만 발췌)
    public final class Person {
       @NotNull
       private final String name;
       private final int age;
    
       public final void introduce() {
          String var1 = "Hi, I'm " + this.name + " my age is " + this.age;
          System.out.println(var1);
       }
    
       @NotNull
       public final String getName() {
          return this.name;
       }
    
       public final int getAge_3KwQcjU/* $FF was: getAge-3KwQcjU*/() {
          return this.age;
       }
    
       private Person(String name, int age) {
          this.name = name;
          this.age = age;
       }
    
       // $FF: synthetic method
       public Person(String name, int age, DefaultConstructorMarker $constructor_marker) {
          this(name, age);
       }
    }
    
    @JvmInline
    public final class Age {
       private final int value;
    
       public final int getValue() {
          return this.value;
       }
    
       // $FF: synthetic method
       private Age(int value) {
          this.value = value;
       }
    
       public static final boolean isOrderThan_Ue8C4z0/* $FF was: isOrderThan-Ue8C4z0*/(int $this, int age) {
          return $this > age;
       }
    
       public static int constructor_impl/* $FF was: constructor-impl*/(int value) {
          boolean var1 = value > 0;
          if (!var1) {
             String var2 = "Check failed.";
             throw new IllegalStateException(var2.toString());
          } else {
             return value;
          }
       }
    
       // $FF: synthetic method
       public static final Age box_impl/* $FF was: box-impl*/(int v) {
          return new Age(v);
       }
    
       public static String toString_impl/* $FF was: toString-impl*/(int var0) {
          return "Age(value=" + var0 + ")";
       }
    
       public static int hashCode_impl/* $FF was: hashCode-impl*/(int var0) {
          return Integer.hashCode(var0);
       }
    
       public static boolean equals_impl/* $FF was: equals-impl*/(int var0, Object var1) {
          if (var1 instanceof Age) {
             int var2 = ((Age)var1).unbox-impl();
             if (var0 == var2) {
                return true;
             }
          }
    
          return false;
       }
    
       public static final boolean equals_impl0/* $FF was: equals-impl0*/(int p1, int p2) {
          return p1 == p2;
       }
    
       // $FF: synthetic method
       public final int unbox_impl/* $FF was: unbox-impl*/() {
          return this.value;
       }
    
       public String toString() {
          return toString-impl(this.value);
       }
    
       public int hashCode() {
          return hashCode-impl(this.value);
       }
    
       public boolean equals(Object var1) {
          return equals-impl(this.value, var1);
       }
    }
    
    public final class ValueClassTestKt {
       public static final void main() {
          int age20 = Age.constructor-impl(20);
          int age19 = Age.constructor-impl(19);
          boolean var2 = Age.isOrderThan-Ue8C4z0(age20, age19);
          System.out.println(var2); // 결과 : ture
       }
    }
    • Person 클래스의 필드로 VO 인 Age age 가 아니라 내부 프로퍼티 타입인 int age 가 선언되어있다.
         private final int age;
    • Age 클래스를 선언해도 타입이 자동으로 내부 프로퍼티의 타입인 int 로 unboxing 되어 할당된다.
      int age20 = Age.constructor-impl(20);
      int age19 = Age.constructor-impl(19);
    • equals(), hashcode(), toString() 메서드가 생성되어있다.

0개의 댓글