클로저

정수·2023년 3월 12일
0

JavaScript

목록 보기
8/15
post-thumbnail

클로저란?

클로저(closure)는 함수형 프로그래밍 언어에서 등장하는 보편적인 특성이기에 다양한 문헌에서 제각각 클로저를 다르게 설명하고 있습니다. 본질을 깨닫고 나면 쉬운 개념인데도 어딘가 갈증이 해소되지 않는 기분을 느끼기도 쉬운 개념이 바로 클로저입니다.

예시를 통해 클로저가 어떤 상황일때 발생하는 현상인지 살펴봅시다.

var outer = function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner; // not "return inner();"
};
var outer2 = outer();
console.log(outer2());
  1. outer 함수의 실행 컨텍스트가 활성화되면 LexicalEnvironment에 식별자들(a, inner)에 대한 정보를 저장합니다.
  2. outer2 변수는 outer의 실행 결과인 inner 함수 자체를 참조하게 되며, outer 함수는 실행을 종료합니다.
  3. outer2 함수의 실행 컨텍스트가 활성화되면 inner의 실행 컨텍스트가 바로 활성화됩니다.
  4. inner 함수는 식별자 a를 찾기 위해 outerEnvironmentReference에 지정된 상위 컨텍스트인 outer의 LexicalEnvironment에 접근해서 식별자 a를 찾습니다.
  5. inner 함수는 2를 출력하며 실행을 종료합니다.
  6. 출력된 값을 로그로 출력하며 종료됩니다.

사실 위 단계에서 이상한 점이 있습니다. inner 함수의 실행 시점(3단계)에는 outer 함수는 이미 종료된 상태(2단계)인데 어떻게 outer 함수의 LexicalEnvironment에 접근(4단계)할 수 있었을까요?

다른 관점으로 보면, outer 함수가 종료되는 시점(2단계)에 식별자들(a, inner)에 대한 참조를 지웠기에 가비지 컬렉터의 수집 대상이 되었지만 그 이후에도 어떻게 해당 식별자에 접근할 수 있었을까요?

사실 가비지 컬렉터는 어떤 값을 참조하는 변수가 호출될 가능성이 있다면 그 값은 수집 대상에 포함시키지 않습니다. 위에서 outer 함수가 종료되었다고 해도 외부함수인 outer2 함수에 의해 inner 함수가 호출될 가능성이 열렸기 때문에 식별자 a는 사라지지 않았습니다. 바로 이러한 현상이 클로저입니다.

정리하자면, 클로저함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우, A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상을 말합니다.

클로저와 메모리 관리

변수 a를 다 사용했음에도 불구하고 사라지지 않으면 메모리 누수가 발생합니다. 그렇기 때문에 필요성이 사라진 시점에는 참조 카운트를 0으로 만들어 GC의 수집 대상에 포함시키면 됩니다. 참조 카운트를 0으로 만드는 방법은 식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefined)를 할당하면 됩니다.

var outer = function () {
  var a = 1;
  var inner = function () {
    return ++a;
  };
  return inner;
};
var outer2 = outer();
console.log(outer2());

outer = null; // 기본형 데이터 할당

Q1. 왜 outer에 기본형 데이터를 할당했을까? outer2inner에 대해 기본형 데이터를 할당했더라면?
Q2. 함수는 데이터 할당을 어떻게 할까?


활용 사례

01. 콜백 함수 내부에서 외부 데이터 사용

var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

fruits.forEach(function (fruit) { // (A)
  var $li = document.createElement('li');
  $li.innerText = fruit;
  $li.addEventListener('click', function () { // (B)
    alert(fruit);
  });
  $ul.appendChild($li);
});
document.body.appendChild($ul);

(B) 함수가 참조할 예정인 변수 fruit에 대해서는 (A)가 종료된 후에도 GC 대상에서 제외되어 계속 참조 가능합니다. 그러나 (B) 함수의 쓰임새가 콜백 함수에 국한되지 않는 경우라면 반복을 줄이기 위해 외부로 분리하는 편이 나을 수도 있을 것입니다 .

...
var alertFruit = function (fruit) { // (B)
  alert(fruit);
};

fruits.forEach(function (fruit) { // (A)
  ...
  $li.addEventListener('click', alertFruit);
  ...
});
...
alertFruit(fruits[1]);

그런데 여기서 li를 클릭하면 과일명이 아닌 [object MouseEvent]라는 값이 출력됩니다. 콜백 함수인 (B)의 인자에 대한 제어권을 addEventListener가 가진 상태이며, 이 함수는 콜백 함수를 호출할 때 첫 번째 인자에 '이벤트 객체'를 주입하기 때문입니다.

이 문제는 bind 메서드를 활용하여 넘겨줄 인자를 직접 지정할 수 있습니다.

...
fruits.forEach(function (fruit) { // (A)
  ...
  $li.addEventListener('click', alertFruit.bind(null, fruit));
  ...
});
...

다만 bind 함수는 첫 번째 인자(새로 바인딩할 this)는 필수로 입력해야하기 때문에 원래의 this와 달라질 수 있다는 점, 이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점(2번째 인자로 출력) 점은 감안해야 합니다.

이런 변경사항이 발생하지 않게끔 하기 위해서는 bind 메서드가 아닌 고차함수를 활용하면 됩니다.

...
var alertFruitBuilder = function (fruit) { // (B)
  return function () {
    alert(fruit); // (C)
  };
};

fruits.forEach(function (fruit) { // (A)
  ...
  $li.addEventListener('click', alertFruitBuilder(fruit));
  ...
});
...

고차함수란 함수를 인자로 받거나 함수를 리턴하는(위 alertFruitBuilder 함수와 같은) 함수입니다.

alertFruitBuilder 함수를 실행하면서 fruit 값을 인자로 전달했고 실행 결과는 다시 함수(C)가 되며 이는 addEventListener에 콜백 함수로 전달될 것입니다.

이후 언젠가 클릭 이벤트가 발생하면 비로소 함수 (C)의 실행 컨텍스트가 열리면서 인자로 넘어온 fruitouterEnvironmentReference에 의해 참조할 수 있을 것입니다. 즉 alertFruitBuilder의 실행 결과로 반환된 함수에는 클로저가 존재합니다.

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

정보 은닉(information hiding)은 어떤 모듈의 외부 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 중요한 개념 중 하나입니다. 흔히 public, private, protected로 접근 권한을 부여하지만 Javascript에서는 기본적으로 변수 자체에 이러한 권한을 직접 부여하도록 설계되어 있지 않습니다.

하지만 방법이 없진 않죠! 클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것은 가능합니다.

var car = {
  fuel: Math.ceil(Math.random() * 10 + 10), // 연료 (L)
  power: Math.ceil(Math.random() * 3 + 2), // 연비 (km/L)
  moved: 0, // 총 이동거리
  run: function () {
    var km = Math.ceil(Math.random() * 6); // 랜덤한 주사위 숫자만큼 거리 이동
    var wasteFuel = km / this.power; // 소요되는 연료 계산
    if (this.fuel < wasteFuel) { // 현재 연료보다 많이 들어가면 "총 이동거리" 출력 후 종료
      console.log('이동불가');
      return;
    }
    this.fuel -= wasteFuel; // 이동이 가능하다면 연료 소진 후
    this.moved += km; // 이동거리 증가
    console.log(km + 'km 이동 (총 ' + this.moved + 'km)');
  }
};

위와 같이 자동차 경주 게임을 만든다고 생각해봅시다. car 변수에 객체를 직접 할당했습니다. fuelpower는 무작위로 생성하고, moved라는 프로퍼티에 총 이동거리를 부여했으며, run 메서드를 실행할 때마다 car 객체의 fuel, moved 값이 변하게 했습니다.

하지만 fuelmoved, power의 값을 변경할 수 있는, 조작할 수 있는 방법이 존재합니다.

car.fuel = 10000;
car.power = 100;
car.moved = 1000;

이렇게 게임의 핵심적인 값들을 직접 변경할 수 있으면 게임의 존재 이유가 사라지겠죠. 이런 값들을 방어하기 위해선 객체가 아닌 함수로 만들고, 필요한 멤버만을 return 하게 한다면 공평하게 게임을 진행할 수 있을 것입니다.

var createCar = function () {
  var fuel = Math.ceil(Math.random() * 10 + 10); // 연료 (L)
  var power = Math.ceil(Math.random() * 3 + 2); // 연비 (km/L)
  var moved = 0; // 총 이동거리
  return {
    get moved () { // getter 만을 부여 (읽기 전용 속성)
      return moved;
    },
    run: function () {
      var km = Math.ceil(Math.random() * 6);
      var wasteFuel = km / power;
      if (fuel < wasteFuel) {
        console.log('이동불가');
        return;
      }
      fuel -= wasteFuel;
      moved += km;
      console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel);
  	}
  };
};
var car = createCar();

그럼 아래와 같이 fuel, power, moved에 대한 값을 변경하지 못하지만 run에 대한 값은 여전히 변경이 가능합니다.

car.fuel = 1000;
console.log(car.fuel); // 1000 
car.run() // 3km 이동(총 3km), 남은 연료: 17.4

car.run = function () { console.log('changed'); };
car.run(); // 'changed'

이런 어뷰징까지 막기 위해서는 Object.freeze를 사용하여 return 하기 전에 변경할 수 없게끔 조치를 취해야 합니다.

var createCar = function () {
  ...
  var publicMembers = {
    get moved () { // getter 만을 부여 (읽기 전용 속성)
      return moved;
    },
    run: function () {
      ...
  	}
  };
  Object.freeze(publicMembers);
  return publicMembers;
};
var car = createCar();

freeze 된 객체를 car에 할당했기 때문에 car에 할당된 값(run 함수)은 더이상 변경(추가, 수정 및 삭제)이 불가합니다. 이 정도면 충분히 안전한 객체가 되었습니다. 내용을 정리하자면 아래와 같습니다.

  1. 함수에서 지역변수 및 내부함수 등을 생성합니다.
  2. 참조형 데이터를 공개하기 위해선 대상이 여럿일 때는 객체 또는 배열을 return 하고, 하나일 때는 함수를 return합니다.
  3. return된 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 됩니다.
profile
해피한하루

0개의 댓글