인라인 클래스
인라인 클래스는 새로운 타입을 안전하고 최적화된 형식으로 정의하는 방법입니다.
인라인은 프로그래밍 언어에서 주로 함수에 사용되며 컴파일 과정에서 원래 함수로 따로 분리되어 있던 것이 최종 컴파일된 코드에서는 함수를 호출하는 위치에 함수의 본문이 삽입되어 프로그램을 최적화해주는 기술입니다.
Code 비교
fun func(n1: Int, n2: Int): Int {
return n1 + n2
}
fun main() {
val result = func(1, 2)
println(result)
}
inline fun func(n1: Int, n2: Int): Int {
return n1 + n2
}
fun main() {
val result = func(1, 2)
println(result)
}
---> 컴파일된 코드를 Java로 디컴파일 했을 시
public static final int func(int n1, int n2) {
return n1 + n2;
}
public static final void main() {
int result = func(1, 2);
boolean var1 = false;
System.out.println(result);
}
public static final int func(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);
}
Inline이 아닐때는 function이 그대로 남아 있어서 main 함수에서 이를 호출하지만 Inline 함수일 경우에는 function의 로직이 main에 Inline되어 실행시켜주고 있습니다.
Kotlin에서는 보통 위와 같이 단순한 경우에는 Inline을 사용할 필요가 없고 람다를 이용해 함수형 인자를 받아 함수에서 실행시켜줄때 Inline을 이용시 성능적으로 많이 개선이 됩니다.
Kotlin의 원시형 최적화
Kotlin의 원시형인 Int, Double, Float 타입을 Java로 디컴파일 해볼시에는 다음과 같습니다.
//Kotlin
fun main() {
val a: Int = 1
}
//Java
public static final void main() {
int a = true;
}
-> Kotlin 내부에서 Int는 클래스로 정의되지만 컴파일 했을 시는 자바의 원시형 int로 변경됨을 알 수 있습니다.
이는 Kotlin 컴파일러가 컴파일 과정에서 원시형들을 자동으로 최적화해주기 때문입니다.
(Int가 클래스로 동작할 시 많은 성능 손실이 예상되기 때문)
인라인 클래스의 필요성
만약 하나의 타입을 만들어 그 타입만 가질 수 있는 여러가지 동작들을 정의하고 싶다면 어떻게 해야 할까요?
class WrapperClass(private val millis: Long) {
fun toCalendar(): Calendar {
return Calendar.getInstance().also {
it.timeInMillis = millis
}
}
}
Wrapper class를 만들어 메서드를 정의할 수 있습니다.
public static final void main() {
UnixMillis unix = new WrapperClass(System.currentTimeMillis());
Calendar calendar = unix.toCalendar();
}
디컴파일된 자바 코드는 위와 같습니다.
inline class WrapperClass(private val millis: Long) {
fun toCalendar(): Calendar {
return Calendar.getInstance().also {
it.timeInMillis = millis
}
}
}
inline 클래스 선언해주었습니다.
public static final void main() {
long unix = WrapperClass.constructor-impl(System.currentTimeMillis());
Calendar calendar = WrapperClass.toCalendar-impl(unix);
}
그리고 디컴파일된 자바 코드를 확인하면 WrapperClass이 static 함수로 정의된 헬퍼 클래스로 변했습니다.
이처럼 Inline 클래스는 코드를 최적화 해주며 새로운 타입을 만들어내고 안전한 사용법을 강제할 수 있습니다.
Inline 클래스의 Wrapping
Inline 클래스는 다른 자료형을 Wrapping해서 새로운 자료형을 만들 때 많이 사용됩니다.
동일한 Type이 무수히 많고, 이름만 가지고 실수할 여지가 있을 때 사용될 수 있습니다.
data class UserInfoResponse(
val index: Long,
val indexId: Long,
...
)
위 코드에서 index와 indexId가 충분히 헷갈릴 수 있다고 판단되어 inline class를 정의해주어 실수를 줄일 수 있습니다.
inline class Index(val index: Long)
inline class IndexId(val index: Long)
data class UserInfoResponse(
val index: Index,
val indexId: IndexId,
...
)
그렇다면 디컴파일 된 자바코드로 확인 할시 아래와 같게 됩니다.
public final class UserInfoResponse {
private final long index;
private final long indexId;
}
-> inline 클래스는 Primitive type을 기준으로 사용할 수 있고, inline 클래스 코드 내에는 프로퍼티와 함수 정의하는게 가능합니다.
예제
예제로 한 번 더 확인해 봅시다.
interface Human {
fun getId(): Int
}
inline class StudentId(val studentId: Int) : Human {
override fun getId() = studentId
}
inline class TeacherId(val TeacherId: Int)
inline class SchoolId(val schoolId: Int)
class Grades(
val studentId: StudentId,
val TeacherId: TeacherId,
val schoolId: SchoolId
) {
}
fun main() {
val studentId = StudentId(1)
studentId.getId()
val teacherId = TeacherId(2)
val schoolId = SchoolId(3)
val grade = Grades(studentId, teacherId, schoolId)
}
--> 디컴파일된 자바 코드
public static final void main() {
int studentId = StudentId.constructor-impl(1);
StudentId.getId-impl(studentId);
int teacherId = TeacherId.constructor-impl(2);
int schoolId = SchoolId.constructor-impl(3);
new Grades(studentId, teacherId, schoolId, (DefaultConstructorMarker)null);
}
객체의 인스턴스화 없이 StudentId, TeacherId, SchoolId를 타입처럼 사용할 수 있게 됩니다.
Inline 클래스의 프로퍼티는 constructor-impl라는 static 메서드를 통해 받아 오고 있으며 Inline 클래스 안에 선언된 메서드들도 static 메서드 형태로 동작할 수 있게됩니다.
보통 인라인 클래스는 다른 자료형을 wrapping해서 새로운 자료형을 만들 때 많이 사용됩니다.