Tag Archives: javascript

Javascript – Garbage Collection

자바스크립트의 메모리 관리는 우리에게는 보이지 않게 자동으로 실행됩니다. 우리가 원시타입의 변수나 혹은 객체, 함수를 선언할때도 모두 메모리를 사용합니다. 만약에 이러한 것들이 더이상 필요없게 된다면? 자바스크립트 엔진은 어떻게 이것들을 찾아내어 삭제할까요?

접근 가능성(Reachability)

자바스크립트 메모리 관리의 주요 개념은 접근 가능성입니다. 간단하게 말하면 “접근 가능한” 값은 어떻게든 엑세스가 가능하거나 사용할 수 있는 값임을 뜻합니다. 이들은 메모리에 유지되는것을 보장 받습니다.

  1. 명백한 이유로 삭제될 수 없는 본질적으로 값에 접근 가능한 기본 세트가 있습니다. 이런것들을 뿌리(Root)라고 부르겠습니다.
    • 현재 함수의 지역 변수와 매개변수
    • 다른 함수의 중첩 호출로 실행된 함수의 경우 현재의 스코프 체인으로 접근 가능한 변수와 매개변수
    • 전역 변수
    • (내부적으로 구현된 다른 것들도 있다;;)
  2. 기타 다른 값은 참조(Reference) 또는 레퍼런스의 참조(A chain of references)에 의해 루트에서 도달 가능한 것으로 간주합니다. 예를 들어 로컬 변수가 특정 객체를 참조하고 있고 그 객체가 또다른 객체를 참조하는 프로퍼티를 가지고 있다면 그 객체에 도달 가능하다라고 간주합니다. 그리고 그것들이 참조하는 다른 것들도 도달할 수 있습니다.

자바스크립트 엔진의 백그라운드 프로세스로 동작하는 가비지 콜렉터라는것이 있습니다. 이것은 모든 객체들을 모니터링 하며 그것들이 접근 불가능하게 되었을 때 삭제하는 작업을 수행합니다.

간단한 예제

여기에 아주 간단한 예제가 있습니다.

// user는 객체에 대한 참조를 가지고 있습니다.
let user = {
  name: "John"
};

여기에 표시된 화살표는 객체 참조를 나타냅니다. 전역 변수 “user”는 {name: “John”} 객체를 참조합니다. (여기서 우리는 짧게 존이라고 부르겠습니다). 존의 “name” 프로퍼티는 원시 타입을 저장하여 객체의 내부에 위치합니다. 여기에서 우리가 user의 값을 덮어쓰게 되면 참조를 잃게 됩니다.

user = null;

이제 존은 접근이 불가능하게 되었습니다. 여기에 접근할 방법은 없으며 아무도 존을 참조하지 않게 되었습니다. 가비지 콜렉터는 데이터를 회수하고 메모리를 비우게 됩니다.

두개의 참조

이번에는 user를 admin으로 참조를 복제하였다고 상상 해 보겠습니다.

let user = {
  name: "John"
};

let admin = user;

이제 우리는 똑같은 작업을 한번 더 해보겠습니다.

user = null;

하지만 여전히 admin 변수가 존을 참조하고 있으므로 메모리에 유지됩니다. 우리가 admin 도 다시 덮어쓴다면 그때 삭제 될 것입니다.

상호 연결된 객체

이번엔 좀 더 복잡한 예제를 살펴보겠습니다. 가족을 예로 들어보겠습니다.

function marry(man, woman) {
  woman.husband = man;
  man.wife = woman;

  return {
    father: man,
    mother: woman
  }
}

let family = marry({
  name: "John"
}, {
  name: "Ann"
});

marry는 두개의 객체를 서로 참조하게 하고 이 둘을 참조하고 있는 새로운 객체를 반환하는 “결혼”(?)을 시키는 함수입니다. 메모리 구조의 결과는 다음과 같습니다.

모든 객체는 서로 접근 가능하게 되었습니다. 이제 두개의 참조를 삭제해보겠습니다.

delete family.father;
delete family.mother.husband;

이 두개의 참조중에 하나만 삭제할 경우에는 여전히 모든 객체가 접근 가능하므로 객체가 삭제되기에 충분하지 않습니다. 하지만 둘 모두를 삭제할 경우 존을 참조하는 참조는 더이상 존재하지 않게 됩니다.

존이 여전히 가지고 있는 바깥으로 향하는 참조는 상관 없습니다. 오로지 안으로 들어오는 참조만이 존을 참조가능한 상태로 만들어줍니다. 그러므로 존은 이제 접근 불가능하게 되었고 메모리에서 제거되게 될 것입니다. 가비지 콜렉션이 동작한 이후에는 다음과 같이 됩니다.

접근 불가능한 섬

외부에서 접근 불가능한, 자기들끼리만 상호 참조하여 만들어진 완벽한 형태의 섬도 메모리에서 삭제 가능합니다. 소스코드는 위와 동일하다고 가정하고 다음의 코드를 실행하도록 하겠습니다.

이 예제는 접근 가능성에 대한 매우 중요한 개념을 보여주는 데모입니다. 명백하게 존과 앤은 연결되어있습니다. 그 둘은 안/밖으로 연결되는 링크들 모두를 가지고 있습니다. 하지만 이것만으로는 충분하지 않습니다. 이전의 family 객체는 루트(Root)와의 연결이 끊어지게 되었습니다. 그러므로 이 완벽한 섬은 접근 불가능하게 되었으며 삭제될 것입니다.

내부 알고리즘

기본적인 가비지 콜렉션의 알고리즘은 마크 앤 스윕(Mark-and-sweep) 이라고 불립니다. 일반적으로 가비지 콜렉션은 다음의 과정을 거칩니다.

  • 가비지 콜렉터는 루트를 획득하여 그들을 마크(기억)합니다.
  • 그리고 그들이 참조하고 있는 모든 것들에 방문하여 마크합니다.
  • 그리고 마크한 모든 객체에 방문하여 그들의 참조 역시 마크합니다. 모든 객체들을 기억하고 나면 미래에는 같은 객체를 두번 방문하지 않습니다.
  • 루트로부터 접근 가능한 방문하지 않은 참조가 있다면 계속해서 반복합니다.
  • 마크되지 않은 모든 객체는 삭제됩니다.

예를 들어 다음과 같은 객체의 구조가 있다고 해보겠습니다.

우리는 오른편에 “접근 불가능한 섬”을 발견할 수 있습니다. 이제 가비지 콜렉터가 진행하는 마크 앤 스윕 과정이 이것을 어떻게 다루는지 보겠습니다. 다음은 루트로부터 첫번째 과정을 거친 결과입니다 :

이후에 그들의 참조들도 마크합니다 :

그리고 그들의 참조도 반복합니다. 가능할때까지 :

이제 방문할 수 없는 객체들은 접근 불가능한것으로 간주되어 삭제될 것입니다.

이것이 가비지 콜렉터가 동작하는 개념입니다. 자바스크립트 엔진은 어플리케이션의 실행에 영향을 주지 않고 빠르게 수행되도록 하기 위해 많은 최적화 옵션을 적용하고 있습니다.

  • 세대별 수집 – 객체는 “새로운 객체”와 이전 객체” 두개의 세트로 나뉘어집니다. 많은 객체들은 나타나고 그들의 일을 수행하고 빨리 죽습니다. 이것들은 공격적으로 청소될 수 있을 것입니다. 하지만 충분히 오래 살아남은 객체들은 “오래된” 객체가 되어 덜 자주 검사를 받게 됩니다.
  • 증분 수집 – 많은 객체가 있고 이러한 많은 객체를 한번에 전부다 방문하며 마크하는 과정을 거치게 되면 실행에 눈에 띄는 지연이 발생할 수 있습니다. 그래서 엔진은 이러한 수거 작업을 여러 조각으로 나누어 수행합니다. 이렇게 나눈 조각의 변화를 추적할 수 있도록 부가적인 처리가 필요하지만 큰 한번의 딜레이 대신에 짧은 단위의 딜레이를 가질 수 있습니다.
  • 유휴 시간 수집 – 가비지 콜렉터는 CPU가 유휴상태일 때만 실행되어 어플리케이션의 실행에 끼치는 영향을 줄입니다.

이것 말고도 가비지 콜렉터의 다른 최적화 기능들이 있습니다. 각 엔진들이 구현하고 있는 추가적인 테크닉들이 있으며 엔진의 발전에 따라 지속적으로 변화하고 있습니다.

정리

알아야 하는 핵심은 다음과 같습니다.

  • 가비지 콜렉션은 자동으로 실행됩니다. 우리는 이것을 강제로 실행하거나 막을 수 없습니다.
  • 객체는 그들이 접근 가능한 동안 메모리에 유지됩니다.
  • 참조가 된다는 것이 루트(Root)에서 참조 가능한것과 같은 말은 아닙니다 : 상호 참조하고 있는 객체들이 전체에서 보면 참조 불가능할 수 있습니다.

참고 : https://javascript.info/garbage-collection

Javascript – 현대 모드 “use strict”

오랜 기간동안 Javascript는 호환성 문제 없이 진화하고 있었습니다. 새로운 기능은 언어에 추가되어갔지만 이전의 기능들은 변화가 없었습니다. 그것은 기존의 코드들의 동작을 깨뜨리지 않는다는 장점이 있었습니다. 하지만 Javascript 제작자가 저지른 실수나 불완전한 결정이 언어에 영원히 머무르게 되었다는 단점이 있습니다.

이러한 단점은 2009년에 ECMAScript 5 (ES5)가 등장할 때 까지 존재하였습니다. 여기에는 언어에 새로운 기능이 추가되고 기존 기능 중 일부가 수정되었습니다. 기존의 오래된 코드들이 동작하도록 하기 위해 이러한 수정 사항은 기본적으로 해제되어있습니다. 이것을 활성화 하기 위해서는 “use strict”를 사용하여 명시적으로 활성화 하여야 합니다.

“use strict”

이 지시자는 “use strict” 또는 ‘use strict’ 라는 문자열로 표현됩니다. 이것이 스크립트의 최상단에 위치하게 되면 모든 스크립트는 “현대적인” 방법으로 동작하게 됩니다. 예를 들면 다음과 같습니다.

"use strict";

// this code works the modern way
...

이러한 지시자는 스크립트의 시작 지점이 아니라 함수의 시작 지점에도 넣을 수 있습니다. 이렇게 할 경우 스크립트 전체가 아닌 해당 함수에만 엄격(Strict) 모드가 적용 됩니다. 하지만 일반적으로 사람들은 모든 스크립트에 적용되도록 사용합니다.

(주의) “use strict”는 최상단에 위치해야 합니다.

항상 “use strict”는 스크립트의 최상단에 위치하도록 해주십시오, 그 외의 경우에는 엄격 모드(Strict Mode)가 활성화 되지 않습니다. 다음은 엄격 모드가 활성화 되지 않는 예시입니다.

alert("some code");
// 아래에 있는 "use strict"는 무시됩니다. 최상단에 선언 되어야 합니다.

"use strict";

// 엄격 모드 (Strict Mode)는 활성화되지 않습니다.

(주의) use strict를 취소할 수 있는 방법은 없습니다.

“no use strict” 와 같은 혹은 비슷한 지시자는 존재하지 않습니다. 한번 엄격 모드가 활성화 되면 그것을 돌이킬 수는 없습니다.

항상 “use strict”를 사용합시다.

“use strict” 와 “default/sloppy” 모드의 차이점에 대해서 알아 볼 필요가 있습니다. 가령 다음과 같은 차이를 발견할 수 있습니다.

1) 일반적으로 어떠한 변수를 사용하기 위해서는 그것을 먼저 선언할 필요가 있습니다. 하지만 과거의 자바스크립트에서는 let 또는 var 없이도 값을 할당하는것만으로도 변수를 선언하는 것이 가능했습니다. 심지어 요즘의 자바스크립트에서도 “use strict” 없이는 여전히 가능합니다. 이러한 동작은 과거의 스크립트와의 호환성을 위한 모습입니다.

// "use strict" 를 사용하지 않은 예제입니다.

num = 5; // 변수 "num"은 존재하지 않음에도 생성됩니다.

alert(num); // 5

이것은 굉장히 나쁜 사용 예이며 엄격 모드(Strict Mode)에서는 오류를 발생하게 됩니다.

"use strict";

num = 5; // 에러: num is not defined

2) 또한 엄격 모드에서는 값의 할당에 대해 발생할 수 있는 오류들을 잡아주는 역할을 합니다.

"use strict";

// 쓰기 금지된 프로퍼티에 값 쓰기 시도
var obj1 = {};
Object.defineProperty(obj1, "x", { value: 42, writable: false });
obj1.x = 9; // throws a TypeError

// Getter 밖에 없는 변수에 대한 값 할당 시도
var obj2 = { get x() { return 17; } };
obj2.x = 5; // throws a TypeError

// 확장 불가능한 객체에 대한 새로운 값 할당 시도
var fixed = {};
Object.preventExtensions(fixed);
fixed.newProp = "ohai"; // throws a TypeError

3) 엄격 모드에서는 삭제 불가능한 프로퍼티를 삭제하려고 시도할 경우 오류를 발생시킵니다. 이전에는 아무런 효과가 없던 코드입니다.

'use strict';
delete Object.prototype; // throws a TypeError

4) Gecko 34 버전 이전 브라우저들의 엄격 모드에서는 오브젝트의 모든 프로퍼티 이름은 고유해야 했습니다. 일반적인 코드에서 프로퍼티의 이름이 중복될 경우 마지막에서 할당한 값이 최종 값으로 결정되도록 하였습니다. 만약 마지막 값을 수정해야 하는데 실수로 그 이전 (중복된 이름의) 프로퍼티 값을 수정할 경우 정상적으로 적용이 안될 수 있으며 이는 엄격 모드에서 오류를 발생하게 변경되었습니다. (하지만 ECMAScript 2015에서 오류를 발생하지 않는것으로 원복되었습니다)

'use strict';
var o = { p: 1, p: 2 }; // !!! syntax error

5) 엄격 모드에서는 함수의 파라미터 이름이 고유해야 합니다. 보통의 코드들에서는 파라미터명이 중복될 경우 마지막 파라미터의 존재가 동일한 이름을 가진 이전 파라미터를 감추게 됩니다. 이 감춰진 파라미터는 arguments[i]로 접근할 수 있으므로 완벽하게 감추어진것도 아닙니다. 이는 의도적으로 감추고자 하더라도 완벽하게 수행될 수 없으므로 옳은 방법이 아닙니다. 그러므로 엄격 모드에서는 파라미터 명이 중복 될 경우 문법 오류가 발생합니다.

function sum(a, a, c) { // !!! syntax error
  'use strict';
  return a + a + c; // 만약 이 코드가 실행된다면 잘못된 동작을 하게 됨
}

6) ECMAScript 5의 엄격 모드에서는 8진 표현법의 사용을 금지합니다. 8진 표기법은 ECMAScript 5의 일부인것은 아니지만 모든 브라우저에서 앞에 0을 붙임으로써 표현이 가능합니다. (예: 0644 === 420 및 “\045” === “%”) ECMAScript 5에서의 8진 표기는 앞에 “0o”를 붙임으로써 지원 가능합니다.

var a = 0o10; // ES2015: Octal

초보 개발자들은 때때로 앞자리에 0을 붙이는 것이 특별한 의미가 없다고 생각하여 코드상의 정렬들 미관을 위해 사용합니다. 하지만! 값의 의미가 변경되게 됩니다. 앞자리에 0를 붙여 8진수로 만드는 것은 흔한 경우는 아니며 실수를 유발할 가능성이 더 크므로 엄격 모드에서는 오류를 발생시킵니다.

'use strict';
var sum = 015 + // !!! syntax error
          197 +
          142;

var sumWithOctal = 0o10 + 8;
console.log(sumWithOctal); // 16

7) ECMASCript 5의 엄격 모드에서는 원시 타입의 변수에 프로퍼티를 추가하는것을 허용하지 않습니다. 엄격 모드가 비활성화 상태일 경우 이러한 코드는 단순히 무시됩니다. 하지만 엄격 모드에서는 TypeError를 발생시킵니다.

(function() {
'use strict';

false.true = '';         // TypeError
(14).sailing = 'home';     // TypeError
'with'.you = 'far away'; // TypeError

})();

참고 :

  1. https://javascript.info/strict-mode
  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode