포스트

Closure

클로저는 어떤 함수가 주변 상태(렉시컬 환경)에 대한 참조와 함께 번들로 묶인(포함된) 함수의 조합이다. 즉, 클로저를 사용하면 내부함수에서 외부함수의 범위에 접근할 수 있다. 자바스크립트에서는 함수가 생성될 때마다 클로저가 생성된다.

  • 클로저(closure)는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 가리킨다.
  • 클로저는 독립적인 (자유)변수를 가리키는 함수이다. 또는, 클로저 안에 정의된 함수는 만들어진 환경을 기억한다.

렉시컬 환경

함수가 어떤 변수를 볼 수 있는지는 함수를 호출한 위치가 아니라 함수를 정의한 위치로 정해진다. 이것을 렉시컬 스코프라고 한다. 클로저는 여기서 한 걸음 더 나아가, 외부 함수가 이미 반환되어 실행이 끝난 뒤에도 내부 함수가 그 환경을 계속 참조하게 한다 (MDN).

1
2
3
4
5
6
7
8
9
10
11
function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

add5와 add10은 같은 함수 본문을 공유하지만 서로 다른 렉시컬 환경을 기억한다. 하나에서는 x가 5이고 다른 하나에서는 10이다.


왜 쓰는가

클로저는 상태를 함수 안에 숨길 때 쓴다. 바깥에서는 반환된 함수를 통해서만 상태에 접근할 수 있으므로, 클래스의 private 필드와 비슷한 캡슐화를 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
function makeCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    value: () => count,
  };
}

const counter = makeCounter();
counter.increment();
counter.increment();
counter.value(); // 2
// count에 직접 접근할 방법은 없다

이벤트 핸들러, setTimeout 콜백, 함수를 반환하는 고차함수가 모두 이 성질에 기대어 동작한다.


Java의 클로저

Java의 람다식과 익명 클래스도 바깥 지역 변수를 캡처한다. 다만 캡처할 수 있는 변수는 final이거나 사실상 final인 것으로 제한된다 (Oracle). 값을 읽을 수는 있어도 캡처한 지역 변수를 바꿀 수는 없으므로, JavaScript의 makeCounter 같은 패턴은 그대로 옮길 수 없고 가변 객체(예: AtomicInteger)나 필드를 거쳐야 한다.

1
2
3
int base = 10;
Function<Integer, Integer> addBase = x -> x + base; // 가능
// base = 20; // 이 줄이 있으면 위 람다식이 컴파일 오류가 된다

주의할 점

  • 반복문과 var: var로 선언한 변수는 함수 스코프라서, 반복문 안에서 만든 클로저들이 같은 변수 하나를 공유한다. 콜백이 실행될 때는 반복이 끝난 뒤이므로 모두 마지막 값을 본다. let이나 const로 선언하면 반복마다 새 바인딩이 생긴다.
1
2
3
4
5
6
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0); // 0, 1, 2
}
  • 메모리: 클로저가 살아 있는 동안 그 클로저가 참조하는 환경도 해제되지 않는다. MDN은 클로저가 필요 없는 곳에서 함수 안에 함수를 만드는 것이 처리 속도와 메모리 사용 모두에 좋지 않다고 설명한다.
참고한 자료외부 출처 7 · 블로그 글 1

외부 출처

이 블로그의 관련 글

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다