클로저

2023년 2월 19일 수정

개인적으로 컴퓨터 프로그래밍 개념에서 클로저(Closure)는 동적으로 생성되는 함수라고 정의한다. 그리고 생성되는 시점의 주변 환경을 복제하는 특수한 능력을 갖추고 있다는 점이 공통적이기도 할 것 같다. 정확한 특징은 아니지만 이름 없는(익명) 함수라고도 불릴 때도 있다.

Javascript의 클로저

아마도 아래와 같은 구조의 예제는 인터넷에서 클로저에 관해 찾아보면 쉽게 발견할 수 있는 Javascript 클로저의 예제일 것이다.

function generateClosure(x) {
    return function(y) {
        return x + y;
    }
}

이런 예가 자주 언급되는 이유가 있다. 앞서 설명했던 클로저의 다양한 특성이 많이 드러나는 예이기 때문이다.

우선 동적 생성 함수라는 점이다. generateClosure() 함수 안에서 이름이 없는 함수를 리턴하고 있다. 이 함수는 코드를 해석하는 시점에서는 존재하지 않다가 이 generateClose() 함수가 호출되어야만 생성된다.

반환되는 함수는 이름이 없다. 즉 이름 없는 함수다. 거기다 이 generateClose() 함수가 아니면 이 함수를 구할 방법이 없다.

나머지 주요 특성은 바로 주변 환경을 복제한다는 점이다. 아래 예제는 위 코드를 호출하고 어떤 결과가 나오는지를 보기 위한 예제다.

> let c = generateClosure(1);
> c(2);
3
> let d = generateClosure(2);
> d(2);
4

여기서 cd 를 같은 매개변수로 호출했음에도 결과가 다르다는 것에 주목하자.

generateClose() 함수 안에서 반환하는 클로저는 앞서 이야기했지만 호출 시점에 동적으로 생성된다. 이 생성되는 시점의 x 의 값을 복제해서 가져간다. 주변 환경을 복제한다는 말은 바로 이 부분이다.

개념은 이렇게 되는데, 클로저가 자주 쓰이는 분야는 아무래도 핸들러(handler) 혹은 콜백(callback) 구현인 것 같다.

setTimeout(function() {
    console.log('over 2 seconds!');
}, 2000);

setTimeout() 함수는 특정 시간 뒤 특정 명령을 실행시키는 함수다. 여기서 첫 매개변수로 이름 없는 함수를 넘기는데 이것도 클로저다.

이 외에 화살표 함수(Arrow Function)의 경우도 동적으로 생성되는 이름 없는 함수이기 때문에 클로저로 볼 수 있다.

> let adder = (x, y) => x + y;
> adder(1, 2)
3

Swift의 클로저

Swift의 클로저도 위의 개념적인 부분은 거의 동일하다.

let adder: (Int, Int) -> Int = {
    (x, y) in
    return x + y
}

문법의 차이만 있을 뿐이지 이름도 없고 동적으로 생성되는 데다 주변 환경을 복제하는 것은 거의 동일하다.

하지만 차이도 있다.

Swift의 메모리 관리 체계인 ARC의 특징으로 인해 주변 환경 복제 뿐만 아니라 납치(Capture) 문제도 발생하게 된다. 값(value) 타입이 아닌 인스턴스의 경우 리테인 카운드를 증가시키게 되어서 메모리 해제를 제때 하지 못 하는 등의 문제를 발생시킬 수도 있다.

그래서 납치 규칙(capture rule) 같은 것도 정의할 수 있다.

let selfAdder: (Int) -> Int = {
    [unowned self] (y) in
    return self.x + y
}

위의 코드는 unowned 를 통해 self 에 대해 weak 참조를 하도록 하는 예이다. 이렇게 하면 위의 클로저의 메모리가 해제되지 않으면 self 도 해제되지 않는 문제를 해결할 수 있다.

Objective-C 블록

Objective-C의 경우 블록(Blocks)이라는 클로저와 비슷한 기능이 제공된다.

int (^adder)(int, int) = ^int(int x, int y) {
    return x + y;
};

^ 가 붙어있는 곳이 이 블록을 담는 변수의 이름이다. 어차피 클로저처럼 이름이 없다보니 변수에 담거나 아니면 익명으로 넘기거나 등으로 써야 한다.

Swift의 납치 규칙과 비슷한 문제로 블록 내부에서 특정 인스턴스를 참조할 때 약하게(weak) 참조하는 것도 비슷하다.

__weak MyClassType *wealSelf = self;
int (^selfAdder)(int) = ^int(int y) {
    return weakSelf.x + y;
};

함수나 메서드의 매개변수 용도로 정의할 때는 별도의 매개변수에 이 블록 인스턴스가 전달되기 때문에 블록 이름을 생략해서 정의한다.

- (void)someMethodWithCompletion:(void (^)(NSError *error))completionHandler
{
    ...
}

위의 예에서 앞의 void 로 해당 핸들러의 반환 타입을 명시하고 꺽쇠에는 이름이 없이 블록 매개변수를 선언하는 모습을 볼 수 있다. 이 경우 타입이 길어질 수 있기 때문에 자주 사용한다면 typedef 로 미리 별칭을 붙여놓자.

typedef void (^SomeCompletionHandler)(NSError *error);

위와 같이 선언하면 이 타입의 별칭을 대신 이용할 수 있다.

- (void)someMethodWithCompletion:(SomeCompletionHandler)completionHandler
{
    ...
}

Python의 클로저와 람다 함수

Python의 경우는 앞서 소개한 것들과 약간 다르게 익명이라기 보다는 그냥 동적으로 생성되는 함수 정도의 소개가 맞는 것 같다.

>>> def gen_closure(x):
...     def closure(y):
...         return x + y
...     return closure
...
>>> c = gen_closure(1)
>>> c(2)
3
>>> d = gen_closure(2)
>>> d(2)
4

함수나 메서드 안에서 특정 함수를 정의하는 경우 이 함수가 바로 클로저라고 이해할 수 있다. 이름이 있을 수 있다는 점만 제외하면 거의 동일한 특성을 보인다.

이 외에 Python의 경우 람다(lambda) 함수라는 특수한 익명 함수를 따로 제공한다. 아래는 람다 함수를 쓰기 위해 잘 안 쓰이는 map 을 사용한 예제이다.

>>> lst = [1, 2, 3, 4]
>>> list(map(lambda x: str(x), lst))
['1', '2', '3', '4']

lambda 라는 이름과 함께 매개변수 이름 선언이 오고 단 한 줄의 구현부가 올 수 있다. 보다시피 이름이 없는 익명 함수다.

생각해 볼 일

여러 언어의 예제를 살펴봤는데, 이런 편리함을 주는 클로저의 존재에서 생각해야 할 일은 더 있다. 바로 퍼포먼스 문제다.

앞서 살펴본 클로저의 공통적인 특징은 클로저 생성 당시의 주변 환경을 복제하는 것이다. 당연하게도 클로저가 참조하는 클로저 외부의 변수들을 모두 복제해야 하니 퍼포먼스 문제는 필연적이다.

그리고 환경 복제는 동시에 메모리 점유가 증가한다는 문제 또한 동시에 가지고 있다.

따라서 클로저를 꼭 사용해서 좋은 곳과 그렇지 않은 곳도 있다는 것을 이해하고 적절하게 사용하는 것이 가장 좋을 것 같다. 예를 들어 루프 내부에서 클로저를 생성하는 행위가 얼마나 안 좋은 설계일지는 깊게 생각하지 않아도 알 수 있을 것 같다.