클로저(Closure): 함수가 환경을 기억하는 마법

1. 개념 소개: 함수가 환경을 기억하는 이유

소프트웨어 개발에서 클로저(Closure)는 언뜻 어렵게 느껴질 수 있지만, 일단 그 개념을 이해하고 나면 수많은 강력한 프로그래밍 패턴과 기술의 핵심 원리임을 깨닫게 됩니다. 클로저는 단순히 고급 기술이 아니라, 현대 웹 프레임워크부터 비동기 처리, 데이터 캡슐화에 이르기까지 광범위하게 사용되는 필수적인 프로그래밍 개념입니다.
정의
클로저는 **"함수와 함수가 선언될 당시의 렉시컬(Lexical) 환경의 조합"**을 의미합니다. 좀 더 쉽게 말해, 어떤 함수가 자신이 생성될 때의 외부 환경(다른 함수의 지역 변수, 매개변수 등)을 기억하고, 그 환경에 접근할 수 있는 기능을 말합니다. 이 "기억"은 함수가 자신의 원래 스코프를 벗어나 다른 곳에서 호출되더라도 유효합니다.
탄생 배경
클로저의 개념은 1960년대 람다 대수(Lambda Calculus)와 함수형 프로그래밍 패러다임에서 유래했습니다. 초기에는 주로 Lisp과 같은 함수형 언어에서 사용되었지만, 시간이 지나면서 자바스크립트, 파이썬, 루비, 스위프트, 고(Go) 등 대부분의 현대 프로그래밍 언어에서 핵심 기능으로 채택되었습니다. 특히 자바스크립트에서는 콜백 함수, 이벤트 핸들러, 모듈 패턴 구현에 필수적인 요소로 자리 잡으며 그 중요성이 더욱 부각되었습니다.
왜 중요한가?
클로저가 중요한 이유는 다음과 같습니다.
- 데이터 은닉 (Information Hiding) 및 캡슐화: 클로저를 통해 특정 데이터를 외부에서 직접 접근할 수 없도록 '프라이빗(private)'하게 유지하면서도, 내부 함수를 통해 그 데이터에 접근하고 조작할 수 있게 합니다. 이는 객체 지향 프로그래밍의 캡슐화와 유사한 효과를 냅니다.
- 상태 유지 함수 (Stateful Functions) 생성: 함수가 호출될 때마다 독립적인 상태를 가지게 할 수 있습니다. 예를 들어, 특정 값을 계속해서 증가시키거나 감소시키는 카운터 함수를 클로저를 이용해 만들 수 있습니다.
- 코드의 모듈성 및 재사용성 증진: 반복되는 로직을 클로저를 활용하여 함수 팩토리(function factory) 형태로 만들면, 필요에 따라 다양한 설정값을 가진 함수를 쉽게 생성하고 재사용할 수 있습니다.
- 고차 함수 (Higher-Order Functions) 및 데코레이터 패턴 구현의 핵심: 다른 함수를 인자로 받거나 함수를 반환하는 고차 함수를 구현할 때 클로저가 필수적으로 사용됩니다. 파이썬의 데코레이터가 대표적인 예입니다.
- 비동기 프로그래밍에서의 유용성: 자바스크립트의 비동기 콜백 함수나 이벤트 핸들러에서 클로저는 특정 시점의 상태를 '기억'하게 하여 예상치 못한 동작을 방지하고 정확한 로직을 구현하는 데 도움을 줍니다.
2. 핵심 원리 설명: 렉시컬 스코프와 기억력

클로저의 핵심 원리를 이해하려면 먼저 렉시컬 스코프(Lexical Scope) 개념을 알아야 합니다.
렉시컬 스코프 (Lexical Scope)
렉시컬 스코프는 함수가 어디에서 선언되었는지에 따라 해당 함수의 상위 스코프(Scope)가 결정되는 방식을 말합니다. 즉, 코드를 작성하는 시점에 이미 함수의 스코프 체인이 결정된다는 의미입니다. 이는 함수가 어디서 호출되느냐에 따라 스코프가 결정되는 다이내믹 스코프(Dynamic Scope)와 대비됩니다. 대부분의 현대 프로그래밍 언어는 렉시컬 스코프를 따릅니다.
클로저는 이 렉시컬 스코프 덕분에 탄생합니다. 내부 함수는 자신이 선언된 외부 함수의 환경(변수, 매개변수 등)을 "기억"하고, 외부 함수가 실행을 마쳐도 해당 환경이 가비지 컬렉션(Garbage Collection) 되지 않도록 참조를 유지합니다.
비유: 레시피와 주방
클로저를 이해하기 위해 요리에 비유해 봅시다.
- 메인 셰프의 주방 (외부 함수 스코프): 메인 셰프가 요리를 준비하는 주방이 있습니다. 이 주방에는 '특별한 소스 재료' (외부 함수의 지역 변수)가 놓여 있습니다.
- 보조 셰프의 레시피 (내부 함수): 메인 셰프는 보조 셰프에게 '특별한 소스 재료'를 사용하는 특정 요리 레시피 (내부 함수)를 가르쳐 줍니다. 보조 셰프는 이 레시피를 배우고, 이 레시피는 '특별한 소스 재료'를 사용해야 한다고 명시되어 있습니다.
- 레시피를 다른 곳으로 가져가도 (클로저): 이제 메인 셰프는 퇴근하고 주방을 떠납니다. 보조 셰프는 메인 셰프의 주방을 떠나 자신의 개인 작업실 (다른 스코프)에서 이 레시피를 사용하려고 합니다. 놀랍게도, 보조 셰프는 '특별한 소스 재료'가 더 이상 메인 셰프의 주방에 없더라도, 레시피에 따라 그 재료를 '기억'하고 사용할 수 있습니다. 마치 레시피 자체가 그 재료를 가져오는 방법을 알고 있는 것처럼 말이죠.
여기서 '보조 셰프의 레시피'가 바로 클로저입니다. 레시피(함수)는 자신이 만들어진 주방(렉시컬 환경)의 재료(변수)를 기억하고, 그 주방이 사라져도 그 재료를 사용할 수 있는 능력을 가집니다.
다이어그램 (개념적 설명)
코드로 표현하면 다음과 같은 구조가 됩니다.
+---------------------------------+
| outer_function() |
| - variable 'x' = 10 |
| |
| +---------------------------+ |
| | inner_function() | |
| | - uses 'x' from outer | |
| | | |
| +---------------------------+ |
| |
| - returns inner_function |
+---------------------------------+
|
| (outer_function 실행 종료 후)
V
+---------------------------------+
| my_closure = inner_function |
| - Still holds reference to 'x'|
| (even though outer_function |
| has finished executing) |
+---------------------------------+
outer_function이 실행되면 x라는 변수가 생성됩니다. inner_function은 outer_function 내부에서 정의되었으므로, inner_function은 x에 접근할 수 있는 렉시컬 환경을 가집니다. outer_function이 inner_function을 반환하면, inner_function은 outer_function의 스코프를 "닫아버린(closure)" 형태로 x에 대한 참조를 유지합니다. 이 반환된 inner_function을 my_closure 변수에 할당하고 나중에 호출해도 x의 값에 여전히 접근할 수 있습니다.
3. 코드 예제: 클로저의 실제 동작
예제 1: Python - 카운터 함수 (데이터 은닉 및 상태 유지)
이 예제에서는 클로저를 사용하여 호출될 때마다 숫자를 1씩 증가시키는 카운터 함수를 만듭니다. create_counter 함수는 count 변수를 감싸고 있는 increment 함수를 반환합니다. increment 함수는 create_counter의 count 변수를 기억하고 조작합니다.
def create_counter():
"""
호출될 때마다 숫자를 1씩 증가시키는 카운터 함수를 생성합니다.
"""
count = 0 # 이 변수는 create_counter의 렉시컬 환경에 속합니다.
def increment():
"""
외부 함수의 count 변수를 참조하고 증가시킵니다.
"""
nonlocal count # Python 3에서 외부(non-local) 스코프 변수를 수정하기 위해 사용
count += 1
return count
# increment 함수는 create_counter 함수의 렉시컬 환경(count 변수 포함)을 '기억'합니다.
return increment
# 첫 번째 카운터 생성
counter1 = create_counter()
print(f"Counter 1: {counter1()}") # 출력: Counter 1: 1
print(f"Counter 1: {counter1()}") # 출력: Counter 1: 2
print(f"Counter 1: {counter1()}") # 출력: Counter 1: 3
# 두 번째 독립적인 카운터 생성
counter2 = create_counter()
print(f"Counter 2: {counter2()}") # 출력: Counter 2: 1 (counter1과는 독립적)
print(f"Counter 1: {counter1()}") # 출력: Counter 1: 4 (여전히 독립적으로 동작)
이 예제에서 counter1과 counter2는 각각 create_counter가 호출될 때마다 생성된 독립적인 count 변수를 기억하는 클로저입니다. 이들은 서로의 count 값에 영향을 주지 않습니다.
예제 2: JavaScript - 이벤트 리스너와 클로저 (특정 시점의 값 유지)
자바스크립트에서 클로저는 비동기 작업이나 이벤트 핸들러에서 특히 유용합니다. 다음 예제는 버튼 클릭 시 해당 버튼의 인덱스를 표시하는 기능을 클로저를 사용하여 구현합니다.
// HTML 구조:
// <div id="container">
// <button>Button 0</button>
// <button>Button 1</button>
// <button>Button 2</button>
// </div>
function setupButtons() {
const container = document.getElementById('container');
const buttons = container.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
// let 키워드 사용: 블록 스코프 변수로, 각 반복마다 새로운 'i' 값을 가집니다.
// 이것이 클로저를 형성하는 핵심입니다.
buttons[i].addEventListener('click', function() {
// 이 익명 함수는 'i'가 선언된 setupButtons 함수의 렉시컬 환경을 기억합니다.
// 그리고 let 덕분에 각 버튼의 리스너는 각기 다른 'i' 값을 가집니다.
console.log(`클릭된 버튼의 인덱스: ${i}`);
});
}
// 만약 for (var i = 0; i < buttons.length; i++) { ... } 였다면,
// 모든 리스너는 'i'의 최종 값 (buttons.length)을 참조하게 되어,
// 어떤 버튼을 클릭해도 항상 '클릭된 버튼의 인덱스: 3' (예시)이 출력될 것입니다.
// 이는 'var'가 함수 스코프를 따르기 때문입니다.
}
// HTML 로드 후 버튼 설정 함수 호출
// (실제 웹 환경에서는 DOMContentLoaded 이벤트 리스너 내에서 호출하는 것이 일반적입니다.)
// setupButtons(); // 이 코드는 HTML 문서에 <div id="container">...</div> 가 존재할 때 작동합니다.
이 예제에서 let i를 사용한 for 루프는 각 반복마다 새로운 i 변수를 생성합니다. addEventListener의 콜백 함수는 이 i 변수를 '기억'하는 클로저를 형성합니다. 따라서 각 버튼의 클릭 리스너는 자신이 생성될 당시의 고유한 i 값을 참조하게 됩니다. 만약 var i를 사용했다면, i는 함수 스코프를 가지므로 모든 콜백 함수가 동일한 i 변수를 참조하게 되어, 예상치 못한 결과를 초래할 것입니다.
4. 실무 적용 사례
클로저는 현대 소프트웨어 개발에서 매우 다양한 방식으로 활용됩니다.
-
데이터 캡슐화 및 프라이빗 변수 흉내 (JavaScript): 자바스크립트에는 클래스 기반의
private키워드가 최근에 도입되었지만, 이전부터 IIFE(즉시 실행 함수 표현)와 클로저를 사용하여 모듈 패턴을 구현하고 프라이빗 변수를 흉내 냈습니다.const counterModule = (function() { let privateCounter = 0; // 클로저에 의해 캡슐화된 private 변수 function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } }; })(); console.log(counterModule.value()); // 0 counterModule.increment(); counterModule.increment(); console.log(counterModule.value()); // 2 // console.log(counterModule.privateCounter); // undefined, 외부에서 접근 불가 -
파이썬 데코레이터 (Decorators): 파이썬의 데코레이터는 함수를 인자로 받아 새로운 기능을 추가한 후 다시 함수를 반환하는 고차 함수입니다. 이때 클로저가 핵심적으로 사용되어 원래 함수의 렉시컬 환경을 유지하면서 추가 기능을 덧붙일 수 있습니다.
def timer_decorator(func): import time def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) # 원래 함수 호출 end_time = time.time() print(f"함수 {func.__name__} 실행 시간: {end_time - start_time:.4f}초") return result return wrapper # wrapper 함수는 func의 렉시컬 환경을 기억하는 클로저 @timer_decorator def slow_function(delay): time.sleep(delay) print(f"{delay}초 지연 완료!") slow_function(2) -
부분 적용 (Partial Application) 및 커링 (Currying): 여러 인자를 받는 함수를 일부 인자만 미리 적용하여 새로운 함수를 만드는 기법입니다. 클로저는 이 미리 적용된 인자들을 기억하는 데 사용됩니다.
function multiply(a, b) { return a * b; } // 부분 적용 함수 팩토리 function partialMultiply(a) { return function(b) { // 클로저: 'a' 값을 기억 return multiply(a, b); }; } const double = partialMultiply(2); // double 함수는 'a'가 2인 클로저 const triple = partialMultiply(3); // triple 함수는 'a'가 3인 클로저 console.log(double(5)); // 10 console.log(triple(5)); // 15 -
비동기 처리 및 콜백: 네트워크 요청, 파일 I/O 등 비동기 작업 후 실행될 콜백 함수가 특정 시점의 변수 값을 기억해야 할 때 클로저가 자연스럽게 사용됩니다.
-
React Hooks (JavaScript): React의
useState,useEffect,useCallback등 대부분의 훅(Hook)은 클로저를 기반으로 작동합니다. 각 컴포넌트 인스턴스는 자신의 상태와 이펙트를 기억하는 클로저를 형성하여 독립적인 상태 관리를 가능하게 합니다.
5. 자주 하는 실수와 해결법
클로저를 사용할 때 초중급 개발자들이 흔히 겪는 실수와 그 해결법을 알아봅시다.
실수 1: 반복문 내 var 변수 문제 (JavaScript)
가장 흔한 실수 중 하나로, for 루프 내에서 var 키워드로 선언된 변수를 클로저가 참조할 때 발생합니다. var는 함수 스코프를 따르므로, 루프가 끝난 후 모든 클로저가 var 변수의 최종 값을 참조하게 됩니다.
// 문제 코드 (var 사용)
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(`인덱스: ${i}`); // 예상: 0, 1, 2 | 실제: 3, 3, 3
}, 100 * i);
}
해결법:
-
let또는const사용 (ES6+):let과const는 블록 스코프를 따르므로, 루프의 각 반복마다 새로운 변수 인스턴스를 생성하여 클로저가 각기 다른i값을 참조하게 합니다.// 해결법 1: let 사용 (권장) for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(`인덱스: ${i}`); // 출력: 0, 1, 2 }, 100 * i); } -
IIFE (즉시 실행 함수 표현) 사용:
let이 없던 시절에는 IIFE를 사용하여 각 반복마다 새로운 스코프를 생성하고, 그 스코프 내에서 변수 값을 캡처했습니다.// 해결법 2: IIFE 사용 (과거 방식) for (var i = 0; i < 3; i++) { (function(index) { // IIFE로 새로운 스코프 생성 및 index 값 캡처 setTimeout(function() { console.log(`인덱스: ${index}`); // 출력: 0, 1, 2 }, 100 * index); })(i); // 현재 i 값을 index 매개변수로 전달 }
실수 2: 메모리 누수 (Memory Leak)
클로저는 외부 환경의 변수를 참조하므로, 클로저가 불필요하게 오래 유지되면 참조하고 있는 외부 변수들도 가비지 컬렉션되지 않고 메모리에 남아있을 수 있습니다. 특히 DOM 요소나 대용량 객체를 참조하는 클로저가 많을 때 문제가 됩니다.
let element = document.getElementById('myButton');
function attachHandler() {
let largeData = new Array(1000000).fill('some data'); // 큰 데이터
element.addEventListener('click', function() {
console.log(largeData.length); // 클로저가 largeData를 참조
});
// 여기서 element와 largeData는 attachHandler 스코프를 벗어나도
// addEventListener의 콜백 함수에 의해 참조되어 메모리에 유지될 수 있습니다.
}
// attachHandler();
// element = null; // DOM 요소를 제거해도 클로저가 참조하고 있으면 메모리가 해제되지 않을 수 있음
해결법:
- 불필요한 클로저 참조 해제: 이벤트 리스너를 제거하거나, 클로저가 더 이상 필요 없을 때 명시적으로 참조를
null로 설정하여 가비지 컬렉션될 수 있도록 합니다. - 약한 참조(Weak Reference) 활용: 특정 언어에서 제공하는 약한 참조 메커니즘을 사용합니다. (예: Python의
weakref모듈) - 클로저 사용의 적절성 검토: 모든 상황에 클로저가 최적의 해결책은 아닙니다. 단순한 경우라면 객체 속성이나 다른 방식으로 데이터를 전달하는 것이 더 효율적일 수 있습니다.
실수 3: 과도한 클로저 사용으로 인한 코드 복잡성
클로저는 강력하지만, 너무 복잡하게 중첩되거나 불필요하게 사용되면 코드의 가독성을 해치고 디버깅을 어렵게 만들 수 있습니다.
해결법:
- 명확하고 간결하게 작성: 클로저를 사용할 때는 그 목적을 명확히 하고, 최소한의 범위 내에서 사용하도록 노력합니다.
- 디자인 패턴 활용: 데코레이터, 모듈 패턴 등 이미 잘 알려진 디자인 패턴의 맥락에서 클로저를 활용하면 코드의 구조를 이해하기 쉽습니다.
- 테스트 코드 작성: 클로저를 사용하는 복잡한 로직은 유닛 테스트를 통해 정확성을 보장하는 것이 중요합니다.
6. 더 공부할 리소스 추천
클로저는 프로그래밍의 깊이를 더해주는 중요한 개념입니다. 다음 리소스들을 통해 더 깊이 있게 학습해 보세요.
-
MDN Web Docs - 클로저 (JavaScript): 자바스크립트 클로저에 대한 가장 표준적이고 상세한 설명을 제공합니다. 다양한 예제와 함께 개념을 명확히 이해하는 데 큰 도움이 됩니다. https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures
-
Python 공식 문서 - Nested functions and scope: 파이썬의 스코프 규칙과 중첩 함수,
nonlocal키워드에 대한 이해는 클로저를 파이썬에서 효과적으로 사용하는 데 필수적입니다. https://docs.python.org/3/tutorial/classes.html#python-scopes-and-namespaces (스코프와 네임스페이스 섹션 참조) -
"You Don't Know JS Yet" 시리즈 (Kyle Simpson) - Scope & Closures: 자바스크립트의 스코프와 클로저에 대해 매우 깊이 있고 철학적인 관점에서 설명하는 명저입니다. 클로저의 동작 원리를 뿌리부터 이해하고 싶다면 반드시 읽어볼 가치가 있습니다. https://github.com/getify/You-Dont-Know-JS/tree/2nd-ed/scope-closures
-
함수형 프로그래밍 관련 서적 및 강의: 클로저는 함수형 프로그래밍 패러다임의 핵심 요소 중 하나입니다. 함수형 프로그래밍에 대한 이해를 넓히면 클로저의 활용 범위를 더욱 확장할 수 있습니다.
클로저는 여러분의 코드에 유연성과 강력함을 더해주는 도구입니다. 이 개념을 제대로 이해하고 활용한다면, 더욱 견고하고 유지보수하기 쉬운 소프트웨어를 만들 수 있을 것입니다.
