(이하의 내용은 부정확한 부분이 많을 수 있습니다. 틀린 부분은 지적해주시면 감사하겠습니다.)
Python의 사양(specifiaction)에서는 메모리 관리에 대해 추상적인 원칙만을 제공한다. 그러한 원칙 중 하나가 바로 private heap이다. private heap은 모든 Python 객체와 자료 구조를 포함하는데, 여기서 private heap은 아직 실재하는 영역이 아니다. 각 구현체가 실제적으로 구현하는 과정에서 구체화된다.
왜 "private"인가? Python은 개발자가 메모리를 명시적으로 관리하게 두기보다 그것을 은닉하려는 철학을 갖고 있다. 그래서 개발자가 직접적으로 접근할 수 없는 private heap과 그를 유일하게 관리하는 Python memory manager를 도입한다. Python memory manager는 코드의 요구에 따라 메모리를 할당하고, 할당 받은 객체가 얼마나 참조되고 있는지를 파악하며, 순환 참조 등의 문제를 주기적으로 탐색하여 처리하는 등의 역할을 수행한다.
그렇다면 Python의 공식적인 참조 구현체(reference implementation)인 CPython은 이를 어떻게 구체화했을까? CPython은 C이므로 C의 방식으로 메모리를 관리한다. 잘 알려진 C의 메모리 구조는 다음과 같다.
코드 영역(code segment)
실행할 프로그램의 코드가 할당되는 곳이며 한 번 할당된 이후에는 read-only다. CPython의 경우에는 Python의 바이트코드와 해당 바이트코드를 해석할 Python Virtual Machine이 코드 영역에 위치하게 된다. (CPython interpreter는 소스 코드를 바이트 코드로 컴파일해는 바이트 코드 컴파일러와 바이트 코드를 머신 코드로 바꿔주는 Python Virtual Machine, 내장 라이브러리로 구성되어 있다. 컴파일 과정은 코드가 실행되기 전에 전처리된다.) 이외에 print()나 len() 같은 함수나 Python이 실행될 때 필요한 소수의 int나 str 객체 따위도 해당 영역에 위치한다.
데이터 영역(data segment)
C언어의 맥락에서 데이터 영역에는 전역 변수(global variables)나 정적 변수(static variables)가 저장된다. CPython에서는 globals() 함수로 확인할 수 있는 global namespace dict.와 그 key(즉 객체를 참조하고 있는 변수)들이 저장되는 것 같다.
힙 영역(heap)
C언어의 맥락에서 컴파일된 코드가 실행되면서 가변적으로 메모리를 할당 받아야 하는 대상들이 상주하게 되는 곳. (힙 자료구조와는 무관하다고 한다) CPython에서는 변수가 참조하는 객체들이 이곳에 생성된다.
스택 영역(stack)
함수가 자기 자신이나 다른 함수를 호출하는 등 어떠한 인과적인 의존 관계가 형성되어 있을 때, 이를 스택 자료구조로 생각할 수 있다. 인과적인 "압력"이 중간에 호출된 함수를 없애지 못하도록 만드므로 가장 마지막에 호출된 함수부터 차근차근 없애야만 하기 때문이다. 하나의 함수(혹은 scope)에 대한 정보를 하나의 stack frame에 저장하게 된다. CPython에서는 각 stack frame에 해당 scope의 local namespace dict.과 이전 stack frame의 주소 등이 있다.
간략하게 말하면 다음과 같다. 객체의 주소를 담는 변수들은 데이터 영역 혹은 스택 영역에 생성된다. 그 변수들이 참조하는 대상은 힙 영역에 생성된다.
def f1(lvl):
a = lvl
f1(1024)
위와 같은 코드가 있을 때, 함수 f1에 해당하는 stack frame은 다음과 같이 도식화할 수 있다.
만약 재귀 호출을 하게 되면 어떻게 될까?
def f1(lvl):
a = lvl
f1(lvl+1)
f1(1024)
재귀호출이 한 번 이루어진 순간에는 다음과 같을 것이다.
재귀의 종료 조건을 설정하지 않았으므로 재귀호출은 무한히 반복될 것이다.(Python의 경우 언어에서 재귀의 상한을 1000번으로 설정하고 있는데, 이는 print(sys.getrecursionlimit())의 출력값을 확인해보면 알 수 있다. sys.setrecursionlimit()을 통해 제한을 해제할 수 있다. 여기서는 그러한 제한이 없다고 가정하겠다) 따라서 그림은 다음과 같아진다.
메모리는 무한하지 않다. 스택 영역이 수용할 수 있는 범위에는 한계가 있고, 함수의 콜스택(call stack)이 쌓여서 스택의 수용 한계를 넘어서면 소위 말하는 스택 오버플로(stack overflow)라는 에러가 발생하게 된다. 이러한 구조는 (CPython이 스택 영역을 이용한다면) CPython에서도 동일할 것이다.
∞/ 시리즈를 마치며
뭔가 급하게 마무리를 하는 것 같은데, 사실이다. 내 능력 바깥의 주제라는 사실을 요 몇 주간 절절히 느꼈다.
private heap은 정말로 "private"했다. 내가 검색 능력이 모자랐을 수도 있지만, CPython의 private heap 구현에 대해 정리해둔 포스팅이나 칼럼 따위를 찾지 못했다. 결국 AI의 발전에 힘입어 LLM에게 물어볼 수밖에 없었는데, 답변이 맥락마다 뒤바뀌곤 했다. CPython 코드에서 특정 파트만 들여다 볼 수 있을까 싶었지만 어림도 없지. 깜냥이 되는 일을 해야 한다는 교훈만 새삼 느꼈다.
Python이라는 언어의 사양과 구현, CPython은 C이므로 결국 C의 메모리 구조를 따라야만 한다는 생각……. 되돌아보면 간단하다. 이만한 포스팅에 그만한 시간이 걸리는 건 크나큰 낭비였다. 물론 이건 결과론이고, 그 과정에서 OOP가 뭔지, C의 메모리 구조가 어떤지를 탐색하게 된 건 소득이다. 하지만 이해하기에 벅찰 정도로 거대한 대상을 만났을 때 cutoff를 잡지 못하고 주변을 맴돌다가 시간을 허비하는 것은 이미 겪어봤던 일이기도 하다.
이해에는 선결조건이 있다. 어떤 개념을 누군가는 보다 쉽게 이해한다. 적절한 과제를 수행하는 것이 효율적이다. 이러한 과정에서 얻는 게 없다고는 하지 않겠지만, 매번 이럴 수는 없는 노릇 아닌가. 앞으로는 토픽의 난이도와 전체상을 가늠하고 시리즈를 시작해야겠다.
'Study > Computer Science' 카테고리의 다른 글
인터페이스 (0) | 2024.04.25 |
---|---|
프로그래밍 패러다임 (0) | 2024.04.15 |
자료구조 (0) | 2024.04.05 |
자료형, 메모리, 그리고 Python [1] (0) | 2024.03.24 |
자료형, 메모리, 그리고 Python [0] (0) | 2024.03.22 |