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 |