파이썬은 왜 멀티스레드를 잘 안 쓸까? GIL부터 FastAPI 비동기 처리까지

2026. 7. 5. 22:15·🍞 AI
반응형

시작하게 된 계기

백엔드 개발을 하다 보면 "성능을 높이려면 병렬로 처리하면 되지 않을까?"라는 생각을 자연스럽게 하게 된다. 다른 언어라면 스레드를 여러 개 띄우는 게 첫 번째 선택지겠지만, 파이썬에서는 이야기가 조금 다르다. GIL(Global Interpreter Lock) 때문이다.

이번 글에서는 GIL이 정확히 무엇이고 왜 멀티스레딩의 발목을 잡는지, 그리고 실무에서는 이를 어떻게 우회해서 병렬/동시 처리를 하는지 — 특히 FastAPI가 이 문제를 어떻게 다루는지까지 정리해본다.

GIL이란 무엇인가

GIL은 CPython(우리가 흔히 쓰는 파이썬 인터프리터)이 한 번에 하나의 스레드만 파이썬 바이트코드를 실행하도록 강제하는 락(lock)이다. 즉, 프로세스 안에 스레드를 아무리 여러 개 만들어도 실제로 파이썬 코드를 실행하는 시점에는 항상 하나의 스레드만 CPU를 점유한다. 멀티코어 CPU를 쓰고 있어도 파이썬 스레드들은 서로 번갈아가며 GIL을 넘겨받는 방식으로 동작하기 때문에, CPU 코어를 동시에 여러 개 활용하는 것이 불가능하다.

왜 이런 설계를 했을까

GIL이 존재하는 이유는 크게 두 가지로 알려져 있다.

  1. 메모리 관리의 안전성: 파이썬 객체는 참조 카운팅(reference counting) 방식으로 가비지 컬렉션을 하는데, 여러 스레드가 동시에 참조 카운트를 변경하면 레이스 컨디션이 발생할 수 있다. GIL은 이를 막기 위한 가장 단순하고 확실한 방법이었다.
  2. C 확장 모듈과의 호환성: 파이썬 초창기부터 수많은 C 확장 라이브러리가 GIL이 있다는 전제로 작성되어 왔기 때문에, GIL을 제거하는 것은 생태계 전체에 영향을 주는 큰 변화다.

참고로 파이썬 3.13부터는 GIL을 선택적으로 비활성화할 수 있는 실험적 빌드(PEP 703, free-threaded 모드)가 나오기 시작했지만, 아직 프로덕션에서 널리 쓰이는 단계는 아니다.

그럼 멀티스레딩은 아예 쓸모없는가?

여기서 중요한 구분이 필요하다. 작업의 성격에 따라 GIL의 영향이 다르다.

  • CPU-bound 작업 (연산, 이미지 처리, 데이터 가공 등): GIL 때문에 스레드를 여러 개 만들어도 실질적인 성능 향상이 없다. 오히려 컨텍스트 스위칭 비용 때문에 더 느려지는 경우도 있다.
  • I/O-bound 작업 (네트워크 요청, 파일 읽기/쓰기, DB 쿼리 대기 등): I/O 대기 중에는 GIL을 놓아주기 때문에, 다른 스레드가 그 틈에 작업을 할 수 있다. 즉 이 경우엔 멀티스레딩이 여전히 유효하다.

정리하면, "파이썬은 멀티스레드를 못 쓴다"가 아니라 "CPU 연산을 병렬화하는 용도로는 못 쓴다"가 더 정확한 표현이다.

실제 병렬/동시 처리 방법

그렇다면 각 상황에서 실무적으로 어떤 선택지를 쓸 수 있는지 정리해봤다.

1. multiprocessing — CPU-bound 작업의 정석

프로세스 자체를 여러 개 띄우는 방식이다. 각 프로세스는 독립된 파이썬 인터프리터와 메모리 공간, 즉 자신만의 GIL을 가지기 때문에 진짜 병렬 실행이 가능하다.

from multiprocessing import Pool

def heavy_computation(n):
    return sum(i * i for i in range(n))

if __name__ == "__main__":
    with Pool(processes=4) as pool:
        results = pool.map(heavy_computation, [10_000_000] * 4)

다만 프로세스 간에는 메모리를 공유하지 않기 때문에 데이터를 주고받으려면 직렬화(pickle) 비용이 들고, 프로세스 생성 자체의 오버헤드도 스레드보다 크다.

2. concurrent.futures — 더 깔끔한 인터페이스

ThreadPoolExecutor와 ProcessPoolExecutor를 상황에 맞게 골라 쓸 수 있는 상위 레벨 API다.

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

# CPU-bound → ProcessPoolExecutor
with ProcessPoolExecutor() as executor:
    results = list(executor.map(heavy_computation, [10_000_000] * 4))

# I/O-bound → ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(fetch_url, url_list))

같은 코드 스타일로 CPU-bound와 I/O-bound 작업을 상황에 맞게 골라 쓸 수 있다는 게 장점이다.

3. asyncio — I/O-bound 작업의 현대적 해법

싱글 스레드에서 이벤트 루프 기반으로 동작하기 때문에 GIL 이슈 자체가 없다. I/O 대기 시간 동안 다른 코루틴에게 제어권을 넘기는 방식이라, 수천 개의 동시 연결도 스레드 방식보다 훨씬 가볍게 처리할 수 있다.

import asyncio
import httpx

async def fetch(client, url):
    response = await client.get(url)
    return response.status_code

async def main(urls):
    async with httpx.AsyncClient() as client:
        tasks = [fetch(client, url) for url in urls]
        return await asyncio.gather(*tasks)

FastAPI는 이걸 어떻게 다룰까

여기서부터가 최근에 실제로 궁금해서 찾아본 부분이다. FastAPI는 asyncio 기반이라 기본적으로 비동기 프레임워크지만, 동시에 동기 함수도 자연스럽게 지원한다. 이 두 가지가 내부적으로 어떻게 처리되는지가 흥미로웠다.

async def vs def

@app.get("/async-endpoint")
async def async_handler():
    result = await some_async_db_call()
    return result

@app.get("/sync-endpoint")
def sync_handler():
    result = some_blocking_db_call()
    return result
  • async def로 선언한 핸들러는 메인 이벤트 루프에서 바로 실행된다. 이 안에서 await 없이 블로킹 코드를 호출하면 이벤트 루프 전체가 멈춰버리기 때문에 주의가 필요하다.
  • 반대로 일반 def로 선언한 핸들러는 FastAPI(정확히는 Starlette)가 자동으로 별도의 스레드풀에서 실행해준다. 그래서 내부에서 블로킹 코드를 써도 이벤트 루프를 막지 않는다.

즉 "동기 코드를 써도 되게" 스레드풀로 감싸주는 구조인데, 이 스레드풀 실행도 결국 GIL의 영향을 받는 I/O-bound 상황에 최적화된 선택이라는 걸 알면 왜 이런 구조를 택했는지 이해가 된다.

진짜 CPU-bound 작업이 껴 있다면

FastAPI 안에서 무거운 연산(이미지 리사이징, 대량 데이터 가공 등)을 처리해야 한다면, 스레드풀만으로는 GIL 문제를 피할 수 없다. 이럴 땐 ProcessPoolExecutor를 이벤트 루프와 함께 써서 별도 프로세스로 위임하는 방식을 쓴다.

from concurrent.futures import ProcessPoolExecutor
import asyncio

process_pool = ProcessPoolExecutor()

@app.get("/cpu-heavy")
async def cpu_heavy_endpoint():
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(process_pool, heavy_computation, 10_000_000)
    return {"result": result}

그렇다면 uvicorn 워커는?

애플리케이션 레벨의 동시성과는 별개로, 배포 시에는 uvicorn --workers 4 처럼 워커 프로세스를 여러 개 띄우는 것도 일반적이다. 이건 각 워커가 독립된 프로세스이기 때문에 GIL의 제약과 무관하게 진짜 멀티코어를 활용하는 방법이다. 실무에서는 이벤트 루프 기반 동시성(비동기 I/O) + 멀티프로세스 워커를 함께 조합해서 쓰는 경우가 많다는 걸 알게 됐다.

정리

CPU-bound 연산 multiprocessing, ProcessPoolExecutor
I/O-bound (수십~수백 동시 요청) threading, ThreadPoolExecutor
I/O-bound (대규모 동시 연결) asyncio
FastAPI 내 동기 코드 자동 스레드풀 위임
FastAPI 내 CPU 연산 run_in_executor + ProcessPoolExecutor
배포 시 멀티코어 활용 uvicorn --workers N

GIL은 "파이썬의 한계"라기보다는, 어떤 작업을 어떤 도구로 처리해야 하는지 명확하게 구분해서 생각하게 만드는 제약에 가깝다는 생각이 들었다. 특히 FastAPI처럼 비동기를 전제로 설계된 프레임워크를 쓸 때는, 핸들러 안에서 내가 지금 짜고 있는 코드가 I/O-bound인지 CPU-bound인지를 항상 의식하는 게 성능에 직결된다는 걸 이번에 정리하면서 다시 확인할 수 있었다.

반응형
저작자표시 (새창열림)

'🍞 AI' 카테고리의 다른 글

Claude Code를 어떻게 토큰 낭비없이 쓸 수 있을까?  (1) 2026.03.23
'🍞 AI' 카테고리의 다른 글
  • Claude Code를 어떻게 토큰 낭비없이 쓸 수 있을까?
박빵이
박빵이
칭찬은 박빵도 춤추게 한다
  • 박빵이
    기억보다 기록
    박빵이
  • 전체
    오늘
    어제
    • 분류 전체보기 (334)
      • 🍞 AI (2)
      • 🍞 Cloud·Infra (1)
        • AWS SAA (1)
        • Microsoft Azure (0)
      • 🍞 FrontEnd (89)
        • HTML · CSS (4)
        • JavaScript (16)
        • TypeScript (3)
        • React (45)
        • Next.js (1)
        • Android (15)
      • 🍞 BackEnd (24)
        • Java (15)
        • Node.js (6)
        • SpringBoot (1)
      • 🍞 Algorithm (147)
        • C++ (4)
        • Baekjoon (41)
        • Programmers (97)
      • 🍞 Computer Science (18)
        • 운영체제 (1)
        • 데이터 통신 (6)
        • 네트워크 (6)
        • 데이터베이스 (1)
      • 🍞 대외활동 & 부트캠프 (42)
        • 삼성 청년 SW 아카데미 (1)
        • LG유플러스 유레카 (0)
        • 한국대학생IT경영학회 (1)
        • IT연합동아리 UMC (17)
        • 길벗 블로깅 멘토 (18)
        • IT연합동아리 피로그래밍 (3)
        • 개발 컨퍼런스 (2)
  • 블로그 메뉴

    • 글쓰기
    • Admin
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    유니온파인드
    플로이드 와샬
    Front
    코딩자율학습
    Android
    프로그래머스
    위상정렬
    umc
    JavaScript
    react
    level1
    알고리즘
    길벗 블로깅 멘토
    길벗 블로깅 멘토링
    level2
    안드로이드
    C++
    백준
    Java
    코틀린
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.6
박빵이
파이썬은 왜 멀티스레드를 잘 안 쓸까? GIL부터 FastAPI 비동기 처리까지
상단으로

티스토리툴바