← SEARCHABLE ARCHIVE
Python · 2026-08-12

Python 기초부터 마스터까지 — 9화

스레드, 프로세스, asyncio와 프로파일링 이해하기

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

여러 작업을 동시에 처리한다고 항상 빨라지는 것은 아니다. 작업이 CPU 계산을 오래 하는지, 파일과 네트워크를 기다리는지에 따라 실행 모델과 병목이 달라진다.

동시성과 병렬성 구분하기

도구잘 맞는 작업주의할 점
ThreadPoolExecutor동기식 파일·네트워크 I/O공유 상태와 잠금
ProcessPoolExecutor독립적인 CPU 집약 계산프로세스 시작과 데이터 전달 비용
asyncio많은 비동기 네트워크·타이머 작업이벤트 루프를 막는 동기 코드

동시성은 여러 작업의 진행을 겹치는 구조이고 병렬성은 여러 계산이 실제 같은 시각에 실행되는 것이다. 먼저 순차 코드가 정확하게 동작하는지 확인하고 시간을 측정한 뒤 병목에 맞는 방식을 선택해야 한다.

스레드와 프로세스

from concurrent.futures import ThreadPoolExecutor
from pathlib import Path


def file_size(path: Path) -> int:
    return path.stat().st_size


paths = list(Path("data").glob("*.json"))
with ThreadPoolExecutor(max_workers=4) as pool:
    sizes = list(pool.map(file_size, paths))

여러 작업이 같은 리스트나 카운터를 변경하면 실행 순서에 따라 결과가 달라질 수 있다. 가능하면 공유 상태를 바꾸는 대신 각 작업이 결과를 반환하게 만들고, 꼭 필요한 상태는 명시적인 잠금으로 보호한다.

기본 CPython에서 긴 Python 계산은 GIL의 영향을 받으므로 CPU 작업은 프로세스를 검토할 수 있다. 다만 작은 작업을 너무 잘게 나누면 프로세스 시작과 직렬화 비용이 이득보다 커질 수 있다.

asyncio로 I/O 겹치기

import asyncio


async def load_player(player_id: int) -> str:
    await asyncio.sleep(0.1)  # 비동기 I/O를 흉내 낸다.
    return f"player-{player_id}"


async def main() -> None:
    async with asyncio.TaskGroup() as group:
        tasks = [
            group.create_task(load_player(player_id))
            for player_id in range(5)
        ]

    print([task.result() for task in tasks])


asyncio.run(main())

TaskGroup은 Python 3.11 이상에서 사용할 수 있다. 자식 작업 하나가 처리되지 않은 예외로 실패하면 나머지를 취소하고 모든 작업의 정리를 기다린 뒤 오류를 전달한다.

coroutine 안에서 time.sleep이나 긴 계산을 실행하면 이벤트 루프 전체가 멈춘다. 취소될 수 있는 작업은 finally에서 파일과 연결을 정리하고 취소를 정상적인 제어 흐름으로 다뤄야 한다.

추측보다 측정

python -m timeit -s "values=list(range(10000))" "9999 in values"
python -m timeit -s "values=set(range(10000))" "9999 in values"
python -m cProfile -m my_app

timeit은 작은 코드 조각을 비교하고 cProfile은 전체 실행에서 시간이 쌓이는 위치를 찾는 데 사용한다. 입력 크기, Python 버전, 실행 환경, 측정 명령을 함께 기록해야 결과를 다시 비교할 수 있다.

비동기와 병렬 처리는 자동 성능 버튼이 아니다. 실패, 취소, timeout, 공유 상태, 종료 조건까지 설계하고 실제 작업으로 측정했을 때만 복잡성을 늘릴 이유가 생긴다.