코어 자바스크립트 #5 클로저

신윤철·2022년 2월 3일
0

코어자바스크립트

목록 보기
5/8
post-thumbnail

클로저

클로저란?

클로저를 정의하는 문헌들은 매우 다양하지만 책을 읽고 제가 이해한 클로저의 개념과 가장 유사한 설명은 유인동님의 설명입니다.
(책에 써있어서 고른거 아님ㅎㅎ..)

자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수 - 유인동 (함수형 자바스크립트 프로그래밍)

자신이 생성될 때 스코프에서 알 수 있었던 -> 즉 생성 당시 lexical environment에 저장된 outerEnvironmentReference을 통해 스코프 체인이 생성되고 외부 변수 탐색함으로서 알 수 있었던.

언젠가 사용할 변수들을 기억하여 유지 -> 스코프에 연결된 외부 함수의 변수들 중 사용할 변수들을 저장해 가비지 컬렉터가 수거하지 못하게 함으로서 사라지는 것을 막습니다.(==유지)

즉 내부 함수에서 외부 함수의 변수를 참조할때 외부 함수의 생명 주기가 끝났을 경우 클로저의 특성이 발동(?)됩니다.

간단한 예제를 살펴보겠습니다.

// 외부 함수의 변수를 참조하는 내부 함수
var outerCloser = function () {
  var a = 1;
  var innerCloser = function () {
    return ++a;
  };
  return innerCloser;
};
var outerCloser2 = outerCloser();
console.log(outerCloser2());	// 2
console.log(outerCloser2());	// 3
console.log(outerCloser.a); 	// undefined
  1. 내부 함수 innerCloser의 변수 a는 외부 함수 outerCloser의 a를 참조합니다. (스코프연결)

  2. 그리고 외부 함수 outerCloser은 innerCloser 함수를 반환합니다.

  3. 변수 outerCloser2를 선언하며 함수 outerCloser의 실행 값 (내부 함수 innerCloser)을 할당하면 outerCloser은 종료되고 가비지 컬렉터에 수거됩니다.)

  4. 하지만 outerCloser2을 실행함으로써 내부 함수 innerCloser을 실행하며 a의 값을 증가시키면 이 때 클로저 특성이 발생합니다.

innerCloser은 a를 갖고있지 않아 증가시키기 위해선 외부 함수인 outerCloser의 a를 참조해야합니다.

outerCloser은 이미 생명주기가 끝나 상식적으로는 사용할 수 없지만 클로저로 인해 innerCloser은 당시 참조하던 외부 변수를 저장하고 있어 사용할 수 있게 됩니다.

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

위의 예제에선 내부 함수를 return하여 외부로 전달하였지만
return 외에도 콜백 함수등 다른 방법으로도 클로저가 발생할 수 있습니다.

// callback 함수를 통해 클로저 발생
var outer = function () {
  var a = 0;
  var intervalId = null;
  var inner = function () {
    if (++a >= 10) {
      clearInterval(intervalId);
    }
    console.log(a);
  };
  intervalId = setInterval(inner, 1000);
};
var outerCloser = outer;
outerCloser();					// 1, 2, 3, 4 ... 클로저 발생

클로저와 메모리 관리

앞선 클로저의 정의에서 배웠듯이 클로저는 실행 컨텍스트가 종료된 이후에도 사용할 변수를 저장하여 사용합니다.

그런데 클로저를 잘 알지 못하고 사용하면 Garbage Collector가 수거하지 못하는 메모리가 쌓여 메모리 누수(소모)가 발생하게 됩니다.

이번 파트에서는 클로저의 사용할때 메모리를 관리하는 방법에 대해 배워보겠습니다.

클로저는 의도적으로 외부 함수의 지역 변수를 사용하여 가비지 컬렉터가 지역 변수를 수거하지 못하게 하므로써 메모리를 사용합니다.

이를 관리하는 방법은 간단합니다. 클로저에서 더 이상 필요성이 없는 지역 변수의 참조 카운터를 0으로 만들어 가비지 컬렉터가 수거하게 하면 됩니다.

그렇다면 참조 카운터를 0으로 만들 수 있을까요?

해당 식별자에 null이나 undefined를 할당하면 됩니다.

앞서 사용한 예제에서 메모리관리의 방법을 다시 살펴보겠습니다.

// 내부 함수의 메모리 관리
var outerCloser = function () {
  var a = 1;
  var innerCloser = function () {
    return ++a;
  };
  return innerCloser;
};
var outerCloser2 = outerCloser();
console.log(outerCloser2());	// 2
console.log(outerCloser2());	// 3
outerCloser2 = null; 			// outerCloser2 함수의 inner 함수 참조를 끊음
// callback 함수의 클로저 메모리 관리
var outer = function () {
  var a = 0;
  var intervalId = null;
  var inner = function () {
    if (++a >= 10) {
      clearInterval(intervalId);
      inner = null;					// inner 식별자의 함수 참조를 끊음
    }
    console.log(a);
  };
  intervalId = setInterval(inner, 1000);
};
var outerCloser = outer;
outerCloser();					// 1, 2, 3, 4 ... 클로저 발생

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

정보 은닉은 어떤 모듈의 내부 로직을 최대한 감춰서 모듈간의 결합도를 낮추고 유연성을 높이는 개발 방법입니다.

잘 알려진 접근 권한에는 public, private, protected의 세 종류가 있습니다.

  • public : 모든 영역에서 접근 가능한 것
  • private : 내부에서만 접근 가능하고 외부로 노출되지 않는 것
  • protected : 자신과 자식 객체에서만 접근 가능한 것

하지만 자바스크립트는 기본적으로 변수에 이러한 접근 권한을 부여할 수 없도록 설계되어있습니다. (public var a = 10 이런식으로 사용하지 않음)

그렇다고 접근 권한 제어 방법이 없는 것은 아닌데, 클로저를 이용하면 됩니다.


간단한 객체를 통해 클로저를 사용한 접근 권한 제어에 대해 알아보겠습니다.

// 자동차 객체
var car = {
  fuel: Math.ceil(Math.random() * 10 + 10), 	// 연료(L) : 10 ~ 20
  power: Math.ceil(Math.random() * 3 + 2), 		// 연비(km/L) : 3 ~ 5
  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 객체에 연료, 연비를 무작위로 주고 run 메소드를 통해 경주 게임을 즐길 수 있는 예제입니다.

하지만 이 코드로 게임을 즐기기엔 문제가 있습니다.
만약 해커가 car.fuel = 10000; car.power = 100; 이런 방식으로 객체를 수정하면 게임이 성립되지 않기 때문입니다.

이렇게 외부에서 내부의 값을 변경할 수 없도록 클로저를 통해 접근 권한을 줘야겠습니다.

var createCar = function () {
  var fuel = Math.ceil(Math.random() * 10 + 10); // 연료(L) : 10 ~ 20
  var power = Math.ceil(Math.random() * 3 + 2); // 연비(km/L) : 3 ~ 5
  var moved = 0;
  return {
    get moved() {
      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 = creatCar();
  • run 메서드에 외부 함수의 변수 fuel, power을 사용함으로써 클로저를 이용하였고 그 결과 외부에서 내부의 비공개 변수들을 변경할 수 없도록 하였습니다.

  • moved와 run 메서드는 return 값으로 주어 외부에서 접근은 가능하지만 moved는 getter만을 부여해 조회만 가능하게 하였고, run메서드는 실행할 수 있도록 하였습니다.

클로저를 사용한 결과 car.fuel = 10000; car.power = 100; 같은 외부에서의 변경 공격을 막을 수 있었습니다.

하지만 run 메서드를 다른 내용으로 덮어 씌우는 어뷰징 공격은 여전히 가능한 상태입니다.

var createCar = function (){
  ...
  var publicMembers = {
    ...
  };
  Object.freeze(publicMembers);
  return publicMembers;
};

return 할 값들을 미리 publicMembers 객체에 담고 freeze 메서드를 통해 변경할 수 없도록 조치하면 어뷰징 공격도 막을 수 있습니다.

마지막으로 클로저를 활용하여 접근권한을 제어하는 방법을 정리하자면

  1. 객체가 아닌 함수에서 지역변수 및 내부함수 등을 생성합니다.

  2. 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터(여럿일 때는 객체또는 배열 하나일 때는 함수)를 return합니다.

-> return한 변수들은 공개 멤버(public)가 되고, 그렇지 않은 변수들은 비공개 멤버(private)가 됩니다.

정리

  • 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부 함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 후에도 메모리에서 해당 변수가 사라지지 않는 현상입니다.

  • 내부 함수를 외부로 전달하는 방법에는 1. return, 2. 콜백 함수 가 있습니다.

  • 클로저는 그 본질이 메모리를 계속 차지하는 개념이므로 더는 사용하지 않게 된 클로저에서는 계속해서 메모리를 차지하지 않도록 관리해줄 필요가 있습니다. (null 할당)

profile
기본을 탄탄하게🌳

0개의 댓글