이번 토픽은 inline class입니다. inline fun은 사용해 봤지만 inline class는 사용해 본적이 없는 것 같아서 주제로 선정하게 되었습니다. 클래스 내부 구조를 살펴볼 때 몇번 본 것 같은데, 왜 사용하는지 알아보겠습니다.
@JvmInline
value class Password(private val s: String)
@JvmInline
어노테이션과 value
키워드를 붙이면 value(inline) 클래스를 선언할 수 있습니다.
@JvmInline
은 KotlinJs, KotlinNative 등 다른 버전과 호환을 위해 존재하는 어노테이션 입니다.
이렇게만 보면 어차피 wrapper 클래스를 사용할거면 data 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.
To solve such issues, Kotlin introduces a special kind of class called an inline class. Inline classes are a subset of value-based classes. They don't have an identity and can only hold values.
공식 문서에 나와 있는 내용을 요약하면 이렇습니다.
비즈니스 로직을 위해 특정 type을 Wrapper로 감싸줘야 할 때가 있는데, 이 때 객체를 생성하기 위해 힙 할당을 하게 됨으로써 런타임 오버헤드가 발생하고 성능 저하가 일어난다는 이야기를 하고 있습니다.
따라서 이를 해결하기 위해 inline 클래스를 사용할 수 있습니다.
솔직히 아직까진 잘 모르겠습니다. 처음부터 차근차근 짚어나가겠습니다.
fun alarm(millis: Long){
print("$millis 밀리 후에 알람이 울립니다.")
}
fun test(){
alarm(2000L)
}
alarm
이라는 함수를 통해 간단히 출력하는 것을 구현해 보았습니다.
여기서 2000L이라는 parameter를 넣어 호출해 보겠습니다.
별 문제 없어 보이는 코드일 수 있습니다. 하지만 2000밀리세컨드는 2초라는 사실을 잘 모르는 경우도 있을 것이고 , 다른 비즈니스 로직을 적용하더라도 소수만 알아 들을 수 있는 어떠한 값이 있을 것입니다.
이러한 문제를 Wrapper Class를 작성함으로써 해결할 수 있습니다.
fun alarm(duration: Duration){
print("{$duration.millis} 밀리 후에 알람이 울립니다.")
}
fun test(){
alarm(Duration.seconds(2))
}
data class Duration private constructor(
val millis: Long,
) {
companion object {
fun millis(millis: Long) = Duration(millis)
fun seconds(seconds: Long) = Duration(seconds * 1000)
}
}
이런 식으로 Duration class
를 통해 2초라는 사실을 바로 확인할 수 있습니다.
그러나, data class
를 사용한다면 매번 alarm을 호출할 때 마다, Duration이라는 객체를 생성하여 할당해 줘야 합니다.
추가 힙 할당으로 생기는 런타임 오버헤드를 방지하기 위해, value라는 modifier를 앞에 붙여주어 inline class를 선언해 보겠습니다.
fun alarm(duration: Duration){
print("{$duration.millis} 밀리 후에 알람이 울립니다.")
}
fun test(){
alarm(Duration.seconds(2))
}
@JvmInline
value class Duration private constructor(
val millis: Long,
) {
companion object {
fun millis(millis: Long) = Duration(millis)
fun seconds(seconds: Long) = Duration(seconds * 1000)
}
}
value class로 선언된 Duration은, 파라미터로 들어올 때, 객체의 프로퍼티로 대체됩니다.
자바로 디컴파일 하면서 무슨일이 일어나고 있는지 확인해 보겠습니다.
public static final void alarm(@NotNull Duration duration) {
Intrinsics.checkNotNullParameter(duration, "duration");
String var1 = duration.getMillis() + " 밀리 후에 알람이 울립니다.";
System.out.print(var1);
}
자바로 디컴파일 했을 때, data class
로 선언된 Duration
의 경우, 객체 자체를 받아서 객체 내의 프로퍼티를 가져오고 있습니다.
public static final void alarm_ZlhV6Ys(long duration) {
String var2 = duration + " 밀리 후에 알람이 울립니다.";
System.out.print(var2);
}
fun alarm(duration: Duration)
분명히 코틀린 코드는 Duration
을 받고 있으나, 자바로 디컴파일 했을 때 위와 같이 Duration
내의 프로퍼티를 가져오고 있습니다.
여기서 디컴파일 시 _ZlhV6TYs
처럼 특정한 문자열을 붙이는 것을 맹글링이라고 합니다.
@JvmInline
value class People(val name: String){
fun f(people: People) {}
fun f(name: String) {}
}
위와 같이 메서드 오버로딩을 적용했을 때, 맹글링이 적용되지 않는다면 디컴파일 하였을 때 메서드 이름과 parameter가 중복이 됩니다.
이처럼, 컴파일러가 자동으로 최적화를 진행해 주기 때문에, 객체를 할당하지 않아도 Wrapper 클래스를 통해 가독성을 높일 수 있습니다.
equals()
, toString()
, hashCode()
메서드만 생성(data class는 copy()
, componentN()
까지 생성)
=== 연산을 지원하지 않음
val 프로퍼티만 허용
결론은 어떠한 데이터를 Wrapper 클래스로 감쌀 때, value class를 사용하자가 되겠습니다..!
참고 자료
https://kotlinlang.org/docs/inline-classes.html
https://velog.io/@dhwlddjgmanf/Kotlin-1.5에-추가된-value-class에-대해-알아보자
컴파일러 수업을 들으셨나요? 그리고 2000밀리는 2000ml 같으니 2000밀리세컨드라구 정정 부탁드립니다