누구나 로고
オンライン講座企業・団体研修ライブラリトレンドインサイト無料コースお知らせお問い合わせ採用サイト

カスタマーサポート

  • お知らせ
  • よくある質問
  • footer.contact
  • コミュニティ

ご利用案内

  • 利用規約
  • プライバシーポリシー
  • 返金ポリシー

サービス

  • 会社紹介
  • 新規登録
  • 新着コース
  • 無料コース
누구나 로고
利用規約プライバシーポリシー返金ポリシー

상호명: NUGUNA  |  대표자: 정우진  |  사업자등록번호: 392-32-01817  |  통신판매업신고: 제 2026-서울양천-0564 호

주소: 서울특별시 양천구 목동서로 100, 309동 1006호  |  이메일: nugunapass@gmail.com  |  전화: 010-6395-3043

© 2026 NUGUNA. All rights reserved.

KB예금주인증
ホームコースライブラリお問い合わせMY
관리자
모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.
로그인무료 회원가입
강좌파이썬 기초 40강 — 처음 배우는 프로그래밍3강

3강 / 전체 6강

파이썬의 실행 모델 — 바이트코드, PVM, GIL

11분 읽기 조회 61

파이썬 코드가 내부적으로 어떻게 실행되는지 CPython 구조를 깊이 이해한다.

🎯 이 강의에서 배우는 것

1강에서 파이썬 코드가 바이트코드를 거쳐 PVM에서 실행된다는 큰 그림을 봤습니다. 이 강의에서는 그 과정을 실제 코드로 직접 들여다봅니다. 바이트코드가 어떻게 생겼는지, 함수 호출 시 내부에서 무슨 일이 일어나는지, 그리고 파이썬의 멀티스레딩이 왜 기대만큼 빠르지 않은지 — GIL의 정체까지 완전히 이해합니다.

📦 .py → .pyc: 바이트코드 컴파일

파이썬 소스 파일(.py)을 처음 실행하면 CPython은 먼저 코드를 바이트코드(bytecode)로 컴파일합니다. 이 바이트코드는 __pycache__ 폴더에 .pyc 확장자로 저장됩니다.

# hello.py
def greet(name):
    return f"안녕하세요, {name}!"

print(greet("파이썬"))

이 파일을 실행하면 __pycache__/hello.cpython-312.pyc 같은 파일이 생깁니다. 파일명에 파이썬 버전이 포함된 이유는 서로 다른 CPython 버전의 바이트코드가 호환되지 않을 수 있기 때문입니다.

바이트코드를 재사용하는 조건은 두 가지입니다: 소스 파일의 수정 시각이 변하지 않았고, 파이썬 버전이 동일해야 합니다. 이 조건이 맞으면 소스 파싱 단계를 건너뛰어 시작 속도가 빨라집니다.

import py_compile
import os

# .py 파일을 .pyc로 명시적 컴파일
py_compile.compile("hello.py")

# __pycache__ 내용 확인
for f in os.listdir("__pycache__"):
    print(f)
# 출력 예: hello.cpython-312.pyc

바이트코드는 특정 CPU 아키텍처에 종속되지 않는 플랫폼 독립적 중간 표현입니다. 같은 .pyc 파일을 Windows에서 만들어 macOS에서 실행할 수 있습니다(단, 파이썬 버전이 같아야 함). 이것이 Java의 "Write Once, Run Anywhere"와 동일한 원리입니다.

🔬 dis 모듈: 바이트코드를 눈으로 보기

표준 라이브러리의 dis 모듈로 바이트코드를 직접 확인할 수 있습니다. 이것이 파이썬 코드가 실제로 어떻게 실행되는지를 이해하는 가장 직접적인 방법입니다.

import dis

def add(a, b):
    result = a + b
    return result

dis.dis(add)

출력(Python 3.12 기준):

  4           RESUME          0

  5           LOAD_FAST       0 (a)       # 로컬 변수 a를 평가 스택에 올림
              LOAD_FAST       1 (b)       # 로컬 변수 b를 평가 스택에 올림
              BINARY_OP       0 (+)       # 스택 상위 2개를 꺼내 더하고 결과를 올림
              STORE_FAST      2 (result)  # 스택 최상위 값을 로컬 변수 result에 저장

  6           LOAD_FAST       2 (result)  # result를 스택에 올림
              RETURN_VALUE                # 스택 최상위 값을 반환

파이썬 PVM은 스택 기반 가상 머신입니다. 계산에 사용되는 임시 값들을 평가 스택(evaluation stack)에 쌓았다가 꺼냅니다. 주요 opcode를 이해해봅시다:

opcode의미
LOAD_FAST로컬 변수를 스택에 올림
LOAD_CONST상수 값(리터럴)을 스택에 올림
LOAD_GLOBAL전역 변수 또는 내장 함수를 스택에 올림
STORE_FAST스택 최상위 값을 로컬 변수에 저장
BINARY_OP스택 상위 두 값으로 산술/비교 연산
CALL함수 호출
RETURN_VALUE함수 반환값을 호출자에게 전달
POP_TOP스택 최상위 값 제거 (사용하지 않는 결과 버림)
# 더 복잡한 예: 조건문이 있는 함수
import dis

def abs_val(x):
    if x < 0:
        return -x
    return x

dis.dis(abs_val)
# 조건 분기가 JUMP_FORWARD 또는 POP_JUMP_IF_FALSE 같은 opcode로 표현됨을 확인할 수 있음

🏗️ 동적 타이핑: 타입은 값에 붙는다

파이썬은 동적 타입(dynamic typing) 언어입니다. 이 말이 내부적으로 어떤 의미인지 C 언어와 비교해 이해해봅시다.

# C 언어에서 (파이썬 코드가 아닌 개념 설명):
# int x = 5;
# → 스택 메모리에 4바이트 공간 확보
# → 그 공간에 5를 저장
# → x라는 이름은 그 메모리 위치(주소)를 의미
# → x는 int 타입을 영구적으로 고정

# 파이썬에서:
x = 5
# → 힙 메모리에 정수 객체 5 생성 (타입 정보 포함)
# → x라는 이름표(참조)가 그 객체를 가리킴

print(type(x))   # <class 'int'>
print(id(x))     # 메모리 주소 (예: 140234567890)

x = "hello"      # 같은 이름 x가 이제 문자열 객체를 가리킴
print(type(x))   # <class 'str'>
print(id(x))     # 다른 메모리 주소

# 타입 정보는 '이름(변수)'에 없고 '값(객체)'에 있다
y = x            # y도 같은 "hello" 객체를 가리킴
print(y is x)    # True - 같은 객체

이 차이가 중요한 이유: 파이썬에서 type(x)는 런타임에만 알 수 있습니다. 컴파일 시점에는 x가 어떤 타입인지 알 수 없습니다. 이것이 파이썬이 실행 속도 면에서 C보다 느린 근본 원인 중 하나입니다 — 모든 연산에서 타입을 확인하는 overhead가 발생합니다.

📚 프레임 객체와 콜 스택

파이썬에서 함수가 호출될 때마다 스택 프레임(stack frame)이 생성됩니다. 프레임은 함수 실행에 필요한 모든 정보를 담고 있습니다:

  • 로컬 변수들
  • 현재 실행 중인 바이트코드의 위치(인스트럭션 포인터)
  • 호출한 함수의 프레임(호출자 프레임, 반환 후 돌아갈 위치)
  • 전역 변수들의 참조
import inspect

def inner():
    frame = inspect.currentframe()
    print(f"현재 함수: {frame.f_code.co_name}")
    print(f"로컬 변수들: {frame.f_locals}")

    # 호출 스택 전체 출력
    for info in inspect.stack():
        print(f"  → {info.function} (줄 {info.lineno})")

def outer():
    x = 42
    inner()

outer()
# 출력:
# 현재 함수: inner
# 로컬 변수들: {'frame': <frame object>}
#   → inner (줄 3)
#   → outer (줄 10)
#   → <module> (줄 13)

함수가 중첩 호출될수록 스택 프레임이 쌓입니다. CPython의 기본 재귀 한도는 1000입니다(sys.getrecursionlimit()). 이 한도를 초과하면 RecursionError가 발생합니다. 22강 '재귀' 강의에서 이것이 실제로 어떤 의미인지 깊이 다룹니다.

🔒 GIL — 파이썬 멀티스레딩의 현실

GIL(Global Interpreter Lock, 전역 인터프리터 락)은 CPython에서 한 번에 하나의 스레드만 파이썬 바이트코드를 실행할 수 있도록 강제하는 뮤텍스(mutex)입니다.

왜 GIL이 존재하는가? CPython의 메모리 관리는 참조 카운팅(reference counting)을 기반으로 합니다. 각 객체는 자신을 참조하는 이름표의 수(참조 카운트)를 유지합니다. 참조 카운트가 0이 되면 메모리를 해제합니다. 이 카운터 증감 연산이 원자적(atomic)이지 않기 때문에, 여러 스레드가 동시에 같은 객체의 참조 카운트를 수정하면 카운트가 잘못되어 메모리 오류가 발생합니다. GIL은 이 문제를 예방합니다.

import threading
import time

# CPU 집약적 작업 (GIL 영향을 크게 받음)
def count_up(n):
    total = 0
    for i in range(n):
        total += i
    return total

N = 50_000_000

# 단일 스레드
start = time.perf_counter()
count_up(N)
single_time = time.perf_counter() - start

# 2개 스레드 (CPU-bound 작업 → GIL로 인해 빠르지 않음)
start = time.perf_counter()
t1 = threading.Thread(target=count_up, args=(N // 2,))
t2 = threading.Thread(target=count_up, args=(N // 2,))
t1.start(); t2.start()
t1.join(); t2.join()
thread_time = time.perf_counter() - start

print(f"단일 스레드: {single_time:.2f}초")
print(f"2스레드:     {thread_time:.2f}초")
# 결과: 2스레드가 오히려 더 느리거나 비슷함 (GIL 경합 오버헤드)

GIL의 영향은 작업 종류에 따라 크게 다릅니다:

작업 유형GIL 영향해결책
CPU-bound (계산 집약)크다 — 스레드를 써도 속도 향상 없음multiprocessing, ProcessPoolExecutor
I/O-bound (파일/네트워크 대기)작다 — I/O 대기 중 GIL 해제됨threading, asyncio 모두 효과적
import concurrent.futures

# CPU-bound 작업에는 multiprocessing (각 프로세스가 독립적 GIL 보유)
def cpu_task(n):
    return sum(range(n))

with concurrent.futures.ProcessPoolExecutor() as executor:
    results = list(executor.map(cpu_task, [10**7, 10**7, 10**7, 10**7]))
    print(results)  # 4개 프로세스가 병렬 실행 → 실제로 빠름

Python 3.13에서는 No-GIL 모드가 실험적으로 도입됐습니다(--disable-gil 빌드 옵션). 기존 C 확장과의 호환성 등 여러 과제가 남아 있지만, 언젠가 파이썬 멀티스레딩의 성능이 크게 향상될 것입니다.

⚠️ 자주 하는 오해

  • "파이썬은 인터프리터 언어라 컴파일이 없다": 틀렸습니다. 파이썬도 바이트코드로 컴파일합니다. 다만 이 과정이 자동으로 실행되고 중간 결과물이 실행 파일이 아닌 바이트코드라는 점이 다릅니다.
  • "GIL 때문에 파이썬은 병렬 처리를 못한다": I/O 작업에는 asyncio나 threading으로 효과적인 동시 처리가 가능합니다. CPU 병렬 처리는 multiprocessing을 사용하면 됩니다.
  • "__pycache__ 폴더를 삭제하면 성능이 좋아진다": 반대입니다. __pycache__는 재실행 시 빠른 시작을 위한 캐시입니다. 삭제하면 매번 다시 컴파일해야 합니다.

📝 정리 및 다음 강의 예고

이번 강의에서 배운 핵심 내용을 정리합니다:

  • 파이썬 소스 코드는 실행 전에 바이트코드(.pyc)로 컴파일되어 __pycache__에 저장됩니다.
  • dis 모듈로 함수의 바이트코드를 직접 확인할 수 있습니다.
  • 파이썬은 동적 타입 언어 — 타입 정보는 변수(이름)가 아닌 값(객체)에 저장됩니다.
  • 함수 호출마다 스택 프레임이 생성되고, 함수 반환 시 제거됩니다.
  • GIL은 참조 카운팅 기반 메모리 관리를 보호하는 락입니다. CPU-bound 작업의 멀티스레딩 성능에 영향을 미칩니다.

다음 강의: 드디어 첫 파이썬 프로그램을 작성합니다. print()와 input()의 모든 매개변수를 분석하고, 표준 입출력 스트림의 원리를 배웁니다.

관련 주제

  • .pyc 바이트코드 컴파일
  • dis 모듈 opcode 분석
  • 동적 타이핑
  • 스택 프레임
  • GIL(전역 인터프리터 락)
  • CPython 내부구조
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 파이썬 기초 40강 — 처음 배우는 프로그래밍
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

목차

이전 2강파이썬 생태계와 개발 환경 완전 설정
다음 4강 첫 프로그램 — print()와 input()으로 배우는 I/O 원리
1.컴퓨터는 어떻게 생각하는가 — 0과 1에서 프로그램까지
2.파이썬 생태계와 개발 환경 완전 설정
3.파이썬의 실행 모델 — 바이트코드, PVM, GIL
4.첫 프로그램 — print()와 input()으로 배우는 I/O 원리
5.변수란 무엇인가 — 이름표, 객체, 참조의 삼각관계
6.정수(int) 완전 해부 — 임의 정밀도, 비트 연산, 정수 연산의 함정