FastAPI

jin·3일 전

FastAPI

  1. 요청 수신: 여러 사용자가 동시에 API 요청을 보냅니다.

  2. 이벤트 루프의 스케줄링: Uvicorn(FastAPI를 실행하는 ASGI 서버)의 이벤트 루프가 이 요청들을 하나의 큐(Queue)에 담아 순차적으로 실행하기 시작합니다.

  3. I/O 작업과 제어권 양보 (await): 요청을 처리하다가 데이터베이스 조회나 외부 API 호출 같이 시간이 오래 걸리는 I/O(입출력) 작업을 만나면 await 키워드가 실행됩니다.

  4. 다른 Task 처리: await를 만난 함수는 "나 여기서 응답 올 때까지 기다려야 하니까, 내 차례는 뒤로 미루고 다른 일 먼저 해!"라며 이벤트 루프에 제어권을 자발적으로 반납(Yield)합니다.

  5. 작업 재개: 이벤트 루프는 대기열에 있던 다른 요청을 처리하다가, 아까 요청했던 DB 조회가 끝났다는 신호(Callback)를 받으면 멈췄던 지점부터 다시 코드를 실행합니다.

FastAPI의 스케쥴링

: 자발적 양보
FastAPI의 단일 쓰레드 환경에서는 OS가 개입하지 않습니다. 대신 파이썬의 이벤트 루프(Event Loop)라는 녀석이 스케줄러 역할을 합니다.

상황: 알바생(단일 쓰레드)은 딱 1명뿐입니다.

이벤트 루프의 스케줄링 방식: 알바생은 커피 머신(DB 등)을 작동시킨 뒤 await(제어권 양보)를 외칩니다. 그러면 현황판(이벤트 루프)을 쓱 쳐다봅니다.
현황판(스케줄러)에는 이렇게 적혀 있습니다.

"지금 1번 손님 커피는 기계가 내리고 있으니 놔두고, 너 노는 동안 빨리 2번 손님 주문부터 받아!"

특징: 알바생은 매니저(OS)에게 강제로 쫓겨나는 게 아닙니다. 자기가 붕 뜨는 시간(I/O 대기)에 스스로 현황판(이벤트 루프)의 지시(스케줄링)를 보고 다음 할 일을 찾아갑니다.
Task와 Couroutine
알바생(단일 쓰레드)이 혼자서 수십 명의 주문을 처리할 때, 머릿속으로 모든 걸 기억하려면 당연히 터져버릴 겁니다. 그래서 알바생 앞에는 **클립보드(이벤트 루프)**가 있고, 손님들의 주문 상태를 **포스트잇(Task 객체)**에 적어서 붙여놓습니다.

1. 상태 기록: 알바생이 A 손님의 에스프레소 버튼을 누르고 제어권을 양보(await)할 때, 멍하니 다른 일을 하러 가는 게 아닙니다.
2. 포스트잇에 이렇게 적습니다: "A 손님 / 아메리카노 / 에스프레소 추출 대기 중 / 머신 울리면 다음 할 일: 물 붓기"
3. 클립보드에 붙이기: 이 포스트잇을 클립보드(대기열)에 붙여둡니다.
4. 다음 일 확인: 알바생은 클립보드를 보고 다음으로 해야 할 B 손님의 포스트잇을 떼어서 봅니다. "B 손님 / 생과일주스 / 믹서기 돌릴 차례" 4. 작업 재개: 커피 머신이 다 되었다고 울리면(이벤트 발생), 알바생은 A 손님의 포스트잇을 다시 꺼내 읽습니다. "아하, 에스프레소가 다 내려왔으니 이제 물을 부을 차례군!" 하고 정확히 멈췄던 지점부터 일을 다시 시작합니다.

FastAPI에서는 메모리의 Heap에 Task라는 이름의 객체를 만들어둔다. 이 객체 안에는 현재까지의 변수 값들과 다음에 실행해야 하는 코드의 줄 번호가 저장되어 있다.
이벤트 루프는 파이썬의 메모리 상에 각 요청들의 현재 상태와 멈춘 지점을 꼼꼼하게 기록해둔 객체(Task, Coroutine)들을 아주 빠르게 번갈아가며 읽고 실행한다.

Coroutine 객체에 따른 Heap의 부하는 없을까?

동시에 10,000개의 요청을 힙에 올려두어도 고작 수십 메가바이트(MB) 정도밖에 차지하지 않는다.
게다가 이 객체들이 힙에 평생 쌓여있는 것도 아닙니다.요청이 완전히 끝나서 손님에게 응답을 보내고 나면, 해당 코루틴 객체는 더 이상 필요가 없어진다.

이때 파이썬의 가비지 컬렉터(Garbage Collector)가 백그라운드에서 쓱 다가와 "어? 이 포스트잇 이제 안 쓰네?" 하고 힙 메모리에서 즉시 지워버린다.

자바의 JVM과의 비교
비교 포인트⚡FastAPI (비동기 이벤트 루프)☕ JVM (전통적인 멀티 쓰레드)
일꾼(Thread)의 수단 1개 (기본적으로)수십 ~ 수백 개 (미리 만들어 둠 - Thread Pool)
스케줄링 주체이벤트 루프 (파이썬 내부에서 알아서)운영체제(OS) (강제로 교체)
요청 1개당 메모리아주 가벼움 (수십 KB 힙 메모리 객체)무거움 (약 1MB 이상의 OS 쓰레드 스택 메모리)
대기 시간 처리법await로 멈춰두고 다른 손님 주문 받으러 감DB 응답이 올 때까지 그 직원은 아무것도 안 하고 쉼 (Blocking)
컨텍스트 스위칭 비용거의 없음 (포스트잇 바꿔 읽기)높음 (운영체제가 직원을 통째로 교체)
가장 큰 강점트래픽이 엄청나게 몰리는 채팅, I/O 바운드 작업복잡한 수학 계산, 데이터 변환 등 CPU 바운드 작업
1. RUNNING (실행 상태): "DB야, 데이터 내놔!"
Thread-A는 CPU를 차지하고 열심히 코드를 실행합니다. 그리고 네트워크를 통해 DB 서버로 쿼리를 전송합니다.

2. BLOCKED / WAITING (대기 상태): "OS 매니저님, 저 DB 올 때까지 잘게요." (이게 블로킹입니다!)
DB 서버가 데이터를 찾아서 돌려주려면 네트워크를 타고 가야 하니 시간이 걸립니다(수십~수백 밀리초).
이때 OS 스케줄러(매니저)가 개입합니다.
OS는 "Thread-A야, 너 어차피 당장 할 수 있는 연산(계산) 없지? CPU 아까우니까 너 저기 '대기실(Wait Queue)'에 가서 자고 있어!"라며 Thread-A를 CPU에서 강제로 쫓아냅니다. (컨텍스트 스위칭 발생)
이제 Thread-A는 CPU를 0% 사용하며 완전히 멈춰 있는 수면 상태가 됩니다. 일을 전혀 안 하는 거죠.
그 사이 OS는 대기표를 뽑고 기다리던 Thread-B를 CPU에 올려서 다른 손님의 요청을 처리하게 합니다. (CPU는 쉬지 않습니다!)

3. READY (준비 상태): "앗, DB 왔다! 저 다시 일할래요!"
DB 서버에서 드디어 데이터 응답이 도착했습니다. 네트워크 카드(NIC)가 OS에게 "데이터 왔어요!"라고 인터럽트(신호)를 보냅니다.
OS는 대기실에서 자고 있던 Thread-A의 멱살을 잡아 깨워서 '준비 줄(Ready Queue)'에 세웁니다.
차례가 오면 다시 CPU를 배정받아 하던 일을 마저 끝냅니다.

어? CPU가 안 놀고 다른 쓰레드(B, C)를 실행하면 효율적인 거 아닌가요?

앞서 자바(Spring/Tomcat)는 미리 정해진 Thread-Pool(보통 200개)를 사용한다. 만약 어떤 이벤트가 터져서 손님 200명이 동시에 접속했다. 쓰레드 200개이 각각 요청을 받고, 전부 DB에 쿼리를 날린다. 그리고 DB 응답을 기다리기 위해 200개의 쓰레드는 몽땅 Wait Queue에 들어간다.

  • CPU 사용률: 쓰레드 200개가 다 Wait상태여서 CPU는 스케쥴링할 프로세스가 없다.
  • 서버상태: 하지만 그 다음 요청이 들어오면? 모든 쓰레드가 대기 상태이기 때문에 요청이 거절되고 로딩만 돈다. 이를 Thread Pool Hell이라고 한다.

하지만 FastAPI는 단일 쓰레드이지만, Wait 상태가 되지 않는다. (Non-blocking)
대신 오래 걸리는 DB 응답은 나중에 이벤트 루프가 알려줄 테니깐 wait 상태로 자지 않고 바로 다음 요청을 받아 CPU위에서 움직인다.

시나리오
만약 파이썬 비동기 환경에서 예전의 동기식 라이브러리를 사용한다면?

결과: 쓰레드 자체가 멈춘다.

해결책 1. 비동기를 지원하는 최신 라이브러리로 바꾼다.
해결책 2. 예비 쓰레드를 깨워서 시킨다

# 🛠️ 예비 인력에게 떠넘기기 (FastAPI의 마법)
@app.get("/heavy-sync-task")
def do_sync_work():
    # async가 없는 일반 def 함수입니다!
    # FastAPI가 알아서 "이건 멈추는 작업이네? 메인 쓰레드 말고 백그라운드 쓰레드 풀(Thread Pool)에 던져서 실행해!" 라고 처리합니다.
    result = heavy_sync_db_query() 
    return result

async def안에서 멈추는 블로킹 코드를 쓰면 서버 전체가 죽지만, 그냥 def로 선언하면 FastAPI 내부적으로 마치 JVM 처럼 별도의 쓰레드 풀에 작업을 위임해버려서 메인 이벤트 루프를 안전하게 보호한다.

profile
성장중

0개의 댓글