Call Stack

혜빈·2024년 9월 15일

보충내용

목록 보기
28/38

기본 배경

  • Call Stack은 오늘날 대부분의 프로그래밍 언어들에서 사용되는 개념이니 잘 알아두어야 함

Call Stack

  • 함수를 사용할 때마다 그와 관련된 정보들이 쌓이는 곳
  • Call은 함수의 호출을 의미
  • Stack은 자료구조의 Stack을 의미

Queue와 Stack

Queue

  • 먼저 들어온 것이 먼저 나가는 First in First Out(FIFO)의 형태

Stack

  • 나중에 들어온 것이 먼저 나가는 Last in First Out(LIFO)의 형태

  • 컴퓨터 프로그램에서 함수들이 연쇄적으로 호출될 때
    Stack의 형태로 메모리에 들어왔다가 나가게 됨

  • 만약 위 코드가 실행이 되면
    제일 먼저 실행된 f1함수가 Stack의 맨 아래에 쌓이고,
    f1함수가 호출한 f2함수가 그 다음에 쌓이고,
    f2함수가 호출한 f3함수가 그 위에 쌓이게 됨
  • 실행이 종료되는 순서도
    f3함수가 종료된 시점에서 f2함수는 여전히 실행중이니까 f3함수가 f2함수보다 먼저 stack에서 나가고,
    f2함수가 종료된 시점에서 아직 f1 함수는 실행이 끝나지 않았으므로 f2 함수가 먼저 stack에서 나가고,
    f1함수가 마지막으로 나가게 됨

예제 1

function f1() {
	console.log("Enter f1");
  	f2();
  	console.log("Exiting f1");
}

function f2() {
	console.log("Entering f2");
  	f3();
  	console.log("Exiting f2");
}
function f3() {
	console.log("Entering f3");
  	console.log("Exiting f3");
}

f1();

  • callstack에 쌓이는 순서
  • f1()이 호출되는 global context인 annoymous가 제일 먼저 실행되고, f1(), f2(), f3() 순서대로 쌓임

  • 실행 종료는 f3()이 먼저 나가고, f2(), f1() 순으로 나감

예제 2

Stack frame

  • Call stack에 올라온 각 항목들을 의미
  • 각 함수들의 스코프(해당 함수에서 접근 가능한 데이터들의 범위)가 포함되어 있음
// 전역 변수
let a = 1;

function f1(b) {
  let c = 3;
  console.log(`${a} ${b} ${c}`);
  f2(4)
}

function f2(d) {
  let e = 5;
  console.log(`${a} ${d} ${e}`);
  f3(6)
}

function f3(f) {
  let g = 7;
  console.log(`${a} ${f} ${g}`); // break point 설정
}

f1(2);
  • 위 코드를 보면 최상단에 특정 함수에 속하지 않은 범위에 전역 변수 a가 하나 있고,
    각 함수들에 매개변수로 주어진 변수들 b, d, f가 있고,
    해당 함수 안에서 선언된 변수들 c, e, g가 있음

  • break point에서 실행해보면 위와 같이 4개의 스택이 쌓임
  • 각 스코프에서 접근 가능한 변수들을 확인해보면 아래와 같음

f3() 스코프에서 접근 가능한 변수들

f2() 스코프에서 접근 가능한 변수들

f1() 스코프에서 접근 가능한 변수들

  • 전역 스코프에 있는 a 변수는 어느 프레임에서든 접근 가능하고,
    각 함수에 주어진 매개변수와 지역변수가 해당 프레임의 스코프에 포함됨

예제 3

Stack overflow 오류

  • 한계 이상으로 함수가 연이어 호출되면 발생하는 오류
function f1() {
  f2();
}

function f2() {
  f1();
}

f1();
  • 이렇게 끊임없이 서로를 호출하는 경우, 허용된 범위가 초과되면 Stack overflow 오류가 발생함

예제 4

재귀함수 사용

  • 재귀함수 사용 시 반드시 종료되는 조건이 있어야 하고, 반복되는 횟수가 너무 많지 않도록 해서 stack overflow를 방지해야 함
function recursiveFunc(n) {
  console.log(`Entering: n = ${n}`);
  if (n > 0) {
      recursiveFunc(n - 1);
  }
  console.log(`Exiting: n = ${n}`);

recursiveFunc(5);
  • 처음 주어진 매개변수 n으로부터 매번 1을 빼서 스스로를 다시 호출함
  • 매개변수가 0이 되기 전까지는 call stack에 프레임들이 쌓이고, 그 이후로는 프레임들이 하나씩 빠져나감
  • 주어진 숫자가 너무 컸다면 프레임들이 쌓이는 과정에서 오류 발생 가능
  • 매개변수가 0보다 커야한다는 반복 조건이 있기 때문에 stack overflow가 발생하지 않음
  • 어떤 작업을 무한 반복해야 하거나, 많은 횟수를 반복해야 한다면 for문과 같은 반복문을 사용하면 좋음

예제 4

Stack trace

  • Call stack을 통해 오류가 발생한 지점을 보여주는 것을 stack trace라고 함
  • 어떤 함수들이 어느 순서대로 호출되는 과정에서 어디에서 오류가 발생한 것인지 찾아낼 수 있음
  • 여러 함수들이 복잡하게 얽힌 실무에서 문제의 원인을 밝혀내는데 매우 유용함
function f1() {
  f2();
}

function f2() {
  f3();
}

function f3() {
  f4();
}

function f4() {
  throw new Error("Error in f4");
}

f1();

Error bubbling

  • 프로그래밍 언어들에서 오류는 일반적으로 call stack 위에서 아래로 전파됨
  • f4 함수에서 오류가 발생하면 해당 오류가 f3함수로, f3에서 처리되지 못하면 f2로 전달되는 방식

function f1() {
  try {
    f2();
  } catch (e) {
    console.error(e, "Error caught in f1");
  }
}

function f2() {
  try {
    f3();
  } catch (e) {
    console.error(e, "Error caught in f2");
  }
}

function f3() {
  f4()
}

function f4() {
  throw new Error("Error in f4");
}

f1();

  • 이렇게 f1함수와 f2함수에 try문으로 오류에 대비하는 코드를 작성해놓으면
    오류가 아래로 전파되는 과정 중 f2함수를 먼저 만나기 때문에 f2함수의 catch문에서 오류가 처리됨

참고

profile
최강 개발자를 꿈꾸는 병아리

0개의 댓글