Closure보다 먼저 알아야할 개념들
Lexical scope vs Dynamic scope
Lexical scope를 사용하는 언어(C, C++, Java, Javascript 등)에서는 소스코드 내에서 변수나 함수가 정의된 위치와 Lexical Context를 기준으로 이름 참조(Name resolution)를 수행한다. 찾아야하는 이름이 내부 Lexical context에 존재하지 않는 경우 외부 Lexical context에서 이를 찾고, 모든 외부 Lexical context에도 해당 이름이 존재하지 않는 경우 오류가 발생한다.
위의 Javascript 코드를 실행하면 아래와 같은 결과가 출력된다.
Javascript는 Lexical scope를 사용하는 언어이기 떄문에 inner
가 x
라는 변수의 이름을 찾을 때에는 자신의 Lexical Context(inner
함수의 Body)에서 우선 x
를 찾는다. 여기에 x
가 없었기 때문에 자신의 전역 Lexical Scope에 위치한 x
(“variable in global”)을 참조하게 된다. 이와 같이 Lexical scope를 사용하는 언어는 Compile time에 함수 혹은 변수 이름의 바인딩이 가능하기 때문에 Early Binding이라고도 불린다.
반면에 Dynamic scope를 사용하는 언어(bash, jinja 등)에서는 실행 시점의 컨텍스트를 기준으로 이름 참조를 수행한다. 찾아야하는 이름이 현재 실행 컨텍스트에 존재하지 않는 경우 상위 실행 컨텍스트에서 이를 찾고, 모든 상위 실행 컨텍스트에도 해당 이름이 존재하지 않는 경우 오류가 발생한다.
위의 Bash 코드를 실행하면 아래와 같은 결과가 출력된다.
Bash는 Dynamic scope를 사용하는 언어이기 때문에 inner
가 x
라는 변수를 찾을 때는 inner
가 실행될 당시의 Call stack을 추적해야 한다.
우선 첫번째 inner
함수가 호출되었을 때, inner
안에는 x
가 없기 때문에 자신의 상위 실행 컨텍스트에서 선언된 x
를 찾게 된다. 첫번째 inner
의 상위 컨텍스트는 전역 Context이기 때문에 “variable in global” 가 출력된다.
두번째 inner
함수 호출은 outer
내에서 수행되기 때문에 inner
의 상위 컨텍스트는 outer
가 된다. outer
내에는 “variable in outer” 값을 가진 x
가 존재하기 때문에 “variable in outer” 가 출력된다.
Dynamic scope를 사용하는 언어는 실제 프로그램이 수행되기 전까지는 함수나 변수에 대한 Binding이 불가능하기 때문에 Late Binding이라고 불린다.
Free variable vs Bound variable
자유 변수(Free variable)는 표현식 내에 해당 변수의 선언이 포함되어 있지 않으며, 다른 영역의 값으로 치환 가능한 변수를 의미한다. 이와 반대로, 종속 변수(Bound variable)은 표현식 내에 해당 변수의 선언이 포함되어 있는 변수를 의미한다.
위 코드의 함수 표현식((bound) => free * bound
)은 두가지 변수(free
와 bound
)를 가지고 있다. 여기서 bound
는 표현식의 인자에 선언된 값이다. 때문에 bound
는 종속 변수이다. 그러나 free
의 선언은 표현식 외부에 존재하기 때문에 이는 자유 변수이다.
쉽게 생각하면 종속 변수는 지역 변수, 자유 변수는 그 이외의 변수라고 생각하면 쉽다.
Closure
위의 createLambdaFunc
가 반환하는 람다 표현식은 Closure가 없이는 실행될 수 없다.
createLambdaFunc
가 반환하는 람다 표현식에서는 자유변수 counter
를 사용하고 있고, 이 counter
는 Lexical Scope 규칙에 따라 createLambdaFunc
에 위치한 counter
주소를 참조하게 된다. 그러나 counter
는 createLambdaFunc
가 종료됨에 따라 Stack Frame에서 사라지게 되고, 람다 표현식이 실행되는 순간의 counter
는 비정상적인 메모리 주소를 참조하게 된다.
이러한 문제를 해결하기 위해 Closure라는 기술이 도입되었다. Closure는 자유 변수들을 람다 표현식에 바인딩하는 기술이다. Closure를 사용하면 위의 구문은 아래와 같이 수행된다.
createLambdaFunc
가 반환하는 람다 표현식(() => { counter++; return counter; }
)은 반환될 때 자신이 가진 자유 변수counter
에 대한 참조를 포함하여 반환된다.func()
가 수행될 때 람다 표현식의counter
는createLambdaFunc
반환 시 포함하였던counter
를 사용하여 수행된다.
분산 처리 프레임워크에서의 Closure
Spark이나 Flink와 같은 분산 처리 프레임워크에서도 람다 표현식을 통한 데이터 변환을 지원한다. 분산 처리 프레임워크는 이러한 람다 표현식을 Serialize해서 각 노드들에 배포한 뒤, 각 노드들은 전달받은 람다 표현식을 Deserialize한 뒤 실행한다.
만일 람다 표현식 내에 자유 변수가 포함되어 있다면 어떻게 될까? 이런 자유 변수 또한 각 노드들로 배포해야 하기 때문에 분산 처리 프레임워크에서는 ClosureCleaner(이름은 다를 수 있다)라는 모듈을 통해 자유 변수를 추적하여 Serialize 한 뒤 배포를 진행한다.
때문에 람다 표현식 내에 Serializable하지 않은 자유 변수가 포함되어 있는 경우에는 java.io.NotSerializableException
(spark)와 같은 오류가 발생하는 것이다.
Spark이나 Flink의 각 함수들에서는 아래와 같이 매개변수로 전달받은 람다 표현식에 대해 Closure Cleaning 작업을 수행하게 된다.
Spark의 RDD
Flink의 Dataset
이러한 Cleaner들은 사용하지 않는 자유변수 혹은 자유변수 내부의 Reference를 지우는 작업도 추가적으로 수행하게 된다.