본문 바로가기
Backend MLOps/Fastapi

[Fastapi] asyncio 제대로 써보기 with pytest - 1

by SteadyForDeep 2022. 6. 3.
반응형

시작

이번 포스팅은 이론적으로 공부했던 비동기 처리의 요약을 포스팅한다.
다음 포스팅 부터 이 포스팅의 지식에 근거하여 실질적인 성능향상을 끌어내고
얼마나 성능이 향상되는지도 알아보려고 한다.

최근 비동기를 활용하여 백엔드 프로젝트를 하나 진행한 적이 있다.
이전에 Go 관련 포스팅을 진행할 때 마지막엔 비동기 구현을 꼭 넣고자 했으나
이 프로젝트를 먼저 진행하게 되면서 파이썬의 async await을 먼저 포스팅 해보고자 한다.
async는 파이썬의 네이티브 코루틴이 적용되는 함수를 생성하는 방법으로
비동기 함수 구현을 사용할 때 쉽고 간단하게 구현할 수 있도록 해준다.

동기? 비동기? 병렬?

우선은 비동기가 뭔지 정리하고 넘어가야겠다.
비슷한 것으로 병렬연산도 함께 정리하면 좋다.

내 언어로 정리해보면

  • 동기 처리는 실행이 선언된 순서대로 실행되며 먼저 실행된 함수가 끝나면 다음 함수가 실행된다.
  • 비동기 처리는 실행 중 지연이 발생하면 다른 비동기 함수에게 실행 순서를 양보할 수 있다.
  • 병렬 처리는 동일한 연산을 독립된 자원들에 반복적으로 수행해야 할 때 동시 수행이 가능한 방식이다.

삼천포로 빠지면...

리눅스의 탄생이라는 책을 보면 리눅스가 처음 생길 때 모습을 볼 수 있다.
거대한 컴퓨터가 하나의 방에 있고 그 컴퓨터에 연결된 타이핑머신들이
유선으로 각자의 작업실 까지 연결되어 있었다.
사용자는 자신의 코드를 타이핑하여 중앙의 컴퓨터에 보내면
컴퓨터는 연산의 결과를 방에 있는 타이핑머신에 전송한다.

여기서 유선의 끝단에 연결된 타이핑 머신이 오늘날 터미널이 되고 tty가 된다.

당시 컴퓨터는 느리고 거대하고 비쌌다.
그래서 동시 사용자가 많았기 때문에 매번 순번과 시간을 정해 쓰는 것은 굉장히 불편한 일이었다.
이를 해결하기 위해서 컴퓨터의 자원을 효율적으로 분배하여
여러가지 요청을 최대한 빠르게 수행하는 방식이 등장했는데 이것을 OS라고 불렀다.
그리고 요청에 맞춰 처리 환경을 바꾸는 방식을 컨텍스트 스위치라고 불렀다.

다시 돌아와서

AI 서빙을 하다보면 동기, 비동기, 병렬 처리를 모두 듣게 된다.
나도 처음엔 굉장히 헷갈렸는데 비동기와 병렬을 구분하는 가장 큰 키워드는 지연이라고 할 수 있다.
그래서 많은 비동기 처리 예제에서 sleep을 꼭 등장시키는 것을 볼 수 있다.

지연은 API서버를 구축했을 때 요청과 응답 사이에서 발생하는 것을 포함한다.
따라서 DB와 소통하거나 다른 API서버와 통신하는 io-job의 경우 비동기 처리를 적용했을 때 효과적이다.
반면 하나의 서버 안에서 동기처리로만 진행되는 함수들을 비동기로 욱여넣으면
즉 cpu-job을 비동기 처리하면 오히려 성능에 저하를 가져올 수 있다.

프로그램 > 프로세스 > 쓰레드 순으로 종속관계에 있다고 보면
병렬처리는 멀티프로그램, 멀티프로세스이고
비동기처리는 멀티쓰레드의 일종이라고 할 수 있다.

asyncio 정말 필요할까?

이쯤 내용을 정리하고 나니 비동기처리가 어디에 왜 필요한지 이해가 됐다.
Fastapi의 경우 method를 작성할때 async def를 이용할 수 있는 경우가 어떤 케이스 인지 여기에서 설명하고 있다.
async def를 이용해 작성된 함수는 네이티브 coroutine에 등록되는데 Fastapi는 coroutine을 이용해서
비동기 최적화를 자동으로 해주는 라이브러리임을 알 수 있다.

앞서 말했듯 처리 도중 지연이 발생하면 처리의 우선권을 양보하는 개념이기 때문에
파이썬의 yield와 개념적으로 같은 동작이라고 할 수 있다.
yield는 제너레이터를 만드는 방식으로 이 방식과 구분짓기 위해 네이티브라는 말을 앞에 붙인다.

쓰레드를 변경하며 함수를 수행하기 위해서는 컨텍스트 스위칭이 발생하여
쓰레드의 수에 비례한 추가 시간이 걸리게 된다.
하지만 coroutine의 경우 하나의 쓰레드 안에서 생성되어 컨텍스트 스위치가 발생하지 않기 때문에
등록된 함수에 비례한 추가시간이 걸리지 않는 장점이 있다.

하지만 역시 coroutine에 함수를 등록하고 처리의 우선권을 양보하며 순서를 결정해야 하기 때문에
단순 연산과 같은 경우 동기처리에 비해 성능이 저하 될 우려가 있고
io-job의 경우에도 connection이 await을 지원하지 않는다면 그냥 동기처리로 작성하는 편이 좋다.
(await은 반드시 coroutine 안에서 정의된다.)

이 부분은 정말 멋진 세션을 강의 해주신 김대희님의 파이콘영상을 꼭 참고해 보기 바란다.

 
반응형

댓글