코틀린 인라인 클래스란? 💍

코틀린의 인라인 클래스의 필요성과 사용법

MJ Studio
MJ Studio

--

An inn in a fantasy world

해당 글은 1.3에 쓰인 것으로 인라인 클래스가 알파버전일 때입니다.

Kotlin 1.5에서는 stable release가 되고 문법도 변했으니 이 글은 Deprecated 되었다고 봐도 무방합니다.

인라인 클래스는 코틀린 1.3부터 알파버전으로 도입된 새로운 타입을 안전하고 최적화된 형식으로 정의하는 방법입니다. 인라인 클래스를 알아보기 전에, 인라인과 코틀린의 원시형 컴파일러 최적화에 대해 잠깐 살펴보고 인라인 클래스의 필요성과 사용법에 대해 알아보겠습니다.

References

코틀린 공식문서

인라인, 인라인 함수

인라인이란 프로그래밍 언어에서 주로 함수에 사용되며 컴파일 과정에서 원래 함수로 따로 분리되어 있던 것이 최종 컴파일된 코드에서는 함수를 호출하는 위치에 함수의 본문이 삽입되어 프로그램을 최적화해주는 테크닉입니다.

한번 코틀린에서 실제로 인라인 함수를 만들어보겠습니다

fun fn(n1: Int, n2: Int): Int {
return n1 + n2
}


fun main() {
val result = fn(1, 2)
println(result)
}

이렇게 기본 함수 fn 으로 호출을 했을 때와,

inline fun fn(n1: Int, n2: Int): Int {
return n1 + n2
}


fun main() {
val result = fn(1, 2)
println(result)
}

fn 을 인라인 함수로 바꿔서 호출을 했을 때, 코틀린 바이트코드를 다시 자바로 디컴파일한 결과물을 비교해보겠습니다

인라인이 아닐 때

public static final int fn(int n1, int n2) {
return n1 + n2;
}

public static final void main() {
int result = fn(1, 2);
boolean var1 = false;
System.out.println(result);
}

인라인일 때

public static final int fn(int n1, int n2) {
int $i$f$fn = 0;
return n1 + n2;
}

public static final void main() {
byte n1$iv = 1;
int n2$iv = 2;
int $i$f$fn = false;
int result = n1$iv + n2$iv;
boolean var4 = false;
System.out.println(result);
}

인라인이 아닐 때는 fn 이 그대로 남아있어서 main 함수에서 이를 호출하지만, 인라인일 때는 fn 에서 하는 일을 main 에서 인라인되어 실행시켜 줌을 확인할 수 있습니다. 코틀린에서는 보통 위와 같이 단순한 경우에는 inline을 사용할 필요가 없고 함수형 인자를 받아 함수에서 실행시켜 줄 때나 건내줄 때 inline이 성능적으로 많이 개선이 됩니다.

코틀린의 원시형 최적화

코틀린의 원시형인 Int, Double, Float 같은 타입들은 자바로 디컴파일 해보면 어떻게 될까요? 바로 다음과 같습니다.

코틀린
fun main() {
val a: Int = 1
}
자바
public static final void main() {
int a = true;
}

코틀린내부에서 Int는 클래스로 정의되지만 컴파일된 코드는 자바의 원시형 int가 되었습니다. 이는 코틀린 컴파일러가 컴파일 과정에서 원시형들을 자동으로 많은 부분을 최적화해주기 때문입니다. 만약 실제로 Int 가 클래스 그 자체로 동작하게 된다면 굉장히 많은 성능 손실이 있을 것입니다.

인라인 클래스의 필요성

만약 우리가 하나의 타입을 만들어 그 타입만 가질 수 있는 여러가지 동작들을 정의하고 싶으면 어떻게 하면 될까요? 예를 들어, Unix timestamp는 기본적으로 Long 형이지만, 이를 java.util.Calendar로 변환한다든지, 날짜와 시간에 관련된 작업을 해주고 싶다고 해보겠습니다. 총 세가지의 방법을 소개할 것입니다.

typealias

첫 방법은 코틀린의 typealias 키워드를 사용하는 것입니다. 이러면 어떤 타입에 다른 별칭을 붙여서 직관적으로 사용할 수 있습니다.

typealias UnixMillis = Long

fun UnixMillis.toCalendar(): Calendar {
return Calendar.getInstance().also {
it
.timeInMillis = this
}
}

fun main() {
val unix: UnixMillis = System.currentTimeMillis()
val calendar: Calendar = unix.toCalendar()
}

확장함수로 toCalendar 를 정의하면 UnixMillis 타입이 Calendar 로 확장함수를 이용해서 변환될 수 있음을 알 수 있습니다.

그러나 이 방법의 단점은 곧바로 드러납니다. 다음과 같은 코드를 보시죠.

(1L).toCalendar() // OK

UnixMillis 타입이 아닌 Long 타입에서도 그대로 toCalendar 를 호출할 수 있습니다. typealias 는 새로운 타입을 만드는게 아닌 그저 Long 에 또 다른 이름을 붙인것에 불과하기 때문입니다. 실제로 바이트코드를 다시 자바로 디컴파일 해보면 다음과 같이 생겼습니다.

long unix = System.currentTimeMillis();

그냥 long 형을 쓴 것과 다른것이 없습니다.

사실 Long 형도 toCalendar 를 호출할 수 있는게 무슨 문제냐? 할수도 있겠지만, 참고한 다른 포스팅에서는 Double 형의 typealias섭씨와 화씨에 대해서 다루는데, 이는 섭씨를 쓸 수 있는 곳에 화씨도 쓸 수 있는 상황이 되기 때문에 코딩할 때 버그를 만들어내기 쉬울 것입니다.

Wrapper class

두 번째 방법은 Wrapper 클래스를 만드는 것입니다.

class UnixMillis(private val millis: Long) {
fun toCalendar(): Calendar {
return Calendar.getInstance().also {
it.timeInMillis = millis
}
}
}

이렇게 되면 어떤가요? 이제 더 이상 Long 따위의 타입에서 toCalendar를 호출할 수는 없을 것입니다. 그러나 이는 디컴파일된 자바 코드에서도 new UnixMillis 생성자를 이용해 계속 객체를 생성해주기 때문에 최적화된 방법이라고 할 수 없습니다.

디컴파일된 자바 코드는 다음과 같습니다.

public static final void main() {
UnixMillis unix = new UnixMillis(System.currentTimeMillis());
Calendar calendar = unix.toCalendar();
}

inline class

이제 위의 Wrapper class 구현체 앞에 inline 하나만 붙여보겠습니다.

inline class UnixMillis(private val millis: Long) {
fun toCalendar(): Calendar {
return Calendar.getInstance().also {
it.timeInMillis = millis
}
}
}

그리고 디컴파일된 자바 코드를 볼까요?

public static final void main() {
long unix = UnixMillis.constructor-impl(System.currentTimeMillis());
Calendar calendar = UnixMillis.toCalendar-impl(unix);
}

놀랍게도 UnixMillis 자료형은 실제로 쓰이지않고 기존 Wrapper class에서 사용되던 함수들이 static 함수로 정의된 헬퍼 클래스로 변했습니다. 그 증거로 UnixMillis 에서 constructor-impl,toCalendar-impl 같은 유틸리티 함수들이 쓰이고 long 자료형이 그대로 사용되었습니다.

이처럼 인라인 클래스는 코드를최적화해주며 새로운 타입을 만들어내고 안전한 사용법을 강제할 수 있습니다.

인라인 클래스의 Wrapping

인라인 클래스가 감싸고있는 값은 주생성자로 전달할 수 있으며, 그러기때문에 인라인 클래스는 주 생성자로 한 개의인자만 가져야 하고 이를 varval을 붙여 속성으로 만들어주어야 합니다.

inline class Password(val value: String)

인라인 클래스의 제약

코틀린의 인라인 함수에도 논로컬 반환같은 상황이 만들어지기 때문에 그를 해결하기 위한 crossinline 같은 키워드들이 존재합니다. 인라인 클래스에서도 클래스 자체가 인라인화 되면서 여러가지 제약이 걸리게 됩니다.

  • init 블록을 가질 수 없음
  • 속성들이 backing 필드를 가질 수 없음(lateinit, delegated 속성들도)

인라인 클래스를 함부로 사용하면 안되는 이유

인라인 클래스는 Wrapping 클래스라고 이야기 했습니다. Wrapping 클래스는 내부에 존재하는 값을 boxing, unboxing을 합니다.

inline class Password(val value: String)

위와 같은 인라인 클래스에서 boxing 한다는 것은 valuePassword 객체로 만들어 저장한다는 것이고 unboxing은 Password에서 value를 가져온다는 것이겠죠.

즉, boxing 과정이 빈번하게 일어나게 되면 이 과정에서 객체를 새롭게 생성하기 때문에 인라인 클래스를 사용하는 이유가 없어집니다.

다음과 같은 공식 문서의 예제를 보겠습니다.

interface Iinline class Foo(val i: Int) : Ifun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}
fun <T> id(x: T): T = xfun main() {
val f = Foo(42)

asInline(f) // unboxed: used as Foo itself
asGeneric(f) // boxed: used as generic type T
asInterface(f) // boxed: used as type I
asNullable(f) // boxed: used as Foo?, which is different from Foo

// below, 'f' first is boxed (while being passed to 'id') and then unboxed (when returned from 'id')
// In the end, 'c' contains unboxed representation (just '42'), as 'f'
val c = id(f)
}

Foo 라는 인라인 클래스를 만들고, 그것의 객체인 f를 생성해서 여러 use-case에서 사용하는 모습입니다. 여기서 boxing이 일어나지 않는 것은 Foo 타입을 그대로 인자로 받는 asInline 함수 하나입니다.

즉, 쉽게 생각했을 때, boxing, unboxing이 일어나야 되는 조건이 아니여야만 인라인된 속성이 그대로 전달된다는 것입니다. 제네릭이나 타입이 Nullability 로 사용될 때도 boxing이 일어납니다.

Foo는 인터페이스 I를 구현하지만, 내부적으로 갖는 IntI를 구현하는 것은 아니기 때문에 I객체를 전달해줘야 하는 asInterface 함수에서는 boxing이 일어나게 됩니다.

id 함수는 더 심각합니다. 인라인 클래스를 사용하지 않으면 그저 인자로 전달되었다가 반환되는 함수로 보이지만, 인라인 클래스가 사용되면 boxing이 되었다가 unboxing이 어 반환되기 때문에 심각한 성능상 이슈가 발생합니다.

이와 관련해서 더 이슈가 일어날 수 있는 상황은 다음과 같은 글의 두 번째 섹션에서 소개합니다.

또한 위 글에서 소개하는 (ViewPadding.kt)(ViewMargin.kt) 는 아주 유용한 인라인 클래스를 이용한 유틸리티이니 사용해보시면 좋겠습니다.

결론

코틀린은 빠르게 발전하는 제가 써본 언어중(많이 안써봤습니다)가장 편리하고 강력한 언어입니다. 아직 인라인 클래스는 실험 🙌단계의 기능이지만 이러한 새로운 기능들을 빠르게 익혀가며 코틀린을 공부하는 즐거움을 가지면 좋을 것 같습니다.

감사합니다.

--

--