클로저란 무엇인가 — 함수가 바깥 변수를 기억하는 법
클로저를 함수와 렉시컬 환경의 결합으로 설명하고, C++·Go·Rust·JavaScript·Python의 캡처 의미론 차이와 루프 변수 함정을 구분한다. 코루틴과의 경계도 함께 정리한다.
난이도 입문 · 선행 없음
🗺️ 프로그래밍 언어 개념 로드맵의 한 편
한 줄 요약
클로저(closure)는 함수와 그 함수가 정의된 렉시컬 환경(주변 변수에 접근하는 데 필요한 환경)을 함께 묶은 것이다. 핵심은 함수가 호출된 위치가 아니라 정의된 위치의 환경을 기억할 수 있다는 것이다.
1. 일반 함수와 무엇이 다른가
클로저는 바깥 스코프의 변수를 참조할 수 있고, 필요한 환경은 함수가 그 스코프를 벗어난 뒤에도 유지될 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
c := counter()
c() // 1
c() // 2
c() // 3
counter() 호출은 끝났지만 반환된 함수가 count에 접근해야 하므로 그 환경은 계속 살아 있다.
클로저를 단순히 “상태를 가진 함수”라고 기억할 수는 있지만, 모든 상태 있는 함수가 클로저인 것은 아니다. 자신이 정의된 외부 렉시컬 환경을 캡처한다는 점이 경계다.
2. 캡처는 언어마다 의미론이 다르다
흔히 값 캡처 vs 참조 캡처 두 종류로만 나누지만 이 구분은 특히 C++을 설명할 때 가장 직접적이다. 다른 언어는 변수 바인딩, 환경 객체, 소유권·빌림 등 서로 다른 규칙을 사용한다.
C++ — 캡처 방식을 명시한다
1
2
3
4
5
6
7
int x = 10;
auto by_value = [x]() { return x; };
auto by_ref = [&x]() { return x; };
x = 20;
by_value(); // 10
by_ref(); // 20
[x]—x의 값을 closure object 안에 저장[&x]— 바깥x를 참조[=],[&]— 사용한 외부 변수의 기본 캡처 정책
참조 캡처는 원본 수명이 클로저보다 짧으면 댕글링 참조가 될 수 있다.
JavaScript — 렉시컬 바인딩을 캡처한다
JavaScript 클로저는 보통 “값의 스냅샷”을 복사하는 것이 아니라 렉시컬 환경의 바인딩에 접근한다.
1
2
3
4
let x = 10;
const f = () => x;
x = 20;
f(); // 20
C++ [&x]와 결과가 비슷해 보일 수 있지만 raw reference를 저장하는 것과 같은 메모리 모델은 아니다. 환경의 수명도 GC가 관리한다.
Python — 이름을 둘러싼 스코프 규칙을 따른다
Python 중첩 함수도 외부 스코프의 이름을 참조한다. 읽기는 자연스럽지만 바깥 변수를 다시 대입하려면 nonlocal이 필요하다.
1
2
3
4
5
6
7
def counter():
n = 0
def inc():
nonlocal n
n += 1
return n
return inc
Python의 대표 함정은 late binding이다. 클로저가 이름의 값을 생성 시점에 복사했다고 생각하면 루프에서 예상과 다른 결과가 나올 수 있다.
Go — 캡처된 변수의 수명을 컴파일러가 연장한다
Go 함수 리터럴은 주변 변수를 참조할 수 있다. 캡처된 변수가 함수보다 오래 살아야 하면 컴파일러가 필요한 저장 위치를 결정한다.
1
2
3
4
5
6
7
func counter() func() int {
n := 0
return func() int {
n++
return n
}
}
이를 단순히 “참조 캡처”라고 부르면 C++ reference와 혼동하기 쉽다. 중요한 것은 같은 변수 상태를 클로저들이 공유할 수 있고, 필요한 수명은 언어가 관리한다는 점이다.
Rust — 사용 방식에 따라 빌림·가변 빌림·소유권 이전이 결정된다
Rust 클로저는 본문이 외부 값을 어떻게 사용하는지에 따라 필요한 캡처 방식을 추론한다.
- 읽기만 하면 공유 빌림으로 충분할 수 있다.
- 변경하면 가변 빌림이 필요할 수 있다.
- 값을 소비하면 소유권을 가져갈 수 있다.
move는 캡처에 필요한 값을 클로저 환경으로 이동시키도록 강제한다.
이 차이는 Fn·FnMut·FnOnce 트레이트와 연결된다. move를 단순히 “참조 대신 값 캡처”라고 외우면 소유권 의미를 놓친다.
3. 루프 변수 캡처 함정
문제의 공통 원리는 여러 클로저가 내가 생각한 개별 값이 아니라 같은 바인딩이나 같은 상태를 보고 있지 않은가다. 하지만 구체적인 원인은 언어마다 다르다.
JavaScript
var는 반복마다 새 블록 바인딩을 만들지 않는다.
1
2
3
4
5
6
7
8
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(() => i);
}
fns[0](); // 3
fns[1](); // 3
fns[2](); // 3
let을 쓰면 반복마다 새 바인딩이 만들어져 의도한 0, 1, 2를 얻을 수 있다.
Python
1
2
fns = [lambda: i for i in range(3)]
[f() for f in fns] # 2, 2, 2
클로저가 i의 생성 당시 값을 복사한 것이 아니라 나중에 그 이름을 조회하기 때문이다. 기본 인자로 값을 고정하는 방식 등을 사용할 수 있다.
Go
Go 1.22부터 for 루프의 반복 변수 의미론이 바뀌어 일반적인 closure capture 함정이 크게 줄었다. 오래된 코드나 이전 언어 버전을 설명할 때는 어느 의미론을 전제로 하는지 구분해야 한다.
C++
C++은 [i]와 [&i]를 직접 선택하므로 캡처 방식은 명확하지만, 참조 캡처한 지역 변수가 먼저 소멸하면 댕글링 참조라는 더 직접적인 수명 문제가 생길 수 있다.
공통 규칙은 “루프에서는 항상 값 캡처”가 아니다. 어떤 바인딩·값·참조를 캡처하고 그 대상의 수명이 얼마나 되는지 확인한다가 더 정확하다.
4. 클로저와 코루틴은 다른 개념이다
둘 다 상태가 호출 경계를 넘어 유지될 수 있어 자주 함께 등장하지만 답하는 질문이 다르다.
| 클로저 | 코루틴 | |
|---|---|---|
| 핵심 | 함수가 정의된 외부 환경을 캡처 | 실행을 중단하고 그 지점에서 재개 |
| 보존 | 필요한 외부 바인딩/값 | 실행 위치 + 중단 뒤 필요한 실행 상태 |
| 호출 | 호출할 때 함수 본문 시작부터 실행 | resume하면 중단 지점부터 이어짐 |
클로저가 내부 상태를 기억한다고 해서 코루틴이 되는 것은 아니다. 반대로 코루틴은 외부 변수를 캡처하지 않아도 중단·재개할 수 있다.
5. 이터레이터와 연결되는 이유
map·filter·find 같은 고차 연산은 “각 원소에 무엇을 할지”를 함수로 받는다. 이때 주변 설정을 함께 써야 하면 클로저가 자연스럽다.
1
2
let threshold = 10;
let filtered = values.iter().filter(|&&x| x > threshold);
threshold를 캡처한 클로저를 이터레이터 어댑터에 넘긴 것이다. 이 연결은 이터레이터와 지연 평가에서 이어진다.
한눈에 정리
| 언어 | 핵심 캡처 관점 |
|---|---|
| C++ | 값/참조 캡처를 명시적으로 선택 |
| JavaScript | 렉시컬 환경의 바인딩 |
| Python | 렉시컬 이름과 late binding |
| Go | 주변 변수 캡처 + 필요한 수명 관리 |
| Rust | 사용 방식에 따른 빌림/소유권 캡처 |
서로 비슷한 결과가 나와도 메모리·수명·소유권 의미론까지 같다고 보면 안 된다.
스스로 점검
1. 클로저의 경계는 무엇인가?
답
함수와 그 함수가 정의된 외부 렉시컬 환경을 함께 다룬다는 것이다. 단순히 상태가 있다는 이유만으로 클로저가 되는 것은 아니다.
2. JavaScript 클로저를 C++ 참조 캡처와 완전히 같다고 볼 수 없는 이유는?
답
둘 다 나중의 변경을 볼 수 있지만 JavaScript는 렉시컬 환경의 바인딩을 캡처하고 그 환경의 수명을 GC가 관리한다. C++ [&x]는 실제 객체 수명을 개발자가 보장해야 하는 참조 캡처다.
3. Rust move 클로저는 단순한 값 복사인가?
답
아니다. move는 필요한 캡처를 클로저 환경으로 소유권 이동시키도록 한다. 타입이 Copy이면 결과적으로 복사될 수 있지만 핵심 의미는 소유권과 수명이다.