재귀 알고리즘 실행 컨텍스트
Javascript 실행 함수는 실행 컨텍스트(Execution Context)에 저장된다. 함수가 호출 될 때 실행 컨텍스트에 함수 실행에 대한 정보를 차곡차곡 쌓아나간다. 함수 실행 시 제어 흐름의 위치, 변수의 현재 value, this 값 등 다양한 내부 정보를 담고 있다.
실행 컨텍스트는 메모리를 사용한다. 재귀 알고리즘은 깊이가 깊어질 수록 콜 스택에 함수 정보를 쌓아나간다. 중첩된 함수가 100개 200개를 넘어서 1억개가 쌓인다고 하면 메모리 누수가 발생할 수 밖에 없다.
재귀 알고리즘으로 문제를 쉽게 풀 수 있지만 메모리 관리에 실패할 수 있다는 얘기다. 재귀 알고리즘 뿐만 아니라 자바스크립트에서 메모리 누수가 발생할 수 있는 요소는 여러가지가 있다. 먼저 Javascript에서 메모리 관리 메커니즘을 이해하고 왜 재귀 알고리즘이 메모리 누수에 취약한지 알아보자.
Javascript Memory
C언어 같은 저수준 언어에서는 malloc()과 free() 함수를 제공한다. 즉 개발자가 직접적으로 메모리를 관리할 수 있게 된다.하지만 Javascript 고수준 언어는 메모리 할당 및 해제가 자동으로 일어난다. Javascript의 Garbage Collector가 이 작업을 자동으로 진행하는 것이다.
프로그램에서 메모리의 생존 주기는 필요할 때 할당 하고 할당된 메모리를 사용하고 필요없어진 메모리는 해제하게 된다. 기본적으로 Garbage Collector가 이를 관리해주지만 맹목적으로 자동 메모리 관리에만 의존하면 프로그램은 망가진다.
Javascript에서 메모리 할당은 변수, 객체를 선언할 때 할당된다. 변수의 데이터를 담기 위해 메모리가 할당된다. 또한 함수를 선언하고 호출할 때는 메모리를 할당하고, 호출시 콜 스택에 함수 정보가 저장되기 때문에 추가적인 메모리 사용이 발생한다.
문제는 재귀 알고리즘에서 탈출 조건을 만족할 때 까지 함수가 호출되고, 이렇게 호출된 여러 층의 함수들이 실행 컨텐스트에 저장되기 때문에 Memory Leak 현상이 발생할 수 있다는 것이다. 만약 탈출 조건을 만날 때 까지 한참 걸리거나 아예 만나지 못하는 경우 불필요한 메모리가 상당히 필요하게 된다.
Javascript의 Garbage Collector는 참조 기준으로 '필요 없는 메모리'를 결정한다. 더 이상 해당 함수, 변수, 객체를 참조하는 코드가 존재하지 않는 경우, 메모리 할당을 해제한다. 문제는 재귀 함수를 호출하는 로직은 사라지지 않고 그대로 남게 되며, 함수를 참조 형식으로 사용하는 경우라면 Memory Leak에 대한 위험성이 남는다.
Javascript Memory Leak
메모리 누수(Memory Leak)이란 더 이상 사용하지 않는 메모리를 해제하지 못하는 상황이다. 어떤 변수나 함수가 메모리를 차지하고 있고 더 이상 사용하지 않음에도 불구하고 메모리 해제가 이뤄지지 않는다면 프로그램의 성능은 한계를 가지게 된다.
Stack vs Heap
Javascript에서 메모리는 스택 메모리와 힙 메모리로 구분된다. 스택 메모리는 단순 변수들에 사용되며, 힙 메모리는 복잡한 객체에 사용된다.
Primitive Type : Javascript에서 단순 변수란 String, Number, Boolean, Null, Undefined, Symbol 등 원시타입의 데이터들이다.
Reference Type : 복잡한 객체란 Object, Array, Function 등 참조 데이터 타입을 포함한다.
Garbage Collector 작동방식
먼저 기억해야 할 부분은 전역 변수는 Garbage Collector에 의해 메모리 해제가 이뤄지지 않는다. 따라서 모든 변수 사용이 종료된다 하더라도 외부 변수를 참조하고 있는 로직이 있다면 메모리 해제는 이뤄지지 않는다. 가급적이면 변수의 오용과 메모리 누수를 방지하기 위해서라도 로컬 스코프 변수를 사용하는게 맞다.
Function이 실행되면 실행 컨텍스트 스택에 function에 관한 변수 값과 타입 등의 정보가 기록된다.
// 함수 선언
function function1(){
// 객체 변수
let obj ={
name : 'javascript memory';
}
return obj;
}
// 전역 변수
// 함수 호출
let res = function1();
① 전역 변수 res를 위한 메모리 할당이 이뤄짐
② function1() 함수가 호출되면서 실행 컨텍스트위에 올라감.
③ 함수 내에서 객체를 선언함. Heap 메모리에 할당됨
④ function1() 이 종료되면서 스택에서 메모리가 해제됨
⑤ 전역변수에 할당된 참조변수 객체는 메모리 해제가 안됨.
위에서 처럼 전역 변수에 할당된 변수는 메모리 할당이 되지 않아 누수가 발생한다. 이처럼 재귀 알고리즘에서도 탈출 조건을 만족하지 못하는 경우를 포함해서, 전역 변수를 참조하는 로직이 포함된 경우 메모리 해제가 되지 않는다.
메모리 해제가 안되는 경우
① 해제 하지 않은 타이머 : 타이머는 특정 객체나 변수를 참조하고 있다가 값의 변경이 일어날 때 기능을 수행해야 하기 때무에 계속해서 변수를 참조하고 있는 상태다. 만약 타이머가 전체 프로그램 생명 주기동안 실행되고 있다면 메모리 누수를 피할 수 없다.
② console.log() : 메모리 할당이 되어 있기 때문에 어디서는 console.log()로 변수 값을 찍어볼 수 있는 것이다. 서버 프로그램에서 디버깅을 위해 만든 console.log()가 있다면 모두 날려버려라. 디버깅은 디버깅 툴로 진행하는 것이 맞다.
뿐만 아니라 console.log / console.error / console.info/ console.dir 등 불필요한 변수 출력 코드는 가급적이면 사용하지 않는게 맞다. 자바스크립트 코딩 스타일 스펙에서도 console 출력을 권장하지 않는다.
③ DOM 잘못된 사용 : 이미 삭제된 노드임에도 불구하고 삭제된 노드를 참조하고 있는 노드가 있다면 메모리 해제가 되지 않는다.
④ 전역 변수 사용 : Global Execution Context의 변수는 Garbage Collector에서 메모리 해제를 진행하지 않는다.
'Algorithm' 카테고리의 다른 글
[Algorithm] 하노이의 탑 알고리즘 Javascript 구현하기 (0) | 2022.05.14 |
---|---|
[Algorithm] Tail Recursion 이란? Optimization 꼬리 재귀 예제 피보나치 수열 (0) | 2022.05.13 |
[Algorithm] 재귀 알고리즘 Tree UI 구현하기 (0) | 2022.05.13 |
[Algorithm] JSON.stringify 메소드 구현하기 (0) | 2022.05.13 |
[Algorithm] 러시아 전통인형 마트료시카 재귀 알고리즘 Matryoshka Algorithm (0) | 2022.05.12 |
댓글