FastAPI 문서를 읽다 async/await에서 멈춰서다

gigagookbob·2026년 1월 20일

호기심 백과사전

목록 보기
3/3

이 글은 FastAPI의 문서를 읽다가 호기심 레이더에 걸려 헷갈리던 개념들을 제대로 이해하게 된 기록이다.


FastAPI를 쓰다 보면 async, await는 거의 자동으로 따라온다.
나도 처음엔 그냥 “비동기니까 빠르겠지” 정도로만 이해하고 썼다.

그런데 막상 파고들다 보니, 머릿속에서 계속 걸리는 질문들이 있었다.

  • async면 동시에 실행되는 거 아닌가?
  • await을 하면 결국 기다리는 건데, 그럼 동기랑 뭐가 다른 거지?
  • CPU 많이 쓰는 작업도 await으로 감싸면 해결되는 거 아닌가?

이 질문들을 하나씩 따라가다 보니, 내가 그동안 async를 문법으로만 이해하고 있었다는 걸 깨닫게 됐다.

이 글은 그 생각의 흐름을 그대로 정리한 기록이다.

async는 “동시에 실행”이 아니다

처음 내가 했던 가장 큰 오해는 이거였다.

비동기 = 동시에 실행

이 말은 반은 맞고, 반은 틀리다.

async/await는 병렬 실행이 아니다.

정확히 말하면 동시성(concurrency) 이다.

  • 병렬(parallel): 실제로 CPU 코어 여러 개에서 동시에 실행
  • 동시성(concurrency): 한 번에 하나씩 실행하지만, 기다림을 겹쳐서 처리

async는 후자다.

즉,

async는 여러 일을 동시에 실행한다가 아니라, 기다리는 시간 동안 다른 일을 처리한다에 가깝다.

async 함수는 호출하면 실행되지 않는다

이것도 처음엔 꽤 헷갈렸다.

async def foo():
    print("hello")

foo()

이걸 실행하면 hello가 출력될까?
아니다.

foo()실행이 아니라 코루틴 객체 생성이다.
실제로 실행되려면 반드시 이게 필요하다.

await foo()

여기서 await는 지금 실행하라는 버튼이라기보다는, 이 코루틴을 이벤트 루프에게 맡긴다 에 더 가깝다고 할 수 있다.

FastAPI에서는 이 await를 내가 직접 쓰지 않아도, 프레임워크와 ASGI 서버가 내부적으로 처리해 주기 때문에 그냥 함수처럼 실행되는 것처럼 보였을 뿐이다.

await은 “기다리는 명령”이 아니다

여기서 가장 중요한 오해를 하나 정리해야 한다.

await을 하면 이 작업이 끝날 때까지 다른 작업들이 기다리는 거 아니야?

아니다. 완전히 반대다.

await를 만나는 순간, 다른 작업들이 멈추는 게 아니라 현재 작업만 멈춘다

정확히 말하면,

"나는 지금 할 일이 없으니까 잠깐 멈출게. 그동안 다른 작업들 먼저 처리해도 돼.”

이게 async/await의 본질이다.

동기 코드에서는 하나가 기다리면 전부 멈추지만, async 코드에서는 하나가 기다리면 그것만 멈춘다.

이 차이 때문에 서버 처리량이 완전히 달라진다.

그럼 await heavy_cpu_work() 하면 되는 거 아니야?

그런데 나는 또 이렇게도 생각해봤다.

CPU 많이 쓰는 작업도 await으로 감싸면 되는 거 아닌가?

결론부터 말하면 안 된다.

이유는 간단하다.

await은 “기다릴 수 있는 작업” 에만 의미가 있다.

CPU bound 작업은 중간에 멈출 수 없고, 기다림이 없고, CPU를 계속 점유한다.

즉, await할 수 있는 지점 자체가 없다!

심지어 CPU 작업을 async def로 감싸도, 내부에 await가 없으면 이벤트 루프는 계속 붙잡힌다.

이 순간 async는 동기보다 더 나빠진다.


그래서 결국 핵심 기준은 이거다

“이 작업은 기다릴 수 있는가?”

이 질문 하나로 정리가 된다.

기다릴 수 있는 작업 (I/O bound)

  • 네트워크 요청
  • DB 쿼리
  • 파일 읽기
  • 외부 API 호출

위와 같은 작업들은 async / await가 의미있다.

기다릴 수 없는 작업 (CPU bound)

  • 복잡한 계산
  • 이미지/영상 처리
  • 암호화
  • 머신러닝 추론

위와 같은 작업들은 이벤트 루프 밖으로 보내야 한다.

이걸 구분하지 않으면, async 서버를 쓰면서도 성능이 망가진다.

그럼 CPU bound 작업은 어떻게 처리해야 하나?

정답은 하나다.

이벤트 루프 밖으로 내보낸다

방법은 상황에 따라 다르다고 할 수 있다.

가벼운 작업은 thread pool로, 무거운 작업은 worker 프로세스로, 매우 무거운 작업은 별도 서비스로 처리한다.

await은 컨텍스트 스위칭인가?

이 부분에서 머릿속 그림이 하나 생겼다.

A 작업을 하다가 await을 만나면
B 작업으로 갔다가
다시 A로 돌아오는 느낌

이 그림 자체는 맞다.

다만 조금 더 정확히 정리하면,

  • OS 수준의 컨텍스트 스위칭이 아니라
  • 이벤트 루프가 관리하는 코루틴 전환

이 조금 더 적합한 것 같다.

  • 커널의 개입이 없고, 스레드 전환이 없으며 매우 가볍지만, 양보하지 않으면 전체가 막힌다.

그래서 async는 협력적 멀티태스킹이라고 부른다.


정리하면서 얻은 한 문장

이 과정을 거치고 나서, 내 머릿속에 남은 문장은 이거다.

async는 빠르게 만드는 기술이 아니라 기다림을 설계하는 기술이다.

그리고

CPU 작업은 async로 해결하는 문제가 아니라 어디서 실행할지의 문제다.


마무리

이 글은 async/await를 설명하려고 쓴 글이라기보다는, 내가 헷갈렸던 지점을 하나씩 정리한 기록에 가깝다.

만약 예전의 나처럼

  • async가 뭔지 어렴풋이 알고
  • await을 쓰긴 쓰는데
  • 그래서 뭐가 다른 거지? 라는 느낌이 있다면

이 흐름이 한 번쯤은 도움이 될 것 같다.

profile
선명한 기억보다 희미한 기록으로

0개의 댓글