함수가 끝나면 사라지는 값과 그 뒤에도 남는 값은 무엇이 다를까요? 책상에 펼쳐 둔 메모지는 일이 끝나면 치우지만, 창고에 넣어 둔 상자는 누군가 찾을 때까지 그대로 남습니다.
프로그램이 쓰는 메모리는 크게 두 구역으로 나뉩니다. 스택과 힙입니다. 둘의 차이는 "언제 사라지는가"와 "크기를 미리 아는가"에서 옵니다. 이 구분을 정확히 이해하면, 왜 어떤 값은 함수가 끝나면 사라지고 어떤 값은 남는지, 왜 큰 객체를 함수에 넘겨도 느리지 않은지, 그리고 왜 메모리 문제는 대부분 힙에서 생기는지가 한꺼번에 풀립니다.
함수가 호출될 때마다 스택에 프레임이 하나 쌓입니다. 그 안에는 세 가지가 들어갑니다. 첫째 매개변수, 둘째 그 함수의 지역변수, 셋째 함수가 끝나면 돌아갈 복귀 주소입니다. 크기는 함수가 컴파일될 때 이미 정해집니다 — 지역변수가 몇 개인지 코드를 보면 알 수 있으니까요. 그래서 스택은 관리가 아주 쌉니다. 포인터를 위로 올리면 할당, 아래로 내리면 해제입니다.
반대로 배열·객체·문자열처럼 크기가 실행 중에 정해지거나 계속 변하는 데이터, 그리고 함수가 끝난 뒤에도 살아 있어야 하는 데이터는 힙에 놓입니다. 힙은 커다란 창고이고, 필요할 때마다 빈자리를 찾아 내어 줍니다. 자리를 찾고 관리하는 비용이 있으므로 스택보다는 비쌉니다.
이것이 둘의 결정적 차이입니다. 함수가 끝나면 그 프레임은 통째로 사라지고, 안에 있던 지역변수도 함께 사라집니다. 하지만 힙에 있는 객체는 함수가 끝나도 남습니다 — 누군가 그 주소를 들고 있는 한 계속 살아 있습니다. 아래 애니메이션에서 login 함수가 끝나 프레임이 사라져도 User 객체는 힙에 남는 장면이 정확히 그것입니다.
함수에 배열을 넘길 때 배열 전체가 복사되는 게 아니라 주소표만 복사됩니다. 그래서 백만 개짜리 배열을 넘겨도 비용은 숫자 하나 복사하는 것과 같습니다. 대신 부작용이 하나 따라옵니다 — 함수 안에서 그 배열을 고치면 바깥에서도 바뀝니다. 같은 상자를 함께 가리키고 있으니까요.
스택은 호출이 깊어질수록 쌓입니다. 재귀 함수가 끝없이 자기를 부르면 스택이 정해진 한계를 넘어 스택 오버플로가 납니다. 반대로 힙은 아무도 안 쓰는 상자가 쌓여 문제가 됩니다. 이 두 번째 이야기가 바로 다음 단원의 주제입니다. 아래에서 스택이 세 칸까지 깊어졌다가 걷히는 동안, 힙 상자들은 계속 남아 있는 것을 확인해 보세요.
1main() {2 user = login("danbi")PC ◀3 cart = ["book"]4 total = checkout(user, cart)5 print(total)6}7login(name) {8 return User(name)9}10checkout(u, items) {11 sum = 012 for it in items: sum += price(it)13 return sum14}15price(it) { return 12000 }
main이 시작되며 스택에 프레임 하나가 놓입니다. 이 프레임 안에는 main의 지역변수(user, cart, total) 자리가 마련됩니다. 아직 값은 비어 있습니다.
시뮬레이터에서 해보기
이 단원의 용어
왜 이렇게 발전했나
왜 굳이 둘로 나눴을까요? 스택만 있으면 함수가 끝난 뒤에도 살아야 할 데이터를 둘 곳이 없고, 힙만 있으면 지역변수 하나 만들 때마다 창고에서 빈자리를 찾아야 해서 너무 느립니다. 그래서 수명이 짧고 크기가 정해진 것은 스택에서 초고속으로, 크고 오래 사는 것은 힙에서 유연하게 — 이 역할 분담이 거의 모든 언어의 공통 설계가 되었습니다.
스택은 함수와 함께 사라지고, 힙은 누가 가리키는 동안 살아남는다.
남은 문제
그럼 힙에 쌓인 상자는 누가, 언제 치울까요? →