안드로이드

[Kotlin] inline함수 reified키워드

코딩하는후운 2022. 8. 30. 17:16
반응형

# 람다의 패널티

고차 함수에 람다를 사용할 경우 런타임 시 특정 패널티가 발생한다, 라는 문구로 코틀린의 '인라인 함수' 참조 문서는 시작한다.

https://kotlinlang.org/docs/inline-functions.html

 

람다를 사용하면 각 함수는 객체로 변환되어 메모리 할당과 가상 호출 단계를 거치게 되는데, 이는 런타임 오버헤드를 초래한다는 것이다.

 

# inline function

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

람다를 매개 변수로 사용하는 고차 함수를 '인라인 함수(Inline Function)'로 정의하여 오버 헤드를 줄일 수 있는 방법을 제공하고 있다.

 

인라인 함수로 정의된 함수는 컴파일 단계에서 호출하는 방식이 아니라 코드 자체가 복사되는 방식으로 컴파일 된다.

 

 

# 람다식을 사용했을 대 무의미한 객체 생성을 예방

인라인 함수를 사용한다면 람다식을 사용했을 때 무의미한 객체 생성을 막을 수 있습니다.

무슨 의미일까요?

이를 이해하기 위해서는 kotlin의 람다식이 컴파일 될 때 어떻게 변하는지를 확인해봐야 합니다.

 

fun doSomethingElse(lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}

이 코드를 컴파일 하면 아래와 같은 java 파일이 됩니다.

Functional Interface인 Function 객체를 파라미터로 받고 invoke 메서드를 실행합니다.

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

 

위의 함수를 사용하기 위해서는 아래처럼 코드를 작성할 수 있습니다.

fun doSomething() {
    println("Before lambda")
    doSomethingElse {
        println("Inside lambda")
    }
    println("After lambda")
}

그렇다면 이런 코틀린 코드는 컴파일되서 어떤 자바코드를 가지게 될까요?

아래 코드처럼 나오게 되는데, 아래 코드의 문제점은 doSomethingElse의 파라미터로 새로운 객체를 생성하여 넘겨준다는 것이다.

이는 doSomething이라는 메서드를 호출할 때마다 새로이 만들어집니다.

무의미하게 새로운 객체를 매번 생성

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");
}

 

이러한 문제점을 해결해주는 것이 바로 인라인(inline)함수 입니다.

인라인 함수를 사용하게 되면 코드는 객체를 항상 새로 만드는것이 아니라 해당 함수의 내용을 호출한 함수에 넣는 방식으로 컴파일 코드를 작성하게 됩니다.

 

inline fun doSomethingElse(lambda: () -> Unit) {
    println("Doing something else")
    lambda()
}

내부에서 사용되는 2개의 함수가 내부 코드로 변환되어 사용되는것을 알 수 있다.

무의미하게 Function 객체를 항상 만들어내는 것이 없어졌다.

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");
}

 

# 람다식에 로컬 변수 사용

로컬 변수를 람다에서 사용한다면 어떻게 컴파일 되는지 알아보도록 하자.

fun doSomething() {
    val greetings = "Hello"                // Local variable
    doSomethingElse {
        println("$greetings from lambda")  // Variable capture
    }
}

람다식에서 사용하는 지역 변수는 아래와 같이 Function객체의 생성자의 변수로 들어가는것을 확인 할 수 있다.

public static final void doSomething() {
    String greetings = "Hello";
    doSomethingElse(new Function(greetings) {
            public final void invoke() {
            System.out.println(this.$greetings + " from lambda");
        }
    });
}

객체에 변수가 추가되었습니다. 즉, 객체의 메모리 사용량이 늘어났다는 것입니다.

이 경우 인라인 함수를 사용하면 좀 더 나은 성능을 보장할 수 있을것이라는 사실을 알 수 있습니다.

: 메모리 사용량이 늘어서 inline을 이용하여 효과를 더 많이 봤다는 말인듯.

 

 

# noinline

매개 변수로 받는 람다가 2개 이상이고, 일부 람다는 inline방식을 사용하고 싶지 않을 경우도 있을 것이다.

그럴 경우엔 인라인 함수 선언 시 해당 파라미터 앞에 'noinline' 키워드를 붙여주면 된다.

 

inline fun inlineFun(op1: ()->Unit, noinline op2: ()->Unit) { ... }

이렇게 선언하면 'inlineFun' 메서드가 인라이닝 될 때 op2 부분은 호출 방식으로 복사된다.

 

val fun1 = { println("op1") }
val fun2 = { println("op2") }
inline fun inlineFun(op1: ()->Unit, noinline op2: ()->Unit) {
	op1()
    op2()
}

fun main(args: Array<String>?) {
	// inlineFun(fun1, fun2)
    println("op1")
    fun2()
}

이런 식으로,

 

 

# Type Reified

자바에서는 제네릭으로 전달되는 타입 파라미터를 구체화하지 못한다.

그래서 아래와 같은 표현은 사용 불가하다.

<T> void method() throws Exception {
  // T 타입에 대한 정보를 런타임에 얻을 수 없기때문에 컴파일 불가.
  T t = T.class.newInstance()
}

<T> void method(Class<T> clazz) throws Exception {
  // 이처럼 T 타입에 대한 정보를 갖고있는 Class 객체를 전달해서 호출.
  T t = clazz.newInstance()
}

 

코틀린도 자바와 동일하게 기본적으로는 타입 파라미터의 정보는 런타임에 사라진다.

다만 inline과 refied라는 제어자를 이용해서 T타입에 대한 정보를 가져올 수 있다.

이는 코틀린 컴파일러가 inline제어자가 붙은 함수에 대해 바이트 코드를 직접 복붙해주기 때문에 가능*syntax sugar이다.

 

*syntax sugar : 문법적인 기능은 그대로 유지하되, 코드를 작성하는 사람 입장에서 혹은 그 코드를 다시 읽는 사람의 입장에서 편의성이 높은 프로그래밍 문법을 의미합니다.

inline fun <reified T> method() {
    val t: T = T::class.java.newInstance()
}

 

 

자바에서 Class<T>타입을 메서드 파라미터로 받는 이유중 상당수는 타입 파라미터를 구체화할 수 없기 때문이다.

그래서 주로 타입을 알아야하는 deserialize관련 코드에 많이 등장한다.

대표적인 예) json을 이용하는 Jackson이나 Gson같은 라이브러리들이다.

String json = "{\"age\":31}";
ObjectMapper objectMapper = new ObjectMapper();
// readValue 는 그 자체로 제네릭 메서드지만 타입을 구체화할 수 없으므로 deserialize 해야할 타입의 Class 정보가 따로 필요하다.
Person person = objectMapper.readValue(json, Person.class);

 

 

코틀린은 type reified와 강력한 타입 추론을 통해 평소 원했던대로 아래와 같이 사용할 수 있다.

val mapper = ObjectMapper()
val json = "{\"age\":31}"
// 메서드에 타입 아규먼트를 전달해 타입을 구체화할 수 있다.
val person1 = mapper.readValue<Person>(json)
// 메서드가 아닌 리턴받는 변수에 타입을 명시함으로써 타입 추론을 통해 타입을 구체화할 수 있다.
val person2: Person = mapper.readValue(json)

 

둘 중 어느 방법을 사용해도 자바를 사용할때처럼 Class<T>를 직접 전달하지 않아도 된다.

물론 함수가 inline + reified로 구현되지 않았다면 자바를 사용할때 처럼 타입을 직접 전달해줘야 한다.

 

 

# 그렇다면 항상 inline을 사용하는게 좋은거 아닌가?

 

기본적으로 JVM의 JIT컴파일러에 의해서 일반 함수들은 inline함수를 사용했을 때 더 좋다고 생각되어지면 JVM이 자동으로 만들어 주고 있습니다.

그리고 inline함수를 사용하면 좋지 않거나 사용이 불가능할 경우도 있습니다.

 

public inline 함수는 private 함수를 호출할 수 없음

public 인라인 함수는 private 함수에 접근할 수 없습니다.

아래 코드처럼 작성하면, Public-API inline function cannot access non-public API fun라는 에러가 발생

inline fun doSomething() {
    doItPrivately()  // Error
}

private fun doItPrivately() { }

 

 

 

 

 

 

참조 : 

https://m.blog.naver.com/yuyyulee/221389623237

https://velog.io/@changhee09/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%9D%B8%EB%9D%BC%EC%9D%B8-%ED%95%A8%EC%88%98

https://multifrontgarden.tistory.com/263

https://sabarada.tistory.com/176
https://datalibrary.tistory.com/33

반응형