최근엔 개발보단 서비스 방향에 집중하다 보니, 제 대학 친구 ‘파이썬’ 이 어떻게 돌아가는지 잊고 있는것 같습니다...
오늘은 그 기본을 다시 정리하며, 제 스스로도 리마인드하려 합니다.
이 글이 분명 도움이 되길... 바라며 시작하겠습니다.
파이썬은 스크립트 언어? 인터프리터 언어?
인터프리터 언어 ( Interpreted Lang )
- 소스 코드를 한줄씩 해석하고 바로 실행하는 언어로 별도의 기계어 컴파일 과정 없이 실행 시점에 해석되는 언어
스크립트 언어 ( Scripting Lang )
- 기본적으로 스크립트 작성 기능을 지원하는 소프트웨어를 제어하는 역할의 언어
보통 스크립트 언어를 인터프리터 언어라고 하며, 파이썬은 인터프리터 언어입니다.
파이썬 실행기?
파이썬이 인터프리터 언어라면 컴파일 해주는 친구가 존재합니다.
파이썬은 보통 C언어로 구현된 CPython 인터프리터를 짝궁으로 두어 .pyc 로 바이트코드로 컴파일 해버립니다.
.py (소스 코드)
↓ (1) 파서(Parser) 단계: 토큰 분석 및 문법 검증
↓ (2) 바이트코드(바이트코드: .pyc)로 컴파일
↓ (3) Python Virtual Machine(PVM)에서 실행
#순서
소스 코드 ─▶ CPython Compiler ─▶ Bytecode ─▶ Python Virtual Machine
메모리 할당

파이썬을 실행할 경우 메모리에서는 어떤 일이 발생할까요?
파이썬을 실행할 경우 CPython 인터프리터를 통해 바이트 코드로 컴파일 됩니다.
Code 영역
이때 Code 영역에는 .py파일이 바이트코드로 컴파일되어 담겨있는 코드 객체가 저장됩니다.
주로 개발하다 마주하는 __pycache__, .pyc와 같은 것을 런타임 중에 PVM(파이썬 가상머신)이 해석합니다.
함수/클래스/모듈마다 고유한 코드 객체가 생기며 함수 객체는 내부에 자신이 실행할 코드 객체(속성 __code__) 를 참조합니다.
간단한 예시로 아래 코드를 볼때 add라는 함수 객체가 있고, 그 내부에 코드 객체가 있습니다. PVM은 이 코드 객체의 바이트코드를 실행합니다.
def add(a, b):
return a + b
print(add.__code__.co_argcount) # 2 (인자 수)
print(add.__code__.co_varnames) # ('a', 'b')
Data 영역
CPython이 C로 구현된 런타임이므로, 인터프리터 자체의 전역/정적 데이터, 테이블, 내부 캐시 등이 프로세스의 .data/.bss 구간에 들어갑니다. 객체를 선언하면 Data에 들어가는게 아닌가? 하고 헷갈릴 수 있지만 CPython 기준에서의 데이터라고 생각하시면 편할 것 같습니다.
Heap 영역
힙 영역은 모든 파이썬 객체가 저장되는곳입니다. 정수, 문자열, 리스트, 딕셔너리, 함수, 클래스, 모듈 등등등 객체가 전부 힙에 저장됩니다.
메모리 관리 방식으로 참조 카운팅(reference counting)과 순환참조를 수거하기 위한 가비지 컬렉터가 존재하는데 이 부분에 대해서는 추후 알아보도록 하겠습니다.
아래 예시는 종종 프로그래밍 입문자가 헷갈릴만한 내용이빈다. 리스트를 선언하고 또다른 변수에 할당을 하였을때,
해당 변수를 변형시키면 원본 데이터에도 영향이 가는 경우가 발생합니다. 여기서 헷갈리는 것이 변수는 데이터 그자체가 아닌 라벨의 개념이기에 변수로 받게되면 같은 데이터 주소를 사용하게 됩니다.
이에 더불어 얕은/깊은 복사, 가변/불변 객체 개념이 있으니 해당 내용은 추후 설명하도록 하겠습니다.
## 이름은 라벨, 객체는 힙
a = [1, 2]
b = a # 같은 리스트 객체(힙의 한 곳)를 함께 가리킴
b.append(3)
print(a) # [1, 2, 3] ← a와 b가 같은 객체를 바라봄
## 불변객체
x = 10
y = x
y += 1 # 새 int 객체가 만들어지고 y가 그걸 가리킴
print(x, y) # 10 11
Stack 영역
함수를 호출할때마다 생성되는 스택 프레임의 영역입니다.
CPython 내부적으로는 C 스택과 파이썬 프레임(힙 객체)이 쓰이지만 호출할 때마다 프레임이 쌓이고 반환시 사라지는 영역 이라고 이해하면 될 것 같습니다.
def f(x):
y = x + 1 # f의 지역 변수 y (프레임에 저장)
return g(y)
def g(z):
return z * 2 # g의 지역 변수 z
print(f(10)) # f프레임 → g프레임 → g반환 → f반환
메모리 영역 별 요약
| 영역 | 설명 | 예시 |
| Code | 실행할 코드(함수, 클래스 등)가 바이트코드 형태로 저장됨 | def add(): ... |
| Stack | 함수 호출 시 지역 변수 저장 | x = 10 (함수 안) |
| Heap | 모든 객체가 동적 할당되는 공간 | 리스트, 딕셔너리 등 |
| Data | CPython 내부 전역/정적 데이터 | 상수, 내부 캐시 등 |
네임스페이스(namespace) 예시
컴퓨터 언어에서 네임스페이스가 존재하는데 네임스페이스는 전역 변수를 위한 네임스페이스와 각 지역변수를 위한 네임스페이스가 따로 존재하며, 파이썬 인터프리터가 어떤 코드를 실행하고 있냐에 따라서 네임스페이스의 값이 변하게 됩니다. 파이썬은 객체지향언어로 모든 데이터가 객체가 되고, 변수는 주소를 포인터 개념으로 저장하고 사용하기 때문에 네임스페이스를 통해 객체와 객체의 변수 이름을 매핑 시켜줍니다. 파이썬에서 dictionary 자료형의 네임스페이스를 사용하고 있으며 힙에 위치하게 됩니다.
딱딱한 설명보다는 간단한 예시를 통해 test.py 내 내용을 아래와 같이 정의하겠습니다.
global_var = 100
def test():
local_var = 0
print("\n--- 2. Global Keys ---")
print(globals().keys())
print("--- 3. Global Values ---")
print(globals().values())
print("\n--- 4. Local Keys ---")
print(locals().keys())
print("--- 5. Local Values ---")
print(locals().values())
print("--- 1. Local Keys ---")
print(locals().keys())
print("--- Local Values ---")
print(locals().values())
test()
위 코드 내용으로 py파일을 실행시켜 보면 아래와 같은 내용이 보입니다.
--- 1. Local Keys ---
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'global_var', 'test'])
--- Local Values ---
dict_values(['__main__', None, None, <_frozen_importlib_external.SourceFileLoader object at 0x736fa51c0f40>, None, {}, <module 'builtins' (built-in)>, '/home/1/test/namespace_test.py', None, 100, <function test at 0x736fa5287b50>])
--- 2. Global Keys ---
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'global_var', 'test'])
--- 3. Global Values ---
dict_values(['__main__', None, None, <_frozen_importlib_external.SourceFileLoader object at 0x736fa51c0f40>, None, {}, <module 'builtins' (built-in)>, '/home/1/test/namespace_test.py', None, 100, <function test at 0x736fa5287b50>])
--- 4. Local Keys ---
dict_keys(['local_var'])
--- 5. Local Values ---
dict_values([0])
test 함수를 실행하기 전 해당 py파일 내 local 네임스페이스는 아래와 같습니다.
1. 파일 내 local
--- 1. Local Keys ---
dict_keys(['__name__', ... , '__file__', '__cached__', 'global_var', 'test'])
--- Local Values ---
dict_values(['__main__', ..., None, 100, <function test at 0x736fa5287b50>])
내부적으로 정의해둔 local_var는 당연하게도 잡히지 않고, __name__이나 __main__과 같은 평상시 주로 사용하는 내용이 보입니다. 추가로 global_var이 여기서 local 키로 잡혀있습니다.
2. 함수 내 global
--- 2. Global Keys ---
dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__annotations__', '__builtins__', '__file__', '__cached__', 'global_var', 'test'])
--- 3. Global Values ---
dict_values(['__main__', None, None, <_frozen_importlib_external.SourceFileLoader object at 0x736fa51c0f40>, None, {}, <module 'builtins' (built-in)>, '/home/wooks/test/namespace_test.py', None, 100, <function test at 0x736fa5287b50>])
함수 내에서 선언한 글로벌은 위에 정의한 내용과 동일하게 보입니다. 이 경우에도 local_var에 대해서는 할당이 되어있지 않습니다.
3. 함수 내 local
--- 4. Local Keys ---
dict_keys(['local_var'])
--- 5. Local Values ---
dict_values([0])
함수 내 local은 local_var만 존재합니다. 이러한 메모리 할당은 C언어에서 stack영역의 stack call 안에서 정의되기 때문입니다.
해당 함수 실행이 시작되면 stack call이 정의되고, 실행이 종료되면 stack call이 stack에서 사라지기 때문에 해제가 됩니다.
📌결론
요즘 세상은 인공지능이 코드를 대신 써주고,
복잡한 문제도 몇 초 만에 결과를 만들어내는 시대가 되었습니다.
하지만 ‘결과를 빠르게 내는 능력’ 과
‘원리를 이해하고 통제하는 능력’ 은 전혀 다른 이야기입니다.
파이썬의 작동 원리를 들여다보면,
변수 하나가 단순한 값이 아니라 객체를 가리키는 이름이며,
실행의 흐름이 스택과 힙, 네임스페이스 위에서 정교하게 흘러간다는 사실을 깨닫게 됩니다.
이런 구조를 이해하는 순간, “코드가 왜 이렇게 동작하는가” 를 스스로 설명할 수 있는 힘이
생길거라고 생각합니다.
AI가 코드를 대신 써주는 시대일수록,
우리는 “어떻게 만들어졌는가”를 이해하는 사람이어야 합니다.
기술은 빠르게 변하지만,
그 밑단의 원리와 기본은 변하지 않습니다.
파이썬의 실행 원리를 다시 짚어보는 이 시간이,
단순한 복습이 아니라 개발자로서의 철학을 다시 세우는 과정이 되었으면 합니다.
결국, 속도가 아니라 깊이가 우리를 구분 짓는 시대니까요.
'개발' 카테고리의 다른 글
| [서버] Rootless Docker란? (0) | 2025.11.13 |
|---|---|
| [서버] 서버 구축기 (0) | 2025.08.31 |
| 가비아 이용해서 도메인등록하기 (15) | 2025.01.16 |
| [배포] 포트포워딩 LG+ 공유기 (9) | 2025.01.15 |
| [배포] 포트포워딩 + Nginx + Gunicorn (11) | 2025.01.10 |