PHpullh

PYTHON · 심층 가이드

Python 성능 완전 정리

CPython의 바이트코드 디스패치 비용과 GIL을 전제에 두고, 측정에서 자료구조 교체, 벡터화, 프로세스 분리까지 이어지는 15개 주제를 다룹니다.

주제 15개 · 예제 코드 포함 · 최종 수정 2026-08-30 · 작성 pullh 편집팀

Python에서 성능 작업은 알고리즘을 다듬는 일보다 연산을 인터프리터 밖으로 내보내는 일에 가깝습니다. 정수 하나도 힙에 할당된 객체이고, 인스턴스는 기본적으로 __dict__를 들고 다니며, 속성 접근 한 번마다 딕셔너리 조회가 일어납니다. C로 짜면 공짜인 루프가 여기서는 반복마다 참조 카운트를 건드립니다. 그래서 최적화의 방향은 대체로 하나입니다. 루프를 C 계층으로 밀어 넣거나, 객체 수 자체를 줄이는 것입니다.

먼저 프로파일링 (cProfile)메모리 프로파일링으로 실제 병목의 위치를 확정한 뒤에 손을 대는 순서를 권합니다. 병목이 데이터 양이라면 리스트 vs 제너레이터itertools — 고성능 이터레이터가 중간 리스트를 없애는 방법을 보여주고, 객체 수가 문제라면 __slots__와 메모리 최적화가 다음 단계입니다. 수치 연산이 남으면 numpy 벡터화, 그래도 부족하면 Cython 소개multiprocessing 순으로 내려갑니다.

함정 하나. multiprocessing은 macOS와 Windows에서 기본 시작 방식이 spawn이라 자식 프로세스가 모듈을 처음부터 다시 임포트합니다. 모듈 최상위에 무거운 초기화 코드를 두면 워커 수만큼 그 비용이 곱해지고, if __name__ == "__main__" 가드를 빼먹으면 프로세스가 무한히 번식합니다. 게다가 인자와 반환값은 모두 피클링을 거치므로, 큰 배열을 주고받는 구조라면 계산으로 아낀 시간을 직렬화가 그대로 되가져갑니다.

01typing 모듈 & Protocol 고급

타입 힌트로 코드 품질과 IDE 지원을 극대화합니다.

Python code

from typing import (
    Optional, Union, List, Dict, Tuple,
    TypeVar, Generic, Callable, TypeAlias
)
from collections.abc import Iterator, Generator

# Optional (= X | None)
def find_user(id: int) -> Optional[str]:
    return "Alice" if id == 1 else None

# Union (Python 3.10+: X | Y)
def process(value: int | str | None) -> str:
    match value:
        case int():  return f"정수: {value}"
        case str():  return f"문자열: {value}"
        case None:   return "없음"

# TypeVar — 제네릭
T = TypeVar("T")

def first(items: List[T]) -> Optional[T]:
    return items[0] if items else None

# Generic 클래스
class Stack(Generic[T]):
    def __init__(self): self._data: List[T] = []
    def push(self, item: T) -> None: self._data.append(item)
    def pop(self) -> T: return self._data.pop()

# TypeAlias (Python 3.10+)
UserID: TypeAlias = int
Matrix: TypeAlias = List[List[float]]

# Callable
Transform = Callable[[int], int]

def apply(fn: Transform, value: int) -> int:
    return fn(value)

# Generator 타입 힌트
def count_up(n: int) -> Generator[int, None, None]:
    for i in range(n): yield i

s: Stack[str] = Stack()
s.push("hello")
print(s.pop())   # hello
알아두면 좋은 점

Python 3.10+에서는 Optional[X] 대신 X | None, Union[X, Y] 대신 X | Y를 쓸 수 있습니다. 훨씬 간결합니다.

자주 하는 실수

타입 힌트는 런타임에 강제되지 않습니다. mypy, pyright, ruff 같은 정적 분석 도구와 함께 사용해야 효과가 있습니다.

02Context Manager & __slots__

contextlib로 리소스를 안전하게 관리하고, __slots__로 메모리를 최적화합니다.

Python code

from contextlib import contextmanager, suppress
import time

# @contextmanager — 제너레이터로 컨텍스트 매니저 구현
@contextmanager
def timer(label: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f"{label}: {elapsed:.4f}초")

with timer("작업"):
    total = sum(range(1_000_000))

# suppress — 특정 예외 무시
with suppress(FileNotFoundError):
    open("nonexistent.txt")
print("파일 없어도 계속 실행")

# __slots__ — 메모리 최적화
# 기본 클래스: __dict__ 사용 (동적 속성 추가 가능)
class NormalPoint:
    def __init__(self, x, y):
        self.x = x
        self.y = y

# __slots__ 클래스: 고정 속성, 메모리 ~50% 절약
class SlottedPoint:
    __slots__ = ("x", "y")
    def __init__(self, x, y):
        self.x = x
        self.y = y

import sys
np = NormalPoint(1.0, 2.0)
sp = SlottedPoint(1.0, 2.0)
print(f"일반: {sys.getsizeof(np.__dict__)}B")
print(f"slots: (no __dict__)")

# 수백만 개 객체 생성 시 큰 차이
normal  = [NormalPoint(i, i) for i in range(100_000)]
slotted = [SlottedPoint(i, i) for i in range(100_000)]
알아두면 좋은 점

__slots__를 사용하면 동적 속성 추가가 불가능해집니다. 데이터 컨테이너처럼 속성이 고정된 클래스에 적합합니다.

자주 하는 실수

__slots__를 상속받은 클래스에서 __slots__를 선언하지 않으면 자동으로 __dict__가 추가되어 최적화 효과가 사라집니다.

03itertools — 고성능 이터레이터

표준 라이브러리 itertools로 메모리 효율적인 데이터 처리를 구현합니다.

Python code

import itertools

# count, cycle, repeat — 무한 이터레이터
counter = itertools.count(start=1, step=2)  # 1, 3, 5, 7...
first5 = list(itertools.islice(counter, 5)) # [1, 3, 5, 7, 9]

# chain — 여러 이터러블 연결
combined = list(itertools.chain([1,2], [3,4], [5,6]))
# [1, 2, 3, 4, 5, 6]

# batched (Python 3.12+) — N개씩 묶기
data = range(10)
batches = list(itertools.batched(data, 3))
# [(0,1,2), (3,4,5), (6,7,8), (9,)]

# groupby — 연속된 같은 값 그룹화
data = [("A",1),("A",2),("B",3),("B",4),("A",5)]
for key, group in itertools.groupby(data, key=lambda x: x[0]):
    print(key, list(group))

# product — 카르테시안 곱
for r, g, b in itertools.product([0,255], repeat=3):
    pass  # 8개 색상 조합

# combinations / permutations
items = [1, 2, 3, 4]
combs = list(itertools.combinations(items, 2))
perms = list(itertools.permutations(items, 2))
print(f"조합: {len(combs)}개, 순열: {len(perms)}개")

# accumulate — 누적 연산
cumsum = list(itertools.accumulate([1,2,3,4,5]))
# [1, 3, 6, 10, 15]

import operator
cumprod = list(itertools.accumulate([1,2,3,4,5], operator.mul))
# [1, 2, 6, 24, 120]
알아두면 좋은 점

itertools의 함수들은 모두 이터레이터를 반환합니다. 결과를 리스트로 필요할 때만 list()로 변환하세요.

자주 하는 실수

itertools.groupby()는 연속된 같은 값만 그룹화합니다. 전체 그룹화가 필요하면 먼저 정렬하거나 defaultdict를 사용하세요.

04__slots__와 NamedTuple 비교

메모리 최적화 전략 비교: slots vs namedtuple vs dataclass

Python code

<span class="cm">// __slots__와 NamedTuple 비교 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("__slots__와 NamedTuple 비교") }
알아두면 좋은 점

PYTHON 공식 문서를 함께 참고하세요.

자주 하는 실수

자주 발생하는 실수에 주의하세요.

05__slots__와 메모리 최적화

__slots__를 선언하면 인스턴스마다 __dict__를 생성하지 않아 메모리를 크게 절약합니다. 수백만 개의 객체를 다루는 데이터 처리나 과학 계산에서 효과적입니다.

Python code

import sys

# 일반 클래스: __dict__ 사용
class PointDict:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

# __slots__ 클래스: 고정 속성만 허용
class PointSlots:
    __slots__ = ('x', 'y', 'z')

    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z

# 메모리 비교
pd = PointDict(1.0, 2.0, 3.0)
ps = PointSlots(1.0, 2.0, 3.0)

size_dict = sys.getsizeof(pd) + sys.getsizeof(pd.__dict__)
size_slots = sys.getsizeof(ps)

print(f"__dict__ 사용: {size_dict} bytes")
print(f"__slots__ 사용: {size_slots} bytes")
print(f"절약률: {(1 - size_slots/size_dict)*100:.1f}%")

# 대량 객체 생성 시 효과
import time

N = 1_000_000
start = time.perf_counter()
dict_points = [PointDict(i, i+1, i+2) for i in range(N)]
t_dict = time.perf_counter() - start

start = time.perf_counter()
slots_points = [PointSlots(i, i+1, i+2) for i in range(N)]
t_slots = time.perf_counter() - start

print(f"\n{N:,}개 생성 시간:")
print(f"  __dict__: {t_dict:.3f}초")
print(f"  __slots__: {t_slots:.3f}초")

# __slots__ 제한: 동적 속성 추가 불가
try:
    ps.w = 4.0
except AttributeError as e:
    print(f"\n속성 추가 오류: {e}")
알아두면 좋은 점

__slots__와 상속을 함께 사용할 때, 부모와 자식 모두 __slots__를 선언해야 효과가 있습니다. 하나라도 빠지면 __dict__가 생성됩니다.

자주 하는 실수

__slots__를 사용하면 __dict__가 없으므로 vars(obj), **obj.__dict__ 등이 작동하지 않습니다. JSON 직렬화 시 별도 처리가 필요합니다.

06프로파일링 (cProfile)

cProfile로 함수별 실행 시간을 측정하여 성능 병목을 찾습니다.

Python code

import cProfile
import pstats
import io

def slow_function():
    total = 0
    for i in range(100000):
        total += i ** 2
    return total

def fast_function():
    return sum(i ** 2 for i in range(100000))

def main():
    slow_function()
    fast_function()
    slow_function()

# 프로파일링 실행
profiler = cProfile.Profile()
profiler.enable()
main()
profiler.disable()

# 결과 출력
stream = io.StringIO()
stats = pstats.Stats(profiler, stream=stream)
stats.sort_stats("cumulative")
stats.print_stats(10)
print(stream.getvalue()[:500])

# 데코레이터로 개별 함수 프로파일링
def profile(func):
    def wrapper(*args, **kwargs):
        pr = cProfile.Profile()
        pr.enable()
        result = func(*args, **kwargs)
        pr.disable()
        pr.print_stats(sort="cumulative")
        return result
    return wrapper

# 명령줄: python -m cProfile -s cumulative script.py
# 시각화: pip install snakeviz
#         python -m cProfile -o output.prof script.py
#         snakeviz output.prof
print("프로파일링 완료")
알아두면 좋은 점

snakeviz로 프로파일 결과를 브라우저에서 시각적으로 분석할 수 있습니다.

자주 하는 실수

프로파일링 자체가 오버헤드를 추가합니다. 운영 환경에서는 샘플링 프로파일러(py-spy)를 사용하세요.

07메모리 프로파일링

메모리 사용량을 측정하고 메모리 누수를 찾는 방법입니다.

Python code

import sys
import tracemalloc

# tracemalloc으로 메모리 추적
tracemalloc.start()

# 메모리를 사용하는 작업
data = [i ** 2 for i in range(10000)]
lookup = {str(i): i for i in range(10000)}

# 현재 메모리 스냅샷
snapshot = tracemalloc.take_snapshot()
top = snapshot.statistics("lineno")
print("=== 메모리 사용 상위 항목 ===")
for stat in top[:3]:
    print(f"  {stat}")

# 객체 크기 측정
print(f"\n=== 객체 크기 ===")
print(f"int(1):      {sys.getsizeof(1)} bytes")
print(f"str('hello'):{sys.getsizeof('hello')} bytes")
print(f"list(1000):  {sys.getsizeof(list(range(1000)))} bytes")
print(f"dict(1000):  {sys.getsizeof({i:i for i in range(1000)})} bytes")
print(f"tuple(1000): {sys.getsizeof(tuple(range(1000)))} bytes")

# __slots__으로 메모리 절약
class PointRegular:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class PointSlots:
    __slots__ = ("x", "y")
    def __init__(self, x, y):
        self.x = x
        self.y = y

reg = PointRegular(1, 2)
slotted = PointSlots(1, 2)
print(f"\n일반: {sys.getsizeof(reg)} bytes (+ __dict__: {sys.getsizeof(reg.__dict__)})")
print(f"slots: {sys.getsizeof(slotted)} bytes")

tracemalloc.stop()
알아두면 좋은 점

__slots__은 인스턴스가 많은 클래스에서 메모리를 40-50% 절약합니다.

자주 하는 실수

sys.getsizeof()는 객체의 직접 크기만 반환합니다. 중첩된 객체의 크기는 포함되지 않습니다.

08리스트 vs 제너레이터

리스트와 제너레이터의 메모리/속도 트레이드오프를 비교합니다.

Python code

import sys
import time

# 메모리 비교
list_data = [x ** 2 for x in range(1_000_000)]
gen_data = (x ** 2 for x in range(1_000_000))

print("=== 메모리 비교 ===")
print(f"리스트: {sys.getsizeof(list_data):>12,} bytes")
print(f"제너레이터: {sys.getsizeof(gen_data):>8,} bytes")

# 속도 비교: 전체 소비
def time_it(name, func):
    start = time.perf_counter()
    result = func()
    elapsed = time.perf_counter() - start
    print(f"  {name}: {elapsed:.4f}초 → {result}")

print("\n=== 합계 계산 속도 ===")
time_it("리스트 컴프", lambda: sum([x**2 for x in range(100000)]))
time_it("제너레이터", lambda: sum(x**2 for x in range(100000)))

# 언제 무엇을 사용?
print("\n=== 선택 기준 ===")
guidelines = {
    "리스트": ["인덱스 접근 필요", "여러 번 순회", "len() 필요", "슬라이싱"],
    "제너레이터": ["한 번만 순회", "대용량 데이터", "메모리 제한", "지연 평가"],
}
for type_name, cases in guidelines.items():
    print(f"  {type_name}:")
    for case in cases:
        print(f"    - {case}")

# 체이닝 성능
# 각 단계에서 중간 리스트 생성 없이 처리
result = sum(
    x for x in (i**2 for i in range(10000))
    if x % 3 == 0
)
print(f"\n체이닝 결과: {result}")
알아두면 좋은 점

한 번만 순회하는 경우 제너레이터가 메모리 효율적입니다. sum(), any(), all()에 적합합니다.

자주 하는 실수

제너레이터를 list()로 변환하면 메모리 이점이 사라집니다. 변환 없이 직접 소비하세요.

09numpy 벡터화

numpy의 벡터화 연산으로 루프 대비 100배 이상 빠른 수치 계산을 수행합니다.

Python code

# numpy 패턴 시뮬레이션 (실제는 pip install numpy)
import time
import array

# 순수 Python 루프
def python_sum_squares(n):
    return sum(x * x for x in range(n))

# array 모듈 (numpy 경량 대안)
def array_operations():
    a = array.array("d", range(1000))
    b = array.array("d", range(1000))
    # 원소별 연산은 여전히 루프 필요
    result = array.array("d", (x + y for x, y in zip(a, b)))
    return sum(result)

# 벤치마크
n = 100000
start = time.perf_counter()
r1 = python_sum_squares(n)
t1 = time.perf_counter() - start

print(f"Python 루프: {t1:.4f}초, 결과: {r1}")

# numpy 사용법 (설치 필요)
# import numpy as np
# arr = np.arange(n)
# start = time.perf_counter()
# r2 = np.sum(arr ** 2)  # 벡터화 연산
# t2 = time.perf_counter() - start
# print(f"NumPy: {t2:.4f}초, 속도향상: {t1/t2:.0f}x")

# numpy 벡터화 패턴
patterns = """
# 벡터화 패턴:
arr = np.array([1, 2, 3, 4, 5])
arr * 2           # [2, 4, 6, 8, 10]
arr ** 2          # [1, 4, 9, 16, 25]
np.sum(arr)       # 15
np.mean(arr)      # 3.0
np.where(arr > 3) # [4, 5]
arr[arr > 3]      # 불리언 인덱싱

# 행렬 연산
A = np.array([[1,2],[3,4]])
B = np.array([[5,6],[7,8]])
C = A @ B         # 행렬 곱
"""
print(patterns)
알아두면 좋은 점

NumPy 배열의 반복문은 순수 Python만큼 느립니다. 항상 벡터화 연산(+, *, np.sum)을 사용하세요.

자주 하는 실수

np.append()를 루프 안에서 사용하면 매번 배열을 복사하여 O(n²)가 됩니다. 리스트로 모은 후 한 번에 변환하세요.

10Cython 소개

Cython으로 Python 코드를 C로 컴파일하여 성능을 극적으로 향상시키는 방법의 개요입니다.

Python code

# Cython 코드 예제 (.pyx 파일)
cython_code = """
# fib.pyx - Cython 코드
def fib_python(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

# C 타입 선언으로 최적화
def fib_cython(int n):
    cdef long long a = 0, b = 1
    cdef int i
    for i in range(n):
        a, b = b, a + b
    return a

# 타입 선언이 핵심!
# int, long, double → C 네이티브 타입
# cdef → C 전용 함수 (Python에서 호출 불가)
# cpdef → C + Python 양쪽에서 호출 가능
"""
print(cython_code)

# 빌드 방법
setup_code = """
# setup.py
from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("fib.pyx"),
)

# 빌드: python setup.py build_ext --inplace
# 사용: from fib import fib_cython
"""
print(setup_code)

# 순수 Python 대안
import time

def fib_pure(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

start = time.perf_counter()
result = fib_pure(100000)
elapsed = time.perf_counter() - start
print(f"Python fib(100000): {elapsed:.4f}초")
print(f"결과 자릿수: {len(str(result))}")
print("Cython은 보통 10-100x 빠릅니다")
알아두면 좋은 점

Cython에서 가장 큰 성능 향상은 루프 내 변수에 cdef 타입 선언을 추가하는 것입니다.

자주 하는 실수

Cython은 빌드 단계가 필요하여 배포가 복잡해집니다. 성능이 정말 필요한 핫스팟에만 적용하세요.

11multiprocessing

multiprocessing으로 CPU 바운드 작업을 여러 프로세스에 분산하여 GIL을 우회합니다.

Python code

from multiprocessing import Pool, cpu_count
import time

def cpu_intensive(n: int) -> int:
    """CPU 바운드 작업"""
    total = 0
    for i in range(n):
        total += i * i
    return total

# 순차 실행
def sequential(tasks):
    return [cpu_intensive(n) for n in tasks]

# 병렬 실행
def parallel(tasks, workers=None):
    with Pool(workers or cpu_count()) as pool:
        return pool.map(cpu_intensive, tasks)

tasks = [500000] * 8

# 벤치마크
start = time.perf_counter()
r1 = sequential(tasks)
t1 = time.perf_counter() - start
print(f"순차: {t1:.3f}초")

start = time.perf_counter()
r2 = parallel(tasks)
t2 = time.perf_counter() - start
print(f"병렬 ({cpu_count()}코어): {t2:.3f}초")
print(f"속도 향상: {t1/t2:.1f}x")

# imap: 지연 평가 (대용량 데이터)
# with Pool() as pool:
#     for result in pool.imap_unordered(cpu_intensive, tasks):
#         print(f"완료: {result}")

# starmap: 다중 인자
def add(a, b):
    return a + b

with Pool() as pool:
    results = pool.starmap(add, [(1,2), (3,4), (5,6)])
    print(f"starmap 결과: {results}")
알아두면 좋은 점

Pool.imap_unordered()는 결과를 완료 순서대로 반환하여 먼저 끝난 작업부터 처리할 수 있습니다.

자주 하는 실수

프로세스 간 데이터 전달은 pickle 직렬화를 사용하므로 큰 데이터를 전달하면 오히려 느려집니다.

12캐싱 전략

다양한 캐싱 전략으로 반복 계산을 제거하고 응답 속도를 향상시킵니다.

Python code

from functools import lru_cache
from collections import OrderedDict
import time

# 1. lru_cache (가장 간단)
@lru_cache(maxsize=256)
def expensive_calc(n: int) -> int:
    time.sleep(0.001)  # 비용이 큰 계산 시뮬레이션
    return n ** 2 + n

# 2. TTL 캐시 (시간 제한)
class TTLCache:
    def __init__(self, ttl_seconds: float = 60.0):
        self._cache: dict = {}
        self._ttl = ttl_seconds

    def get(self, key, default=None):
        if key in self._cache:
            value, timestamp = self._cache[key]
            if time.time() - timestamp < self._ttl:
                return value
            del self._cache[key]
        return default

    def set(self, key, value):
        self._cache[key] = (value, time.time())

# 3. LRU 캐시 (수동 구현)
class LRUCache:
    def __init__(self, capacity: int):
        self._cache = OrderedDict()
        self._capacity = capacity

    def get(self, key):
        if key in self._cache:
            self._cache.move_to_end(key)
            return self._cache[key]
        return None

    def put(self, key, value):
        if key in self._cache:
            self._cache.move_to_end(key)
        self._cache[key] = value
        if len(self._cache) > self._capacity:
            self._cache.popitem(last=False)

# 사용
cache = LRUCache(3)
cache.put("a", 1)
cache.put("b", 2)
cache.put("c", 3)
cache.get("a")      # a를 최근으로
cache.put("d", 4)   # b가 제거됨 (가장 오래됨)
print(f"a: {cache.get('a')}")  # 1
print(f"b: {cache.get('b')}")  # None (제거됨)
알아두면 좋은 점

캐시 적중률이 낮으면 오히려 오버헤드가 됩니다. cache_info()로 적중률을 모니터링하세요.

자주 하는 실수

캐시를 무효화하지 않으면 오래된 데이터를 반환합니다. TTL 또는 이벤트 기반 무효화를 구현하세요.

13데이터베이스 쿼리 최적화

N+1 문제, 인덱싱, 배치 처리 등 데이터베이스 성능 최적화 패턴입니다.

Python code

import sqlite3
import time

# 테스트 DB 생성
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
conn.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, "
             "user_id INTEGER, amount REAL)")

# 대량 데이터 삽입
conn.executemany("INSERT INTO users VALUES (?, ?)",
    [(i, f"User-{i}") for i in range(1000)])
conn.executemany("INSERT INTO orders VALUES (?, ?, ?)",
    [(i, i % 1000, i * 100) for i in range(10000)])
conn.commit()

# 안티패턴: N+1 쿼리
start = time.perf_counter()
users = conn.execute("SELECT id FROM users LIMIT 100").fetchall()
for (uid,) in users:
    conn.execute("SELECT SUM(amount) FROM orders WHERE user_id=?",
                 (uid,)).fetchone()
t1 = time.perf_counter() - start
print(f"N+1 쿼리: {t1:.4f}초 (101회 쿼리)")

# 최적화: JOIN으로 1회 쿼리
start = time.perf_counter()
conn.execute("""
    SELECT u.id, u.name, COALESCE(SUM(o.amount), 0)
    FROM users u
    LEFT JOIN orders o ON u.id = o.user_id
    GROUP BY u.id
    LIMIT 100
""").fetchall()
t2 = time.perf_counter() - start
print(f"JOIN 쿼리: {t2:.4f}초 (1회 쿼리)")
print(f"속도 향상: {t1/t2:.1f}x")

# 인덱스 추가
conn.execute("CREATE INDEX idx_orders_user ON orders(user_id)")
start = time.perf_counter()
conn.execute("SELECT * FROM orders WHERE user_id = 500").fetchall()
t3 = time.perf_counter() - start
print(f"인덱스 조회: {t3:.6f}초")

conn.close()
알아두면 좋은 점

EXPLAIN QUERY PLAN으로 쿼리 실행 계획을 확인하면 인덱스 사용 여부를 알 수 있습니다.

자주 하는 실수

모든 컬럼에 인덱스를 추가하면 INSERT/UPDATE 성능이 저하됩니다. 자주 검색하는 컬럼에만 추가하세요.

14비동기 성능

비동기 프로그래밍의 성능 특성을 이해하고 I/O 바운드 작업을 최적화합니다.

Python code

import asyncio
import time

async def io_task(name: str, delay: float) -> str:
    await asyncio.sleep(delay)
    return f"{name} 완료"

async def benchmark_sequential():
    start = time.perf_counter()
    for i in range(5):
        await io_task(f"작업-{i}", 0.5)
    return time.perf_counter() - start

async def benchmark_concurrent():
    start = time.perf_counter()
    await asyncio.gather(
        *[io_task(f"작업-{i}", 0.5) for i in range(5)]
    )
    return time.perf_counter() - start

async def benchmark_batched():
    """배치 처리: 동시성을 제한하며 실행"""
    start = time.perf_counter()
    sem = asyncio.Semaphore(3)
    async def limited(name, delay):
        async with sem:
            return await io_task(name, delay)
    await asyncio.gather(
        *[limited(f"작업-{i}", 0.5) for i in range(9)]
    )
    return time.perf_counter() - start

async def main():
    t1 = await benchmark_sequential()
    print(f"순차 실행:   {t1:.2f}초")

    t2 = await benchmark_concurrent()
    print(f"동시 실행:   {t2:.2f}초 ({t1/t2:.1f}x 빠름)")

    t3 = await benchmark_batched()
    print(f"배치 실행:   {t3:.2f}초 (동시 3개)")

    print(f"\n핵심: I/O 대기 시간을 겹치는 것이 비동기의 핵심")

asyncio.run(main())
알아두면 좋은 점

비동기는 I/O 대기 시간을 겹쳐서 총 시간을 줄입니다. CPU 바운드 작업에는 효과가 없습니다.

자주 하는 실수

asyncio.gather()에 수천 개의 태스크를 넣으면 메모리와 연결 수가 폭발합니다. Semaphore로 제한하세요.

15PyPy

PyPy는 JIT 컴파일러를 내장한 Python 인터프리터로, CPython 대비 수배~수십배 빠른 성능을 제공합니다.

Python code

import time
import sys

print(f"Python 구현체: {sys.implementation.name}")
print(f"버전: {sys.version}")

# PyPy에서 극적으로 빨라지는 코드
def numerical_heavy(n: int) -> float:
    """반복적 수치 계산 (JIT 최적화 대상)"""
    total = 0.0
    for i in range(n):
        total += (i * 0.5) ** 2 + i * 1.5
    return total

def string_heavy(n: int) -> int:
    """문자열 처리"""
    count = 0
    for i in range(n):
        s = f"item_{i}"
        if "5" in s:
            count += 1
    return count

# 벤치마크
for name, func, arg in [
    ("수치 계산", numerical_heavy, 1_000_000),
    ("문자열 처리", string_heavy, 500_000),
]:
    start = time.perf_counter()
    result = func(arg)
    elapsed = time.perf_counter() - start
    print(f"{name}: {elapsed:.3f}초 (결과: {result})")

# PyPy 사용 시 주의사항
notes = """
=== PyPy 가이드 ===
장점: JIT 컴파일로 루프/수치 계산 5-50x 빠름
설치: pypy.org에서 다운로드 또는 conda install pypy
호환: 대부분의 순수 Python 코드 호환
주의:
  - C 확장(numpy, pandas)은 호환 안 될 수 있음
  - 시작 시간이 CPython보다 느림 (JIT 워밍업)
  - 메모리 사용량이 더 클 수 있음
적합: 루프 중심 코드, 순수 Python, 장기 실행 서비스
부적합: C 확장 의존, 짧은 스크립트, 데이터 과학
"""
print(notes)
알아두면 좋은 점

PyPy는 루프 중심의 순수 Python 코드에서 가장 큰 성능 향상을 보입니다. 장기 실행 서비스에 적합합니다.

자주 하는 실수

PyPy는 numpy, pandas 등 C 확장 모듈과 호환되지 않을 수 있습니다. 프로젝트 의존성을 먼저 확인하세요.

정리하며

  • 추측으로 고치지 말고 cProfile로 호출 횟수와 누적 시간을 먼저 확인합니다
  • 짧은 구간의 상대 비교는 cProfile 대신 timeit으로 측정합니다
  • 객체를 수백만 개 만드는 경로에서만 __slots__의 효과가 눈에 보입니다
  • multiprocessing은 피클링 비용을 계산해 보고 도입 여부를 정합니다

더 깊이 들어가고 싶다면 Python 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.