본문 바로가기
코틀린

코틀린 inline 클래스 와 함수의 정의 및 사용하는 이유, 경우

by 기계공학 주인장 2023. 1. 7.
반응형

인라인(inline) 코드란?

  • 본래라면 컴파일 시 별도의 함수 or 클래스로 만들어져야하는 것을 호출하는 본문 안에서 정의하도록 한 것
  • 인라인 클래스는 주로 Wrapping Class를 만들 때 사용한다
  • Boxing 과정이 빈번하게 이뤄지면 인라인 클래스를 사용하는 의미가 없어진다

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."


인라인 코드는 무엇인가?

본래라면 컴파일 시 별도의 함수 or 클래스로 만들어져야하는 것을 호출하는 본문 안에서 정의하도록 한 것이다.

 

인라인 일때와 아닐 때 코드 비교

  • 인라인을 사용하지 않고 정의한 함수를 호출
fun fn(n1: Int, n2: Int): Int {
    return n1 + n2
}

fun main() {
    val result = fn(1, 2)
    println(result)
}
  • 컴파일 결과
// 코틀린에서 정의한 것과 똑같이 별도의 함수가 생성된다 
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);
}

 

  • 인라인으로 정의한 함수를 호출
inline fun fn(n1: Int, n2: Int): Int {
    return n1 + n2
}

fun main() {
    val result = fn(1, 2)
    println(result)
}
  • 컴파일 결과
public static final int fn(int n1, int n2) {
   int $i$f$fn = 0;
   return n1 + n2;
}

// 정의한 함수를 main 안에 넣어서 실행하준다
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);
}

이처럼 인라인 코드는 정의한 클래스 / 함수를 호출한 곳에 넣어주기 때문에

 

함수형 인자를 받을 때 사용하면 많은 성능 개선이 이루어진다

 

주의사항 및 팁

  • 인라인 함수를 사용하면 컴파일된 코드가 길어진다
  • 대상 함수가 상대적으로 작을 때 더욱 효과적이다
  • 코틀린 표준 라이브러리가 제공하는 여러 고차 함수 중 상당수가 인라인 함수이다
  • 다음과 같은 고차 함수에 적용하면 효과적이다
fun main(args: Array<String>) {
    println(indexOf(intArrayOf(4, 3, 2, 1)){ it< 3}) // 2

    // inline indexOf() 함수가 컴파일된 모습을 보면 아래와 같다
    val number =intArrayOf(4,3,2,1)
    var index = -1

    for (i in number.indices){
        if (number[i] < 3){
            index = i
            break
        }
    }
    println(index) // 2
}

inline fun indexOf(numbers: IntArray, condition: (Int) -> Boolean): Int {
    for (i in numbers.indices) {
        if (condition(numbers[i])) return i
    }
    return -1
}

하지만 인라인 함수는 다음과 같은 제약이 따른다

  • null이 될 수 있는 인자를 받을 수 없다
  • private 인자를 받을 수 없다
  • 캡슐화를 해야할 경우 사용할 수 없다 = 이유는 해당 함수가 클래스 안으로 들어가기 때문이다

인라인 클래스는 왜 필요할까?

인라인 클래스는 주로 Wrapping Class를 만들 때 사용한다

 

예를 들어 UnixMillis같은 기본적으로 Long 형이지만 이를 java.util.Calendar로 변환 한다던가 날짜와 시간에 대한 변환을

 

하는 작업을 하고싶다고 가정한다

 

첫 번째 방법은 일반적인 Wrapper Class를 만드는 것이다.

class UnixMillis(private val millis: Long) {
    // 받은 mills 데이터를 Calendar로 변환한다
    fun toCalendar(): Calendar {
        return Calendar.getInstance().also {
            it.timeInMillis = millis
        }
    }
}

이를 디컴파일 하면 다음과 같은 자바 코드를 얻을 수 있다

public static final void main() {
	 // UnixMillis 객체를 만들고
   UnixMillis unix = new UnixMillis(System.currentTimeMillis());
	 // 해당 객체를 사용해서 Calendar에 대한 변환을 실시한다
   Calendar calendar = unix.toCalendar();
	 // 이는 매번 Calendar 객체를 만들기 때문에 최적화된 방법이라 할 수 없다
}

이러한 방법은 toCalendar() 함수를 사용할 때 마다 객체를 만들기 때문에 부담이 간다

 

이번에는 인라인 클래스로 만들어서 확인한다

// 그냥 앞에 inline만 붙이고 끝
inline class UnixMillis(private val millis: Long) {
    fun toCalendar(): Calendar {
        return Calendar.getInstance().also {
            it.timeInMillis = millis
        }
    }
}

이를 디컴파일 하여 자바 코드로 바꾸면 다음과 같다

public static final void main() {
	// UnixMillis 객체가 아닌 Long 객체를 만듬
	// 또한 파라미터를 주생성자(constructor)로 전달하는 것을 알 수 있다
   long unix = UnixMillis.constructor-impl(System.currentTimeMillis());
   
	// 호출한 함수 내의 코드가 그대로 가져옴(최적화만 했음)
   Calendar calendar = UnixMillis.toCalendar-impl(unix);
}

인라인 클래스로 사용하면 매번 객체를 만들지 않는 것을 확인할 수 있다.

 

주의사항

  1. 인라인 클래스는 주 생성자로 한 개의 인자만 가져야하며 이를 var나 val을 붙여줘야한다
  2. 인라인 클래스는 init을 가질 수 없음
  3. backing field(ex: lateinit, delegated 등)을 가질 수 없음
  4. 코틀린 1.5 이후에는 value 클래스로도 사용할 수 있다
  5. JVM 백엔드의 경우에는 반드로 @JvmInline을 붙여줘야한다

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

앞에서 말했다시피 인라인 클래스는 Wrapping Class를 만들 때 주로 사용된다

 

Wrapping Class는 사용될 때 내부에 있는 값을 boxing, unboxing 한다.

 

아래의 인라인 클래스로 예시를 들어보면 

inline class Password(val value: String)
  • Boxing은 value 를 Password객체로 만들어 저장한다는 의미
  • Unboxing은 Password에서 value를 가져온다는 의미이다
  • 즉, Boxing 과정이 빈번하게 이뤄지면 인라인 클래스를 사용하는 의미가 없어진다

 

Boxing을 여러 번 하는 예시

interface I
inline class Foo(val i: Int) : I
fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}
// id 함수는 그냥 받은 값을 그대로 반환하는 함수임
fun <T> id(x: T): T = x

fun main() {
    val f = Foo(42)
    
    // unboxed: f가 Foo의 객체이기 때문에 그대로 사용
    // 그렇기 때문에 f를 사용할 때 다시 Boxing할 필요 없이 바로 사용 가능
    asInline(f)

    // 기본적으로 함수 인자로 인라인 클래스 객체를 그대로 받지 않는 경우에는 전부 
    // 한 번씩 boxing을 한 다음에 다시 unboxing을 해야한다

    // boxed: 제네릭 타입 T를 사용하여 Foo 객체를 받기 때문에 Boxing 필요
    // 그렇기 때문에 한 번 Boxing을 하고 다시 Unboxing을 해야 f를 사용 가능
    asGeneric(f)

    // boxed: Foo가 상속받은 I를 인자로 받는 함수이기 때문에 Boxing 필요
    asInterface(f)

    // boxed: null을 받을 수 있는 경우에도 Boxing을 한 다음에 Unboxing을 실시해야한다
    asNullable(f)
    
    // 'f'를 넣을 때 한 번 boxing 한다 ('id' 함수로 보내질 때)
    // 그리고 'id' 함수에서 값을 반환할 때 Unboxing 한다
    // 마지막에 'c' 는 unboxed된 f 값을 가지게된다
    val c = id(f)  
}

 

반응형


"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."


댓글