[Kotlin] value class로 값을 감싸보자
다음 글을 요약했음을 밝힙니다.
코드를 작성하다 보면, primitive 타입을 감싸는 wrapper 클래스가 필요할 때가 있다. 예를 들어 도형의 너비를 ``Int``로 직접 나타내는 대신 ``Width`` 클래스로 나타내는 것이다. 물론 ``Width``는 내부적으로 ``Int`` 값을 가지고 있다.
class Width(val value: Int)
그러나 위 코드처럼 하면 런타임 성능이 크게 나빠진다. 객체를 만들 때 힙 메모리를 할당하기 때문이다. 게다가 위 코드처럼 primitive 타입을 감싸면 성능이 더 나빠진다. Primitive 타입은 가장 많이 쓰이는 만큼 최적화도 잘 되어 있는데, wrapper 클래스는 완전히 새로운 클래스인 만큼 최적화가 전혀 안 되어있기 때문이다.
Inline class
Kotlin에는 이러한 문제를 해결하는 inline class가 존재한다. 다음과 같이 inline class를 선언할 수 있다.
value class Width(private val value: Int)
``inline class``가 아님에 주의하자. JVM에서는 ``@JvmInline`` 어노테이션을 추가해야 한다.
@JvmInline
value class Width(private val value: Int)
이제 ``Width`` 객체를 만들어 보자. 코드상으로는 ``Width``가 생성되지만, 실제 런타임에서는 Java primitive ``int``가 선언된다.
val width = Width(200) // 바이트코드에서는 int가 선언된다.
여기까지가 inline class의 핵심 기능이다. Inline이라는 이름이 잘 어울리는 기능이다.
멤버 함수?
Inline class에도 ``init`` 블럭과 함수, property를 선언할 수 있다.
@JvmInline
value class Width(private val value: Int) {
init {
require(value > 0) { }
}
val halfWidth: Int
get() = vaule / 2
fun printValue() = prnitln("Width is $value")
}
``require`` 함수는 조건문의 결과가 ``false``일 때 ``IllegalArgumentException``을 던진다. 블럭 안에 exception 메시지를 작성할 수도 있다. Inline class의 핵심 예시 중 하나로, ``require``를 잘 활용하면 조건에 맞는 값만을 받아들일 수 있다.
``lateinit`` 또는 위임된 property는 선언할 수 없다.
상속
Inline class는 인터페이스를 구현할 수는 있으나, 클래스의 위계질서에 끼어들 수는 없다. 부모 클래스도, 자식 클래스도 될 수 없다.
Inline class vs. type alias
C의 ``typedef``처럼 Kotlin에서도 ``typealias`` 키워드를 사용하여 타입에 새로운 이름을 부여할 수 있다.
typealias WidthType = Int
val width: Widthtype = 100
언뜻 보면 ``typealias``와 inline class는 비슷해 보인다. 새로운 타입을 만들고, 런타임에서는 자신이 실제로 표현하는 타입으로 치환되기 때문이다.
그러나, 둘 사이에는 결정적인 차이점이 있다. ``typealias``는 타입에 또 다른 이름을 부여한다. 반대로 inline class는 완전히 새로운 타입을 선언한다. 그 증거로 `` Width`` 타입의 값을 ``Int`` 타입의 변수에 대입할 수 없으며, 그 반대도 불가능하다.
물론 컴파일된 바이트코드는 같지만, 적어도 코드에서는 완전히 다른 개념이다.
위에서 예시로 든 ``Width``처럼 특정 개념을 무조건 inline class로 나타내야 한다고 말하는 사람도 있다. 값의 범위가 매우 중요하다면 그렇게 할 수도 있겠으나, primitive 타입을 무조건 배척할 이유도 없다. 직접 사용해 보고 뭐가 더 나은지 결정하자. 항상 유연함을 유지해야 한다.