본문 바로가기
  • soldonii's devlog
Javascript 공부/Advanced Javascript(-)

자바스크립트 런타임 : 콜스택과 메모리 힙

by soldonii 2019. 8. 27.

*Udemy의 "Advanced Javascript Concepts" 강의에서 학습한 내용을 정리한 포스팅입니다.

*https://soldonii.github.io에서 2019년 8월 16일(금)에 작성한 글을 티스토리로 옮겨온 포스팅입니다.

*자바스크립트를 배우는 단계라 오류가 있을 수 있습니다. 틀린 내용은 댓글로 말씀해주시면 수정하겠습니다. 감사합니다. :)


1. Writing Optimized Code

최적화된 코드를 위해서, 아래 5개 키워드를 사용할 때에는 주의가 필요하다는 점을 인지하자.

  • eval()
  • arguments ⇒ destructuring을 통해 arguments 키워드를 대체할 수 있다.
  • for in ⇒ Object.key()를 사용하는 것을 추천한다.
  • with
  • delete

 

# 인라인 캐싱(Inline Caching)

// Inline Caching
function findUser(user) {
  return `found ${user.firstName} ${user.lastName}`;
}

const userData = {
  firstName: 'Johnson',
  lastName: 'Junior'
}

findUser(userData); // 'found Johnson Junior'로 대체

인라인 캐싱은 컴파일러에 의해서 자동적으로 수행되는 기능 중 하나이다. 만약 위 코드에서 findUser(userData); 함수가 1백만번 반복적으로 실행된다면, 컴파일러는 userData 객체를 1백만번 찾는 대신, findUser(userData);'found Jonhson Junior' 라는 텍스트로 대체하여 실행 속도를 대폭 향상시킨다.

 

# 히든 클래스(Hidden Classes)

// hidden classes
function animals(x, y) {
  this.x = x;
  this.y = y;
}

const obj1 = new animals(1, 2);
const obj2 = new animals(3, 4);

obj1.a = 30;
obj1.b = 100;

obj2.b = 30;
obj2.a = 100;

위의 경우, animals라는 객체의 값을 설정한 순서에 맞춰서 추후에 선언할 객체 또한 순서를 동일하게 정렬해야 한다. Hidden Class는 컴파일러가 내부적으로 활용하는 도구 중 하나로, obj1과 obj2를 선언할 때 컴파일러은 두 객체가 동일한 hidden class를 공유한다고 인지한다. 그런데 그 이후에 obj1와 obj2 객체의 값을 다른 순서로 부여하면 컴파일러가 헷갈려서 두 객체가 동일한 class를 공유한다고 인지하지 않게 되면서 최적화에 지장을 준다.

 

delete 키워드를 사용할 때 주의해야 하는 이유도 이와 맞닿아 있다. 예를 들어서 delete obj1.x = 30; 코드로 객체의 key를 삭제할 경우, obj1과 obj2는 더 이상 동일한 hidden class를 공유하지 않기 때문에 최적화가 어려워진다.

 

2. 콜스택과 메모리 힙

현재까지 배운 자바스크립트의 기초도 당연히 매우 중요하지만, 사실 매일매일 실무를 할 때 맞부딪히게 될 일은 아닐 수 있다. 하지만 콜스택과 메모리 힙은 매일 일하면서 고려해야 하는 요소이며, 시니어 개발자라면 반드시 알아야만 하는 요소이다.

 

자바스크립트 엔진이 구동되면서 코드를 읽고 실행하는 과정에서 아주 중요한 두가지 단계가 있는데, ① 정보(ex. 변수, 함수 등)를 특정한 장소에 저장하는 것과 ② 실제 현재 실행되고 있는 코드를 트래킹하는 작업이 그 두가지이다. 여기서 정보를 저장하는 공간(Memory Allocation이 발생하는 공간)이 바로 메모리 힙(Memory Heap)이고, 실행 중인 코드를 트래킹하는 공간콜 스택(Call Stack)이다.

 

# 메모리 힙(Memory Heap)

자바스크립트 엔진 속 메모리 힙

 

예를 들어 const someNum = 610; 라는 코드는 자바스크립트 엔진에게 "헤이! someNum이라는 변수를 위해서 너 메모리 공간 하나만 좀 내줄래? 그리고 그 공간에다가 610이라는 값을 좀 보관해주라!"라고 얘기하는 행위이다. 다른 예시로 const human = {first: 'hyunsol', last: 'do'}; 라는 코드는 "야! human이라는 변수가 들어갈 공간 좀 또 마련해주라. 그리고 거기에다가 저 객체 좀 보관해줄래? 만약에 내가 나중에 human 어딨냐고 물어보면 주소 좀 알려줘!" 와 같은 의미이다.

변수, 함수 저장, 호출 등의 작업이 발생하는 이 공간이 바로 메모리 힙이다. 쉽게 생각하면 'Memory Heap'이라는 이름의 창고가 있고, 변수나 함수들은 겉에 이름이 라벨지로 붙어있는 박스들인거다.

 

# 콜스택(Call Stack)

자바스크립트 엔진 속 콜스택

 

콜스택은 메모리에 존재하는 공간 중의 하나로, 코드를 읽어내려가면서 수행할 작업들을 밑에서부터 하나씩 쌓고, 메모리 힙에서 작업 수행에 필요한 것들을 찾아서 작업을 수행하는 공간이다. 예시를 하나 들어서 작업 순서를 구체적으로 살펴보자.

function subtractTwo(num) {
  return num - 2;
}

function calculate() {
  const sumTotal = 4 + 5;
  return subtractTwo(sumTotal);
}

calculate();

 

위 함수의 실행 순서는 아래와 같다.

 

① 자바스크립트 파일이 실행되면서 동시에 (anonymous)라는 함수가 콜스택의 가장 아래에 들어온다. 이를 Global Execution Context라고 하는데 나중에 자세히 살펴보자. 함수 이름이 (anonymous)인 이유는 자바스크립트 파일의 이름이 현재 없어서 그렇다.

② 가장 아래에 작성된 코드인 calculate() 함수를 읽음과 동시에 이 함수가 콜스택에서 두번째 밑으로 들어온다. 그리고 이를 수행하기 위해 calculate() 함수가 선언된 코드 안 쪽으로 들어가게 된다.

③ sumTotal을 계산해서 그 값을 얻어낸 후 다음 줄로 내려간다.

④ 이 코드를 실행하기 위해서는 subtractTwo() 함수를 알아야하기에 당장 calculate() 함수가 콜스택에서 제거되지 못하고, 대신 subtractTwo() 함수가 콜스택에 새롭게 쌓인다. 그리고 이 함수를 실행하기 위해 함수가 정의된 곳으로 간다.

 

⑤ subtractTwo() 함수의 값을 계산하여 값 7을 얻어냈다.

⑥ 계산된 값을 토대로 subtractTwo() 함수를 실행했으므로 이 함수는 콜스택에서 제거된다.

⑦ calculate() 함수 또한 실행에 필요한 모든 값을 얻었으므로, 실행되고 콜스택에서 제거된다.

⑧ 마지막으로 본 파일의 실행이 모두 끝났으므로 Global Execution Context인 (anonymous) 함수 또한 콜스택에서 제거되고, 모든 수행이 종료된다.

 

위 과정을 토대로, 콜스택을 통해 현재 코드가 어디에 위치해 있는지를 트래킹 할 수 있다는 점을 알 수 있다. stack frame(분홍색 조각들)을 통해 현재 코드의 어디에 위치해 있는지 알 수 있고, 메모리 힙을 참고하여(노란색 화살표) 코드 실행에 필요한 변수, 함수 등의 위치를 참조하면서 실행을 한다. 참고로 간단한 변수들은 콜 스택에 저장되고, 객체, 배열, 함수 등 복잡한 데이터 구조의 값들은 메모리 힙에 저장된다.

 

3. 스택 오버플로우(Stack Overflow)

콜스택에 작업이 순차적으로 쌓이는데, 위 사례처럼 특정 작업을 수행하기 위해서 다른 작업이 필요할 경우 콜스택에서 작업이 제거되는 대신 다른 작업이 위에 추가로 쌓이게 된다. 이처럼 작업이 수행되지는 않고 계속 콜스택 위로 쌓이기만 할 경우 콜스택의 한정된 공간의 크기를 넘어서게 되는데, 이를 스택 오버플로우(Stack Overflow)라고 한다.

 

function inception() {
  inception()
}

위 함수는 내부에서 자기 자신을 다시 호출하고 있다. 이러한 형태의 함수 호출을 Recursion이라고 하는데, recursion 함수가 유용할 때도 있지만 잘못 사용될 경우 쉽게 stack overflow를 유발하는 원인이 되기도 한다.

 

4. 가비지 컬렉터(Garbage Collector)

콜스택과 메모리 힙을 배우면서 각각의 공간은 무제한이 아니고 한정적임을 알 수 있다. 공간이 무한정 하다면야 크게 효율성에 대해서 고려하지 않을 수도 있지만, 콜스택과 메모리 힙은 언제나 한정적이기 때문에 이를 효율적으로 관리할 필요가 있다. 자바스크립트는 이 공간을 효율적으로 관리하기 위해서, 더 이상 효용가치가 없다고 판단되는 변수, 함수 등을 함수 실행 종료 후 메모리 힙에서 제거하는 동작을 수행한다. 필요한 데이터만 메모리 힙에 저장함으로써 메모리를 더욱 여유롭게 관리한다. 따라서 자바스크립트는 Garbage Collected Language라고 말할 수 있으며, 이러한 역할을 수행해주는 도구를 Garbage Collector라고 한다.

 

하지만 이는 자칫 자바스크립트 개발자들에게 잘못된 인상을 심어줄 수도 있다. '스스로 메모리 관리를 해주니까 내가 따로 메모리를 신경쓸 필요는 없지않을까?' 라고 생각할 수 있지만, 언제나 그렇듯 이 또한 프로그램에 지나지 않기 때문에 완벽하지는 않다. (그러니 그런 생각은 하덜덜 말자.)

그럼 이 Garbage Collector는 어떠한 원리로 작동하는 것일까?

 

# 마크 앤 스윕 알고리즘(Mark and Sweep Algorithms)

Mark and Sweep 알고리즘

 

5. 메모리 누수(Memory Leak)

계속 메모리에 대한 이야기를 하고 있는데, 메모리 힙이 제대로 관리되지 않을 경우 메모리 공간의 범위를 넘어서서 정보들이 저장되는 경우가 생기는데, 이를 메모리 누수(Memory Leak)이라고 한다. 이는 과거에 사용되었고, 현재는 필요가 없음에도 불구하고 메모리 공간에서 제거되지 않고 공간을 차지하고 있는 현상을 의미한다.

 

let array = [];
for (let i = 5; i > 1; i++) {
  array.push[i-1];
}

위 코드의 경우, array에 계속 특정 값을 더하고 있는데 이 경우 무한 루프가 돌게 되기 때문에 메모리 누수가 발생한다. array가 사용 중이기 때문에 garbage collector가 array를 메모리 공간에서 지울 수가 없기 때문이다.

아래 세가지는 메모리 누수를 유발하는 가장 흔한 3가지 패턴들이다.

 

 global scope에서 전역 변수를 많이 만들 경우, 메모리 누수가 발생한다.

// 1. Global Variables
var a = 1;

 

 이벤트 리스너의 경우 사용이 완료되면 제거되도록 해야 하는데, 제거시키지 않을 경우 계속 이벤트 리스너가 추가되기 때문에 메모리 누수가 발생한다.

// 2. Event Listeners
var element = document.getElementById('button');
element.addEventListner('click', onClick);

 

 setInterval() 함수의 경우 일정 주기마다 특정 작업을 수행하도록 지시해주기 때문에, 이는 계속 사용 중인 것으로 간주되어 가비지 컬렉터에 의해서 제거되지 못하고 메모리 공간을 계속 차지하게 된다.

// 3. setInterval
setInterval(() => {
  // referencing objects...
})

댓글