[Kotlin] Inline-Funtions 과 Reified 사용 이유

Jae Eon·2021년 12월 12일
2

Kotlin 공부

목록 보기
5/6

실무를 하다가 inline이 사용된 메소드를 발견하게 되어 뭐하는 명령어인가.. 궁금해져 정리한 포스트입니다.

🍎 Inline-Funtions 들어가기

인라인(inline) 키워드는 자바에서는 제공하지 않는 코틀린만의 키워드이다.

코틀린 공식문서의 인라인 펑션 (inline 공식문서)을 보면,
코틀린에서 고차함수(High order functions, 함수를 인자로 전달하거나 함수를 리턴)를 사용하면 패널티가 발생한다고 나와있다.

패널티란 추가적인 메모리 할당 및 함수호출로 Runtime overhead가 발생한다는 것으로, 람다를 사용하면 각 함수는 객체로 변환되어 메모리 할당과 가상 호출 단계를 거치는데 여기서 런타임 오버헤드가 발생한다.

inline functions는 내부적으로 함수 내용을 호출되는 위치에 복사하며, Runtime overhead를 줄여준다.

여기서 내부적으로 함수 내용을 호출하는 위치에 복사된다는게 어려울 수도 있지만 아래의 설명을 보면 이해 할 수있을것 같다.


🍊 객체로 변환되는 오버헤드?

아래와 같은 고차함수(함수를 인자로 전달하거나 함수를 리턴하는 함수)를 컴파일 하면 자바 파일이 생성됩니다.

// 앞으로 사용할 메소드(함수)
fun doSomethingElse(lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}

아래 컴파일된 자바 코드는 Functional Interface인 Function 객체를 파라미터로 받고 invoke 메서드를 실행합니다.

{ 여기서 invoke란 코틀린 표준 라이브러리는 메소드 시그니처에 따라(변수의 개수에 따라 22까지 정의됨) 인터페이스가 생성되어있고,
내부에 invoke() 메서드에 람다식을 구현하고 해당 객체를 파라미터로 사용한다.}

public static final void doSomethingElse(Function0 lambda) {
    System.out.println("Doing something else");
    lambda.invoke();
}

그러면 이제 위에서 선언한 메소드를 이용해 다음과 같은 메소드를 만들어 보겠습니다.
doSomethingElse를 실행 하기전 출력문을 실행 후
함수를 호출하며 파라미터로 { println("Inside lambda") } 람다식을 넣었습니다.

// 1번 메소드
fun doSomething() {
    println("Before lambda")
    
    doSomethingElse {
        println("Inside lambda")
    }
    
    println("After lambda")
}

위 1번 메소드를 자바 코드로 컴파일 하면

// 1번 메소드의 자바 컴파일 코드
public static final void doSomething() {
    System.out.println("Before lambda");
    
    doSomethingElse(new Function() {   // "<---여기가 문제임 "
            public final void invoke() {
            System.out.println("Inside lambda");
        }
    });
    
    System.out.println("After lambda");
}

위 1번코드가 컴파일 된 코드를 보면 문제점이 있는데,
파라미터로 매번 새로운(new Function() )객체를 만들어 넣어준다는 것이다.
이렇게 의미없이 객체로 변환되는 코드가 바로 객체로 변환되는 오버헤드이자 패널티이다.


🍑 Inline-Funtions 으로 오버헤드 해결하기

메소드 앞에 inline를 붙여 주었다.

// inline을 붙인 2번 메소드
inline fun doSomethingElse(lambda: () -> Unit) {
   println("Doing something else")
   lambda()
}
// 2번 메소드의 자바 컴파일 코드
public static final void doSomething() {
    System.out.println("Before lambda");
    System.out.println("Doing something else");
    System.out.println("Inside lambda");
    System.out.println("After lambda");
}

위 자바 컴파일 코드를 보면 새로운 객체를 생성하는 부분이 사라지고
System.out.println("Doing something else");
System.out.println("Inside lambda");
두 코드로 변경된 것을 알 수있다.

추가로 람다식에 로컬변수를 넣을때 inline을 사용하면 조금더 성능을 끌어 올릴 수 있는데,

//3번 메소드
fun doSomething() {
    val greetings = "Hello"
    
    doSomethingElse {
        println("$greetings from lambda") 
    }
}

그 이유는 아래 메소드를 보면 new Funtion(greetings)
새로운 객체를 생성할때 로컬 변수까지 추가되어 메모리 사용량이 늘어나는데 이 부분을 inline 을 이용해 없앨 수 있기 떄문입니다.

// 3번 메소드의 자바 컴파일 코드
public static final void doSomething() {
    String greetings = "Hello";
    doSomethingElse(new Function(greetings) {
            public final void invoke() {
            System.out.println(this.$greetings + " from lambda");
        }
    });
}

🍒 Reified란?

inline과 함께 refied 키워드를 사용하면 Generics를 사용하는 메소드 까지 처리할 수 있습니다.
범용성 좋은 메소드를 만들기 위해 generics <T> 를 사용할 때가 있습니다.

fun <T> doSomething(someValue: T)

하지만 이러한 class Type T 객체는 타입에 대한 정보가 런타임에서 Type Erase되어버려 알 수없어집니다.
그래서 실행하면 에러가 발생합니다.
왜냐하면 타입을 알 수가 없기 때문입니다.
따라서 Class를 함께 넘겨 type을 확인하고 casting 하는 과정을 거치곤합니다.

  // runtime에서도 타입을 알 수 있게 Class<T> 넘김
fun <T> doSomething(someValue: T, Class<T> type) { 
    // T의 타입을 파라미터를 통해 알기에 OK
    println("Doing something with value: $someValue")  
    // T::class 가 어떤 class인지 몰라서 Error
    println("Doing something with type: ${T::class.simpleName}") 
}

인라인(inline) 함수와 reified 키워드를 함께 사용하면 T type에 대해서 런타임에 접근할 수 있게 해줍니다.
따라서 타입을 유지하기 위해서 Class와 같은 추가 파라미터를 넘길 필요가 없어집니다.

//reified로 런타임시 T의 타입을 유추 할 수있게됨
inline fun <reified T> doSomething(someValue: T) {
  // OK
  println("Doing something with value: $someValue")              
  // T::class 가 reified로 인해 타입을 알 수 있게되어 OK
  println("Doing something with type: ${T::class.simpleName}")    
}

🍋 하지만 inline은 만능이 아니다.

  • 기본적으로 JVM의 JIT 컴파일러에 의해서 일반 함수들은 inline 함수를 사용했을 때 더 좋다고 생각되어지면 JVM이 자동으로 만들어주고 있다.
  • 또한 private 접근자일 경우 사용이 불가능 하다.
  • 위에서 변환된 자바 바이트 코드를 보면 길이가 더 길어져 너무 긴 메소드에 사용시 메모리가 낭비 될 수있다.
  • inline keyword는 1~3줄 정도 길이의 함수에 사용하는 것이 효과적일 수 있습니다.
  • 필요시 특정 메소드를 인라인 방식에서 제외 하고 싶다면 noinline을 사용하자.
profile
🖋정리를 안하면 잊어버린다.👣한 발자국씩 가보자!

2개의 댓글

comment-user-thumbnail
2022년 10월 2일

좋은 글 감사드립니다!

1개의 답글