클로저(closure)에 대하여

클로저의 의미

클로저(closure)라는 영어단어의 사전적 의미는 ‘닫혀있음’, ‘폐쇄성’, ‘완결성’ 정도의 의미를 가집니다.
그렇다면 자바스크립트에서의 클로저는 무슨 의미를 가지고 있을까요?

클로저(closure)는 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특징입니다.
자바스크립트에서만 사용하는 개념이 아니다 보니 ECMAScript 명세에서도 클로저의 정의를 다루지 않고 있다고 합니다.
다양한 서적에서 클로저를 정의하고 있는 문장들을 한번 살펴보겠습니다.

  • 자신을 내포하는 함수의 컨텍스트에 접근 할 수 있는 함수 - 더글라스 크록포드, ‘자바스크립트 핵심 가이드’(한빛 미디어)
  • 함수가 특정 스코프에 접근 할 수 있도록 의도적으로 그 스코프에서 정의 하는 것 - 에단 브라운, ‘러닝 자바스크립트’(한빛 미디어)
  • 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출 할 수 있는 함수 - 존 레식, ‘자바스크립트 닌자 비급’(인사이트)
  • 이미 생명 주기상 끝난 외부 함수의 변수를 참조하는 함수 - 송현주, 고현준, ‘인사이드 자바스크립트’(한빛 미디어)
  • 자유변수가 있는 함수와 자유변수를 알 수 있는 환경의 결합 - 에릭 프리먼, ‘Head First Javascript Programing’(한빛 미디어)
  • 로컬 변수를 참조하고 있는 함수 내의 함수 - 야마다 요시히로, ‘자바스크립트 마스터 북’(제이펍)
  • 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수 - 유인동, ‘함수형 자바스크립트 프로그래밍’(인사이트)

MDN 에서는 클로저에 대해

“A closure is the combination of a function and the lexical environment within which that function was declared.”
직역해 보면
“클로저는 함수와 그 함수가 선언될 당시의 lexical environment의 상호관계에 따른 현상” 정도가 되겠습니다.

A함수 내부에서 B함수를 선언 했을 경우에
A에서는 B에서 선언한 변수에 접근할 수 없지만 B에서는 A에서 선언한 변수에 접근 가능합니다.
내부 함수 B가 A의 lexical enviroment를 언제나 사용하는 것은 아닙니다. 내부함수에서 외부 변수를 참조하는 경우에 한해서만 사용 가능합니다.

간단하게 한번 정리해 보자면 "어떤 함수에서 선언한 변수를 참조하는 내부 함수에서만 발생하는 현상"이라고 볼 수 있겠습니다.

예제 1
1
2
3
4
5
6
7
8
9
10
var outer = function() {
var a = 1
var inner = function () {
console.log(++a)
}
inner()
}
outer()
outer()
outer()

코드펜 링크

실행순서를 살펴보면 outer 함수에서 변수 a를 선언했고, outer의 내부함수인 inner 함수에서 a의 값을 1만큼 증가시킨 다음 출력합니다.
inner 함수에서는 a를 선언하지 않았기 때문에 스코프체인을 타고 outer 함수에 접근해서 a를 찾습니다.
그리고 4번째 줄에서 2가 출력됩니다. outer 함수가 종료되면 저장된 식별자들(a, inner)에 대한 참조를 지웁니다.
그러면 각 주소에 저장돼 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 가비지 컬렉터의 수집 대상이 됩니다.
그래서 2를 3번 출력하게 됩니다.`

예제 2
1
2
3
4
5
6
7
8
9
10
11
var outer = function() {
var a = 1
var inner = function () {
return ++a
}
return inner
}
var outerFunc = outer()
console.log(outerFunc())
console.log(outerFunc())
console.log(outerFunc())

코드펜 링크

위 예제를 살펴보면 inner함수의 실행 시점에는 outer함수는 이미 실행이 종료된 상태인데 outer함수의 Lexical environment에 접근할 수 있는 걸까요?
이는 가비지 컬렉터의 동작 방식 때문입니다. 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않습니다.
그래서 예제를 보면 outer 함수는 실행 종료 시점에 inner 함수를 반환합니다.
외부 함수인 outer 의 실행이 종료되더라도 내부 함수인 inner 함수는 언젠가 outerFunc를 실행함으로써 호출될 가능성이 열린 겁니다.
언젠가 inner 함수의 실행 컨텍스트가 활성화되면 outer함수 a 의 값을 필요로 할것이므로 수집대상에서 제외됩니다.

예제1과 2의 다른점은 변수가 가비지 컬렉터에 포함 되었느냐 제외 되었느근냐 입니다.
함수의 실행 컨텍스트가 종료돈 후에도 lexical environment가 가비지 컬렉터의 수집대상에서 제외되는 경우는 지역변수를 참조하는 내부 함수가 외부로 전달된 경우가 유일합니다.
위 현상을 토대로 클로저의 정의를 다시 고쳐보자면 “어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상”이라고 할 수 있습니다.

외부로 전달하는 방법으로는 return 외에도 Window의 메서드(setTimeout 또는 setInterval), DOM의 메서드(addEventListener) 같은 경우도
지역변수를 참조하는 내부함수를 외부로 전달 했기 때문에 클로저라고 할 수 있습니다.

클로저의 메모리 관리

클로저의 메모리 소모는 본질적인 특정일 뿐입니다. ‘메모리 누수’라는 표현은 개발자의 의도와 달리 어떤 값의 참조 카운트가 0이 되지 않아 GC(Garbage Collector)의 수거 대상이 되지 않는 경우에는 맞는 표현이지만 개발자가 의도적으로 참조 카운트를 0이되지 않게 설계한 경우는 ‘누수’라고 할 수는 없습니다.
클로저의 메모리 관리 방법은 간단합니다.
클로저는 어떤 필요의 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생합니다. 그러면 필요성이 사라진 시점에 메모리를 소모하지 않도록 해주면 됩니다. 참조 카운트를 0으로 만들면 GC가 수거 해가고 이때 소모됐던 메모리가 회수 됩니다.
참조 카운트를 0으로 만드는 방법은 식별자의 참조형이 아닌 기본형 데이터(null 이나 undefined)를 할당하면 됩니다.

예제 3 메모리 관리
1
2
3
4
5
6
7
8
9
10
11
12
var outer = function() {
var a = 1
var inner = function () {
return ++a
}
return inner
}
var outerFunc = outer()
console.log(outerFunc())
console.log(outerFunc())
console.log(outerFunc())
outer = null // outer 식별자의 inner 함수 참조를 끊음

클로저 활용 사례

콜백 함수와 클로저

콜백 함수 내부에서 외부변수를 참조하기 위한 방법으로 세가지가 있습니다.

  • 콜백 함수를 내부 함수로 선언해서 외부 변수를 직접 참조하는 방법으로 클로저를 사용하는 방법
  • bind 메서드로 값을 직접 넘겨줘서 클로저를 발생 시키지 않는 대신 여러가지 제약이 있는 방법
  • 콜백 함수를 고차함수로 바꿔서 클로저를 적극적으로 활용한 방법

접근 권한 제어(정보은닉)

정보은닉(information hiding)은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나 입니다.
흔희 접근 한에는 public, private, protected 의 세 종류가 있습니다.

  • public은 외부에서 접근 가능한 것
  • private은 내부에서만 사용하며 외부에 노출되지 않는 것
  • protected는 상속받은 클래스 또는 같은 패키지에서만 접근이 가능한 것(java)

자바스크립트는 기본적으로 변수 자체에 이러한 접근 권한을 직업 부여하도록 설계되어 있지 않습니다.
그렇다고 접근 제한이 불가능 한 것을 아닙니다. 클로저를 활용해서 함수차원에서 public한 값과 private 한 값을 구분하는 것이 가능합니다.

정보은닉
1
2
3
4
5
6
7
8
9
var outer = function () {
var a = 1
var inner = function () {
return ++a
}
return inner
}
var outerFunc = outer()
console.log(outerFunc())

outer 함수를 종료할 때 inner 함수를 반환함으로써 outer 함수의 지역변수인 a의 값을 외부에서도 읽을 수 있게 됐습니다.
외부에서는 outer라는 변수를 통해 outer함수를 실행 할 수는 있지만 outer함수 내부에는 어떤한 개입도 할 수 없습니다.
외부에서는 오직 outer함수가 return한 정보에만 접 할수 있습니다.
그러니까 외부에 제공하고자 하는 정보들을 모아서 return하고, 내부에서만 사용 할 정보들은 return 하지 않는 것으로 접근 권한 제어가 가능한것입니다.
return한 변수들은 공개맴버(public member) 가 되고, 그렇지 않는 변수들은 비공개 멤버(private member)가 되는 것입니다.

부분 적용 함수 (partially applied function)

부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가,
나중에 (n-m) 개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수 입니다.

커링 함수 (currying function)

커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말합니다.
부분 적용 함수와 다른 점은 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 합니다. 또한 중간 과정상의 함수를 실행한 결과는 그다음 인자를 받기 위해 대기만 할 뿐으로, 마지막 인자가 전달되기 전 까지는 원본 함수가 실행되지 않습니다(부분 적용 함수는 여러 개의 인자를 전달할 수 있고, 실행 결과를 재실행할때 원본 함수가 무조건 실행됩니다).

Reference

  • 코어 자바스크립트 (위키북스)

댓글

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×