본문 바로가기

Kotlin

Kotlin Closure

Unsplash, Aitk sulianame.

Closure

Closure (Close over) 기능을 최초로 제공한 언어는 1958년, John McCarthy 에 의해 개발된 언어 Lisp 입니다. Closure 를 포함한 수많은 현대적 프로그래밍 개념과 아이디어를 선보인 언어죠. Lisp 에서는 함수 정의 시, 외부 변수를 캡처하고 사용할 수 있었기 때문에, Closure 의 개념이 최초로 등장한 것으로 여겨집니다.

 

오늘은 Kotlin 의 Closure 를 알아봤습니다.


For what?

Closure 는 임의의 함수 내에서 외부 가변 변수를 캡처하고, 그 변수에 액세스 및 재할당할 수 있도록 하는 기능을 제공합니다. 개념 자체가 다소 복잡한데요.

 

 

Closure 내에서 Capture 되었을 때 해당 변수가 Modified 되기 위해 Reference object 로 감싸졌다는, 다소 이해하기 어려운 설명을 볼 수 있습니다.

 

fun main() {
    var name = "john"

    thread {
        name = "tommy"
    }
}

 

변수 name 에 할당된 "john" 을 새로운 스레드에서 "tommy" 로 재할당하는 코드입니다. Kotlin 에서는 별다른 문제 없이 컴파일할 수 있지만, 위 소스 코드를 Java 로 작성하면 컴파일 에러가 발생합니다.

 

Java 는 Closure 를 제공하지 않기 때문에, 가변 상태의 변수가 선언된 스레드와는 다른 스레드나 콜백 함수 스코프 내에서 해당 변수에 접근하려면, 해당 변수가 final 로 선언되어 있어야만 하기 때문입니다. 

 

멀티 코어 환경에서는 공유된 자원에 접근하는 함수들이 동일한 CPU 에서 수행될 것이라는 보장이 없고, 각 CPU 는 작업의 수행 결과를 Register 를 통해 Cache 에 저장합니다. Cache 는 각 CPU 내부에 존재하므로, 임의의 CPU 는 다른 CPU 의 Cache 에 접근할 수 없습니다. Cache 의 값이 RAM 에 도달해야만, 다른 CPU 가 액세스할 수 있게 됩니다.

 

그렇지 않은 경우에 다른 CPU 에서 이에 접근하고자 하는 경우, 함수 수행 결과가 반영되지 않은 기존의 자원에 접근하게 되기 때문에 프로그램이 개발자의 의도대로 작동하지 않으므로, 컴파일러 차원에서 이를 방지하고자 final 이 붙은 불변 상태의 변수에만 접근할 수 있도록 하는 것입니다.


How does it works?

그렇다면 코틀린의 Closure 는 어떤 방식으로 이러한 문제를 해결할까요?

이를 확인하기 위해, 위 코드를 Java 코드로 변환해보겠습니다.

 

public final class FileKt {
   public static final void main() {
      final Ref.ObjectRef name = new Ref.ObjectRef();
      name.element = "john";
      ThreadsKt.thread$default(false, false, (ClassLoader)null, (String)null, 0, (Function0)(new Function0() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }

         public final void invoke() {
            name.element = "tommy";
         }
      }), 31, (Object)null);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

 

소스 코드가 예상보다 훨씬 길어졌습니다. 우리는 3번 라인의 Ref.ObjectRef 를 확인할 수 있습니다.

 

public static final class ObjectRef<T> implements Serializable {
    public T element;

    @Override
    public String toString() {
        return String.valueOf(element);
    }
}

 

타입 파라미터 <T> 를 받는 제네릭 클래스로 구현이 되어 있으며, 복잡한 것 없이 가변 상태의 변수를 참조형 불변 변수 객체 내의 Field 로 새롭게 선언해버립니다. 

 

'함수의 실행 환경과 변수를 함께 캡처한다' 라는 설명이 많은데, 개인적으로 다소 난해하다고 여겨지는 표현입니다.

 

제가 이해한 바로, Kotlin 의 Closure 는 Outer Scope 에 선언된 Variable 을 Variable Field 로 갖는 Value 를 자동으로 선언한다. 라고 얘기할 수 있겠습니다.

 

Closure 는 외부 함수 스코프에 선언된 변수들을 스택이 아닌 힙에 저장할 수 있도록 Ref.Object 를 사용합니다. 그러므로, 함수의 고유한 공간인 스택 프레임과 관계 없이 액세스하고 재할당할 수 있게 됩니다.

 


사실 Closure 는 사용하려고 마음먹고 사용하는게 아닌 것 같다는 생각을 합니다. 물론 Java 로 코드를 작성했다면 그렇게 했겠지만 말입니다. 

'Kotlin' 카테고리의 다른 글

Flow Cancellation (feat. Exception Handling)  (0) 2023.09.24
Coroutine Details  (0) 2023.06.22
Generics 와 Reified 키워드  (0) 2023.04.21
by 를 사용한 Kotlin 의 Delegation Pattern  (0) 2022.11.17
확장 함수 간단 정리  (0) 2022.11.15