FastAPI 공식문서로 알아본 Concurrency & Async
Table of Contents
현대 파이썬은, “코루틴” 을 사용하여 “비동기 코드” 를 지원한다. 이것은 async
, await
을 사용해서 구현할 수 있다. 그러면 아래 3가지가 각각 어떤 의미인지 보자.
- Asynchronous Code
async
andawait
- Coroutines
Asynchronous Code#
비동기식 코드는 프로그래밍 언어💬 가 컴퓨터/프로그램 🤖 에 코드의 어느 시점에서 컴퓨터/프로그램🤖 이 다른 무언가가 다른 곳에서 끝나기를 기다려야 할 것이라는 것을 말해주는 방법이 있다는 것을 의미한다. 다른 무엇인가를 “slow-file"📝이라고 가정해 보자.
따라서 이 시간 동안 컴퓨터는 “slow-file” 📝을 완료하는 동안 다른 작업을 수행할 수 있다.
그러고 나서는 컴퓨터 / 프로그램 🤖 은 다시 기다리고 있기 때문에 기회가 있을 때마다, 혹은 🤖 가 그 시점에서 했던 모든 일을 끝낼 때마다 돌아올 것이다. 그리고 🤖는 기다리고 있던 작업 중 이미 완료된 것이 있는지, 해야 할 일을 무엇이든 할 수 있는지 확인할 것이다.
다음으로, 🤖는 첫 번째 작업을 완료하고(예를 들어, “slow-file” 📝), 이 작업과 관련된 모든 작업을 계속한다.
“다른 것을 기다린다"는 것은 일반적으로 다음과 같이 프로세서 및 RAM 메모리의 속도에 비해 상대적으로 “느린” I/O 작업을 말한다:
- 네트워크를 통해 전송할 클라이언트의 데이터
- 네트워크를 통해 클라이언트가 수신하기 위해 프로그램에서 보낸 데이터
- 시스템에서 읽고 프로그램에 제공할 디스크의 파일 내용
- 프로그램이 디스크에 기록할 시스템에 제공한 내용
- 원격 API 작업
- 데이터베이스 작업
- 결과를 반환하기 위한 데이터베이스 쿼리
실행 결과는 대부분 I/O 작업을 대기하는데 사용되기 때문에, 이것을 “I/O bound” 작업이라고 부른다.
“비동기"라고 하는 이유는 컴퓨터/프로그램이 느린 작업과 “동기화"될 필요가 없기 때문이다. 즉, 작업이 끝나는 정확한 순간을 기다리면서 아무 것도 하지 않고, 작업 결과를 받아 계속 작업할 수 있을 때까지 기다리지 않아도 된다.
대신에 “비동기” 시스템이 되면, 작업이 끝나면 컴퓨터/프로그램이 무엇을 하러 갔든간에 조금 기다렸다가 (몇 마이크로초 정도) 결과를 받아 계속 작업할 수 있다.
“동기” (비동기와 반대)의 경우, 컴퓨터/프로그램이 다른 작업으로 전환하기 전에 모든 단계를 순차적으로 따르기 때문에 일반적으로 “순차적(sequential)“이라는 용어도 사용한다. 이러한 단계에 대기가 포함되어 있더라도 마찬가지이다.
Concurrency#
- concurrency = “동시성”
- parellelism = “병렬성”
위에서 설명한 비동기 코드의 개념은 때때로 “동시성"이라고도 불립니다. 이것은 “병렬성"과는 다르다. 동시성과 병렬성은 모두 “다른 것들이 대략 동시에 일어나는 것"과 관련이 있다.
하지만 동시성과 병렬성 사이의 세부적인 차이점은 상당히 다르다.
차이점을 이해하기 위해, 다음과 같은 햄버거🍔 에 관한 이야기를 상상해 보자 !
Concurrent Burgers#
썸남😍 과 햄버거 집에 갔다고 하자. 주문을 하기 위해서 주문대 앞에 선 줄 뒤에 설 것이다. 내 차례가 되면, 2개의 맛있는 햄버거를 주문할 것이다. 캐셔는 주방에게 2개의 햄버거를 준비하라고 전달해준다. 이때 주방장은 이미 앞 사람들의 버거를 요리하고 있다. 그 다음 나는 결제하고, 캐셔는 번호가 적힌 영수증을 준다.
버거를 기다리는 동안 썸남과 이런저런 이야기를 나누고, 그 동안 주문판에 내 번호가 나오는지 종종 확인한다. 드디어 내 차례가 주문판에 뜨면, 카운터에 가서 버거들을 가지고 자리로 온다.
이 이야기 속에서 당신이 컴퓨터/프로그램 🤖 이라고 상상해보자.
줄을 서 있는 동안, 당신은 그저 한가롭게 😴 대기하고 있을 뿐이며, 아무런 “생산적인” 일을 하지 않는다. 하지만 줄이 빠르게 진행되는 이유는 계산원이 주문만 받고 준비하지 않기 때문에 괜찮다.
그리고 당신 차례가 되면, 실제 “생산적인” 일을 한다. 메뉴를 보고, 원하는 것을 결정하고, 썸남의 선택을 받아내고, 지불하고, 올바른 지폐나 카드를 주었는지 확인하고, 정확하게 청구되었는지 확인하고, 주문에 올바른 항목이 있는지 확인하는 등의 일을 합니다.
하지만 그 후에도, 아직 햄버거를 받지 못했지만, 계산원과의 일은 “일시 정지” ⏸ 상태가 됩니다. 왜냐하면 햄버거가 준비될 때까지 기다려야 하기 때문이다.
하지만 카운터에서 떨어져 테이블에 앉아 당신 차례를 알리는 번호와 함께 있으면, 당신의 관심을 당신의 마음에 드는 사람에게로 전환 🔀 할 수 있고, 그와 함께 “작업” ⏯ 🤓을 시작할 수 있다. 그러면 당신은 다시 매우 “생산적인” 일인 마음에 드는 사람과의 데이트에 몰두하게 됩니다 😍.
그 후 계산원 💁이 카운터의 주문판에 당신의 번호를 놓음으로써 “햄버거를 만드는 일을 끝냈다"고 알린다. 하지만 당신의 차례 번호가 디스플레이에 나타났을 때 미친 듯이 뛰어가지 않는다. 당신은 아무도 당신의 햄버거를 훔쳐가지 않을 것이라는 것을 알고 있으며, 모두 자신의 차례 번호를 가지고 있기 때문이다.
그래서 당신은 마음에 드는 사람이 이야기를 마치도록 기다리고 , (현재 작업 종료 ⏯ / 처리 중인 작업 🤓), 부드럽게 미소를 지으며 햄버거를 가져오러 갈 것이라고 말한다 ⏸.
그 후에는 카운터로 돌아가 🔀, 이제 끝난 초기 작업으로 돌아가 햄버거를 집어들고 감사하다고 말한 후 테이블로 가져온다. 그것은 카운터와의 상호 작용 과제/단계를 마친다 ⏹. 그러면 새로운 작업, “햄버거 먹기” 🔀 ⏯이 시작되지만, 이전의 “햄버거 가져오기” 작업은 끝났다 ⏹.
Parallel Burgers#
이제 이것들이 “동시적 햄버거"가 아니라 “병렬적 햄버거"라고 상상해 보자.
당신은 마음에 드는 사람과 함께 병렬적 패스트푸드를 먹으러 간다. 당신은 줄을 서 있고, 동시에 요리사인 여러 (예를 들어 8명의) 계산원들이 당신 앞 사람들로부터 주문을 받는다.
당신 앞에 있는 모든 사람들은 카운터를 떠나기 전에 자신의 햄버거가 준비될 때까지 기다리고 있다. 왜냐하면 8명의 계산원 중 각각이 햄버거를 바로 준비한 후에 다음 주문을 받기 때문이다.
당신 차례가 되면, 당신과 썸남을 위한 멋진 햄버거 2개를 주문하고, 결제한다.
캐셔는 주방으로 가고, 당신은 카운터 앞에서 기다린다 🕙. 왜냐면, 주문 번호를 따로 안 주기 때문에 다른 사람들이 당신의 햄버거를 가져갈 수 도 있기 때문이다.
당신과 당신의 썸남은 누구도 자신들 앞으로 가서 햄버거를 가져가지 못하게 하느라 바쁘고, 햄버거가 도착하는 즉시 그것을 가져가야 하기 때문에, 당신의 마음에 드는 사람에게 주의를 기울일 수 없다. 😞
이것은 “동기적” 작업이다. 당신은 캐셔/요리사 👨🍳와 “동기화"되어 있다. 캐셔/요리사 👨🍳가 햄버거를 만들어 주는 정확한 순간에 거기에 있어야 하고 기다려야 한다. 그렇지 않으면 다른 누군가가 그것들을 가져갈 수 있다.
캐셔/요리사 👨🍳 는 마침내 당신의 햄버거들을 가져오고, 당신은 오랜 시간의 기다림 🕙 끝에 햄버거들을 받을 수 있다.
햄버거들 테이블로 가져온 다음, 썸남과 햄버거를 먹고, 작업은 종료된다.
많은 시간이 카운터 앞에서 대기🕙 하는데 사용되었으므로, 당신은 썸남과 많은 대화를 하지는 못했을 것이다. 😞
이 병렬 햄버거 시나리오에서, 당신과 당신의 마음에 드는 사람은 두 개의 프로세서 를 가진 컴퓨터/프로그램 🤖으로, 둘 다 오랫동안 “카운터에서 기다리는” 🕙 것에 주의를 기울이며 ⏯ 기다린다.
패스트푸드점에는 8개의 프로세서(캐셔/요리사)가 있다. 반면에 동시적 햄버거 가게는 아마도 단 2개(하나의 계산원과 하나의 요리사)만 가졌을 것이다.
하지만 동시적 햄버거 가게와 비교해서 여전히 최종 경험은 최고는 아니다. 😞
결론#
이 “당신의 마음에 드는 사람과 함께하는 패스트푸드 햄버거” 시나리오에서, 많은 대기 🕙 시간이 있기 때문에, 동시적 시스템 ⏸🔀⏯을 가지는 것이 훨씬 더 합리적이다.
이것은 대부분의 웹 애플리케이션에 해당하는 경우이다.
많은 사용자들이 있지만, 서버는 그들의 그다지 좋지 않은 연결을 통해 요청을 보내는 것을 기다리고 있다. 그리고 다시 응답이 돌아오기를 기다린다.
이 “대기” 🕙 시간은 마이크로초 단위로 측정되지만, 모두 합하면 결국 많은 대기 시간이 된다.
그래서 웹 API에 비동기적인 ⏸🔀⏯ 코드를 사용하는 것이 매우 합리적이다.
이 이야기의 교훈은 그것이 아니다. 동시성은 병렬성과 아예 다른 문제이다.
따라서 다음의 짧은 스토리를 상상해보자 : 당신은 크고 더러운 집을 청소해야 한다.
이 경우에는, 기다림이 필요 없고, 집의 여러 곳에서 해야 할 작업만 무지 많다. 주방 -> 거실 -> 방.. 등 순서를 정해서 할 수 있지만 어느 곳에서도 기다릴 필요가 없기 때문에 순서는 아무런 의미가 없다. 순서가 있는 것과 없는 것에서 같은 양의 일을 해야 한다.
이 경우에는, 8 명의 사람이 동시에 청소한다면 1명이 청소하는 것보다 훨씬 빠르게 작업을 끝낼 수 있다.
이 시나리오에서 각 청소부는 프로세서가 될 것이다. 이 때 실행시간의 대부분은 기다림보다 실제 작업시간이다. 컴퓨터에서 작업은 CPU 에 의해 실행되므로, 이러한 문제들은 “CPU bound” 이다.
CPU bound 연산의 예시들은 복잡한 수학적 연산을 포함하는 작업들이다.
- 오디오, 이미지 처리
- 컴퓨터 비전
- 머신 러닝
- 딥러닝
Concurrency, Parallelism 을 더 자세히 비교하기 위해 파이썬으로 실습 코드를 작성했다. 아래의 글에서 확인할 수 있다.
Concurrency + Parallelism : Web + Machine Learning#
FastAPI 를 사용해서는, 웹 개발에서 흔한 동시성의 장점을 가질 수 있다. 또한, 병렬성과 멀티프로세싱(여러 개의 프로세스가 병렬적으로 실행되는 것) 의 장점도 가진다.
그리고 파이썬은 데이터 사이언스에서 가장 많이 사용되는 언어이므로, FastAPI 는 데이터 사이언스 / 머신러닝 web API 를 서빙하기 위한 가장 적합한 프레임워크이다.
async and await#
현대의 파이썬 버전은 비동기 코드를 정의하는 매우 직관적인 방법을 가지고 있다. 이것은 일반적인 “순차적” 코드처럼 보이게 하고, 적절한 순간에 “대기"를 처리해 준다.
결과를 제공하기 전에 대기가 필요한 연산이 있고, 이러한 새로운 파이썬 기능을 지원하는 경우, 코드를 다음과 같이 작성할 수 있다:
burgers = await get_burgers(2)
여기서 핵심은 await
이다. 이것은 파이썬에게 get_burgers()
가 작업을 마치고 🕙 결과를 burgers에 저장하기 전에 대기 ⏸ 해야 함을 알려준다. 이를 통해 파이썬은 그 사이에 다른 일을 하러 가야 한다는 것을 알게 된다 🔀 ⏯ (예를 들어 다른 요청을 받는 것처럼).
await
이 작동하려면, 비동기를 지원하는 함수 내부에 있어야 한다. 이를 위해서는 단순히 async def
를 사용해서 함수를 선언하면 된다.
async def get_burgers(number: int):
# Do some asynchronous stuff to create the burgers
return burgers
async def
를 사용함으로써, 파이썬은 해당 함수 내에서 await
표현식에 주의해야 되며, 해당 함수의 실행을 “일시 정지” ⏸ 할 수 있고, 다른 일을 하러 가기 전에 🔀 다시 돌아올 수 있음을 알게 된다.
async def
함수를 호출하려면, 그것을 “await” 해야 한다. 따라서 다음과 같은 방식으로는 작동하지 않는다 :
# This won't work, because get_burgers was defined with: async def
burgers = get_burgers(2)
await
는 async def
로 정의된 함수 내부에서만 사용할 수 있다는 것을 알아차렸을 것이다.
하지만 동시에, async def
로 정의된 함수는 “await"되어야 한다. 따라서 async def
로 정의된 함수는 역시 async def
로 정의된 함수 내부에서만 호출될 수 있다.
그렇다면 닭과 달걀의 문제처럼, 첫 번째 async 함수를 어떻게 호출할까?
FastAPI를 사용하고 있다면 이에 대해 걱정할 필요가 없다. 왜냐하면 그 “첫 번째” 함수가 당신의 경로 작업 함수가 될 것이고, FastAPI는 올바른 방법을 알고 있기 때문이다.
하지만 FastAPI 없이 async / await를 사용하고 싶다면, 그것도 가능하다. 왜냐면 FastAPI 는 AnyIO 로 작성되었고, 이것은 파이썬의 라이브러리인 asyncio 와 Trio 호환된다.
Coroutines#
코루틴은 async def
함수에 의해 반환된 것을 지칭하는 용어이다. 파이썬은 이것이 시작될 수 있고 어느 시점에 끝날 것이지만, 내부에 await
이 있을 때마다 내부적으로 일시 정지될 수도 있는 함수와 유사한 것임을 알고 있다.
하지만 async와 await를 사용하여 비동기 코드를 활용하는 모든 기능은 종종 “코루틴” 사용으로 요약된다. 이것은 Go의 주요 특징인 “고루틴(Goroutines)“과 비교할 수 있다.
이제 아래의 말이 이해 된다.
Modern versions of Python have support for “asynchronous code” using something called “coroutines”, with
async
andawait
syntax.