시배's Android

Kotlin vs Java | Open Class, Value Class는 자바로 어떻게 구현될까? 본문

Kotlin/Kotlin vs Java

Kotlin vs Java | Open Class, Value Class는 자바로 어떻게 구현될까?

si8ae 2024. 2. 6. 20:09

Open Keyword

Kotlin에서 클래스나 메서드를 상속 가능하게 하려면 open 키워드를 사용해야 합니다. 이는 Liskov Substitution Principle(LSP)를 준수하기 위한 것입니다.

LSP는 서브타입(subtype)이 슈퍼타입(super type)을 대체할 수 있어야 한다는 원칙으로, 이를 위해서는 서브타입에서는 슈퍼타입의 모든 규칙을 따라야 합니다.

예를 들어, Animal 클래스를 살펴봅시다.

open class Animal {
    open fun bark(){
        println("animal")
    }

    fun test(){
        println("test")
    }
}

 

여기서 open 키워드는 이 클래스를 상속 가능하게 만듭니다. bark() 메서드 또한 open 키워드를 통해 서브클래스에서 오버라이딩이 가능하도록 합니다.

class Cat : Animal() {
    override fun bark() {
        println("cat")
    }

    // test() 메소드는 상속 불가능
}

Cat 클래스는 Animal을 상속받아 bark() 메소드를 오버라이딩합니다. 하지만 test() 메소드는 open 키워드가 없으므로 상속이 불가능합니다.

// Cat.java
import kotlin.Metadata;

@Metadata(
   mv = {1, 9, 0},
   k = 1,
   d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\b\u0010\u0003\u001a\u00020\u0004H\u0016¨\u0006\u0005"},
   d2 = {"LCat;", "LAnimal;", "()V", "bark", "", "compareJava"}
)
public final class Cat extends Animal {
   public void bark() {
      String var1 = "cat";
      System.out.println(var1);
   }
}
// Animal.java
import kotlin.Metadata;

@Metadata(
   mv = {1, 9, 0},
   k = 1,
   d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\b\u0002\b\u0016\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\b\u0010\u0003\u001a\u00020\u0004H\u0016J\u0006\u0010\u0005\u001a\u00020\u0004¨\u0006\u0006"},
   d2 = {"LAnimal;", "", "()V", "bark", "", "test", "compareJava"}
)
public class Animal {
   public void bark() {
      String var1 = "animal";
      System.out.println(var1);
   }

   public final void test() {
      String var1 = "test";
      System.out.println(var1);
   }
}
// MainKt.java
import kotlin.Metadata;

@Metadata(
   mv = {1, 9, 0},
   k = 2,
   d1 = {"\u0000\b\n\u0000\n\u0002\u0010\u0002\n\u0000\u001a\u0006\u0010\u0000\u001a\u00020\u0001¨\u0006\u0002"},
   d2 = {"main", "", "compareJava"}
)
public final class MainKt {
   public static final void main() {
      Cat cat = new Cat();
      cat.bark();
   }

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

이를 Java로 디컴파일하면 open 키워드가 붙은 Animal 클래스와 bark() 메서드는 final 키워드가 붙지 않은 것을 확인할 수 있습니다. 이는 Kotlin이 기본적으로 상속을 허용하지 않고, 명시적으로 open 키워드를 사용하여 허용하도록 하는 특징입니다.

Kotlin은 이러한 설계로 LSP를 보다 엄격하게 지키면서 상속을 제어하고자 합니다. 이로 인해 코드의 안정성과 일관성이 높아지며, 서브클래스에서의 오버라이딩을 명시적으로 지정함으로써 예상치 못한 동작을 방지할 수 있습니다.

이렇게 Kotlin은 open 키워드를 통해 상속을 엄격하게 제어하고 LSP를 준수하여 안정성을 높이는 특징을 갖고 있습니다.

Value class(Inline class)

Kotlin에서 제공하는 inline class는 값 타입을 표현하기 위한 특별한 클래스로, 해당 클래스를 사용하면 런타임에서의 객체 생성을 피하고 성능을 최적화할 수 있습니다. 이러한 inline class를 Java로 디컴파일해보면 어떻게 구현되는지 알아보겠습니다.

@JvmInline
value class Id(
    val id: Long
)

 

위 코드에서 Id는 value class(inline class)로 선언되었고, @JvmInline 어노테이션은 이를 Java에서 사용할 때의 특수한 구현을 나타냅니다. 이를 디컴파일한 Java 코드는 다음과 같습니다.

@JvmInline
public final class Id {
   private final long id;

   public final long getId() {
      return this.id;
   }

   private Id(long id) {
      this.id = id;
   }

   public static long constructor_impl(long id) {
      return id;
   }

   public static final Id box_impl(long v) {
      return new Id(v);
   }

   public static String toString_impl(long var0) {
      return "Id(id=" + var0 + ")";
   }

   public static int hashCode_impl(long var0) {
      return Long.hashCode(var0);
   }

   public static boolean equals_impl(long var0, Object var2) {
      if (var2 instanceof Id) {
         long var3 = ((Id)var2).unbox_impl();
         return var0 == var3;
      }

      return false;
   }

   public static final boolean equals_impl0(long p1, long p2) {
      return p1 == p2;
   }

   public final long unbox_impl() {
      return this.id;
   }

   public String toString() {
      return toString_impl(this.id);
   }

   public int hashCode() {
      return hashCode_impl(this.id);
   }

   public boolean equals(Object var1) {
      return equals_impl(this.id, var1);
   }
}
  1. 메서드 인라이닝: inline class는 생성자를 제외하고 대부분의 메서드가 인라이닝되어 있습니다. 이는 해당 메서드의 내용이 호출되는 곳에 직접 삽입되어 성능을 향상시킵니다.
  2. 자동 생성 메서드: toString_impl, hashCode_impl, equals_impl 등은 Kotlin 컴파일러가 자동으로 생성한 메서드로, Java의 toString(), hashCode(), equals() 메서드를 대체합니다.

이러한 특성들은 Kotlin이 inline class를 사용하여 성능 최적화를 수행하고, 동시에 가독성을 유지하는 데에 기여합니다. 이를 통해 개발자는 값 타입을 사용함으로써 코드의 간결성과 성능 향상을 모두 얻을 수 있습니다.