Kotlin 1.6 버전부터 value class가 도입되었습니다.
이전 버전에서는 data class를 통해 유사한 효과를 얻을 수 있었지만, 데이터 클래스는 추가적인 객체 생성과 메모리 할당이 필요한 문제가 있었습니다.
value class가 추가되면서, 객체 생성 및 메모리 할당을 최소화하고 성능을 개선할 수 있게 되었습니다.
Kotlin 1.5에서는 inline class라는 키워드를 사용하였지만 1.6 버전으로 업그레이드되면서 value class가 등장하고 inline class는 Deprecated 되었습니다.
그 이유는 Kotlin 팀에서는 inline class를 대신하기 위해 value class를 도입하였기 때문입니다.
해당 포스팅에서는 value class가 무엇인지와 장점에 대해 알아보고, 잘 사용하던 data class를 두고 왜 value class를 사용해야 하는지 차이점과 함께 살펴보겠습니다.
Value Class란?
value class는 이름 그대로 값 클래스입니다.
primitive 타입이나 다른 value class를 감싸는 래퍼 클래스로서의 역할을 할 수 있습니다.
@JvmInline
value class Password(private val s: String)
위와 같은 형태로 value class를 정의할 수 있습니다.
여기서 @JvmInline 어노테이션은 Kotlin 1.3부터 도입된 어노테이션으로, inline class 또는 value class를 Java코드에서도 사용할 수 있도록 해줍니다.
추가로, value class에는 몇 가지 특징이 있습니다.
1. 기본적으로 final 형태이며 open으로 변경할 수 없어 상속이 불가능합니다.
2. === 비교 연산이 불가능하며, 오직 == 연산으로 값을 비교합니다.
3. toString(), equals(), hashcode() 메서드가 자동으로 구현됩니다.
4. 하나의 프로퍼티만을 감쌀 수 있습니다. (현재 까지는 2개 이상 감쌀 수 없습니다.)
그렇다면 왜 value class를 사용하는 것일까요?
기존 class 앞에 value 키워드를 추가해주기만 해도, 해당 클래스가 JVM 컴파일러에 의해 최적화 대상이 되므로, 메모리 사용량에 많은 이점을 얻을 수 있기 때문입니다.
@JvmInline
value class Password(val password: String) {
init {
println("패스워드 생성!")
}
}
fun login(password: Password) {
// do something
}
위 코드에서는 value class가 선언되어 있고, 해당 클래스의 객체를 login이라는 메서드에서 인자로 받고 있습니다.
여기서 login() 메서드를 Decompile을 통해 아래와 같은 결과를 얻을 수 있습니다.
public final void login_OZPmQGc(@NotNull String password) {
Intrinsics.checkNotNullParameter(password, "password");
}
디컴파일 결과, login 뒤에 _와 함께 알 수 없는 문자열(맹글링)이 추가되었으며, Password가 아닌, String을 인자로 받도록 바뀌었습니다.
즉, value class를 사용하면, 컴파일러가 객체가 아닌 primitive 타입 변수를 전달받도록 최적화됩니다.
따라서, 더 이상 객체를 사용하지 않으므로 Password 객체가 힙 메모리 영역을 차지하지 않아도 된다는 장점이 있습니다.
맹글링(Mangling)
맹글링(Mangling)이란, 컴파일러가 이름 충돌을 방지하기 위해 함수나 변수의 이름을 변형하는 과정을 의미합니다.
OOP 특징 중 하나인 다형성의 대표적인 Overload는 이름은 중복되지만 다른 인자, 다른 반환 타입을 가질 수 있습니다.
이때, 이름이 중복되는 현상을 방지하기 위해, 컴파일러는 맹글링이라는 과정을 거칩니다.
// Overload
fun foo(num1: Int, num2: Int): Int
fun foo(num1: Float, num2: Float): Float
fun foo_<hashcode>
예를 들어, 위와 같은 코드가 있을 때 컴파일러는 맹글링을 통해 foo_ffiii(), foo_iifff() 등으로 메서드명을 변환합니다.
이러한 이유로 login() 메서드가 login_OZPmQGc()와 같은 형태로 변경된 것입니다.
@JvmName("fooInt")
fun foo(num1: Int, num2: Int): Int
@JvmName("fooFloat")
fun foo(num1: Float, num2: Float): Float
만약 수동으로 맹글링을 비활성화해주고 싶다면, 위 코드처럼 @JvmName 어노테이션을 사용할 수 있습니다.
그러면 맹글링을 하지 않으며, Java에서 해당 메서드를 호출할 때 fooInt(), fooFloat()과 같은 형태로 호출이 가능합니다.
constructor_impl()
@JvmInline
value class Password(val password: String) {
init {
println("패스워드 생성!")
}
}
// Decompile
@NotNull
public static String constructor_impl(@NotNull String password) {
Intrinsics.checkNotNullParameter(password, "password");
String var1 = "패스워드 생성!";
System.out.println(var1);
return password;
}
value class를 decompile 하였을 때 확인할 수 있는 특징이 하나 더 있습니다.
자체적으로 constructor_impl()이라는 static 메서드를 생성한다는 점입니다.
코드를 확인해 보면 init 초기화 블록 내부 로직을 수행한 후, password 변수를 반환하고 있습니다.
이 방식은 객체를 생성하지 않기 위해 컴파일러가 의도적으로 생성한 코드입니다.
fun superLogin() {
login(Password("1234"))
}
// Decompile
public final void superLogin() {
login-OZPmQGc(Password.constructor-impl("1234"));
}
예를 들어 login() 메서드를 호출하기 위한 superLogin() 메서드를 추가로 만들었다고 가정해 보겠습니다.
여기서 login() 메서드를 호출하면서 Password 객체를 전달하고 있지만, Decompile을 해보면 객체를 생성하지 않고 Password의 constructor_impl() 메서드를 호출하고 있습니다.
즉, 기존에 생성자를 호출하면서 호출되던 init 로직을 수행하면서, 객체는 생성하지 않고 값만 반환하는 구조를 따르고 있습니다.
value class가 객체를 사용하지 않기 위한 하나의 방식이라고 할 수 있습니다.
Value Class와 Data Class의 차이점
1. 자동 생성하는 메서드가 다릅니다.
Data Class : toString(), equals(), hashcode(), copy(), componentN()
Value Class : toString(), equals(), hashcode()
2. === 연산을 지원하지 않습니다.
Data Class는 == 연산과 === 연산을 지원하는 반면,
Value Class는 === 연산은 지원하지 않습니다.
따라서 객체를 비교할 때, hascode()와 equals()를 통해 비교하므로 내부 프로퍼티의 값이 같다면 True를 반환합니다.
3. var 프로퍼티를 허용하지 않습니다.
Data Class의 프로퍼티는 val, var 프로퍼티를 지원하는 반면,
Value Class는 val 프로퍼티만 지원하기 때문에 항상 불변성을 보장할 수 있습니다.
4. 하나의 프로퍼티만 가질 수 있습니다.
Data Class는 프로퍼티 개수에 제한이 없지만,
Value Class는 오직 하나의 프로퍼티만을 가질 수 있습니다.
+
테스트 코드를 작성할 때, @ParameterizedTest와 @MethodSource를 사용하는 경우, value class 객체를 전달하면 type convert와 관련된 오류가 발생할 수 있습니다.
이는 JUnit 팀에서 아직 value class에 대한 테스트 코드 대책을 내지 않은 상황이기 때문입니다.
따라서, 경우에 따라 위 어노테이션을 대체하는 방식을 사용하기를 권장합니다.
참조 문서
https://kotlinlang.org/docs/inline-classes.html
해당 포스팅 내용에 오류가 있다면 댓글로 남겨주세요 :)
github : https://github.com/tmdgh1592
'Programming > Kotlin' 카테고리의 다른 글
[Kotlin] Jvm Prefix Annotation 5가지 파헤치기 (1) | 2023.04.05 |
---|---|
[Kotlin] val a: Int = 1000과 val b: Int = 1000은 다르다 (2) | 2023.02.20 |
[Kotlin] const val vs val - 둘의 차이점은 무엇일까? (0) | 2023.02.11 |