안녕하세요, 홉스의 안기욱입니다.

오늘은 저희의 백엔드 서버 인프라를 Flask에서 FastAPI로 전환한 후기에 대해서 공유드리려고 합니다. 이 글에서는 Flask에서 FastAPI로 서버를 전환하게 된 이유와 전환을 위한 가이드라인, 그리고 추가적인 FastAPI 사용 팁과 덩달아 사용하게 된 비동기 라이브러리에 대해 소개합니다. 마지막으로 전환에 대한 간단한 제 생각도 같이 공유드릴 예정입니다.

시작하기에 앞서, 이 글은 2021년 10월 2일 PyCon Korea 2021에서 발표한 대본을 토대로 작성하였음을 밝힙니다.

Flask를 FastAPI로 전환하게 된 이유

Flask 기반의 백엔드 프로젝트 작업을 진행하다가, 웹소켓 엔드포인트를 추가해야 하는 요구사항이 생겼습니다. 처음에는 node 기반의 백엔드 서버를 따로 띄우려는 기획을 했었으나, 빠른 제품 개발을 위해 기존에 Python으로 구현해둔 모델 등의 여러 구현체들을 활용하고 싶었습니다. 그렇지만 웹소켓은 필연적으로 통신 대기 시간이 많이 발생하므로, asyncio를 활용해서 비동기로 동작하는 엔드포인트를 만들고 싶었습니다.

Flask는 2021년 5월 21일에 런칭한 2.0.0 버전에서 asyncio를 이용한 엔드포인트 지원을 추가하였지만, 웹소켓 엔드포인트를 만들어야 했던 3월 당시에는 그러한 방법이 없었습니다. 만약 전통적인 방법으로 웹소켓 엔드포인트를 구현할 경우, 통신 대기 시간의 유휴 자원이 낭비되므로 이러한 방법을 선택할 수는 없었습니다. 이에 코드베이스를 유지하면서 asyncio 엔드포인트를 작성할 수 있는 방법을 찾기 시작했습니다.

asyncio 간단한 소개

여기서 잠깐 asyncio에 대한 간단한 설명을 드리도록 하겠습니다.

전통적인 Python 코드는 파일을 읽거나 쓰는 작업, 네트워크 통신 작업 등 I/O 작업을 수행하면 필연적으로 대기 시간이 발생하게 됩니다. 그래서 보통 이러한 작업들을 구현할 때에는 Celery를 쓰거나 이러한 대기 시간을 마냥 기다리다가 다음 작업을 실행하는 블로킹 방식으로 구현을 하게 되는데요.

asyncio는 이벤트 루프를 사용해 이런 작업들을 관리하여 대기 시간을 낭비하지 않고 다른 작업을 실행할 수 있도록 해 줍니다. 이벤트 루프는 asyncio의 가장 중요한 부분이지만 일반적인 사용에서는 자세한 디테일을 몰라도 괜찮습니다. 다음은 공식 문서의 머릿말 내용입니다.

<aside> 📄 이벤트 루프는 모든 asyncio 응용 프로그램의 핵심입니다. 이벤트 루프는 비동기 태스크 및 콜백을 실행하고 네트워크 IO 연산을 수행하며 자식 프로세스를 실행합니다.

응용 프로그램 개발자는 일반적으로 asyncio.run()과 같은 고수준의 asyncio 함수를 사용해야 하며, 루프 객체를 참조하거나 메서드를 호출할 필요가 거의 없습니다. 이 절은 주로 이벤트 루프 동작을 세부적으로 제어해야 하는 저수준 코드, 라이브러리 및 프레임워크의 작성자를 대상으로 합니다.

https://docs.python.org/ko/3/library/asyncio-eventloop.html

</aside>

그리고 이런 asyncio를 활용하는 함수들은 전통적인 함수와 구분하기 위해 async def라는 예약어로 함수를 선언합니다. 또한 이렇게 선언된 함수를 실행할 때에는 명시적으로 await 예약어를 붙여주어야 합니다.

asyncio 예제

다음은 asyncio 공식 문서에 있는 간단한 예제 코드입니다. say_after라는 delaywhat을 입력받으면 delay만큼 기다린 다음 what으로 입력받은 내용을 출력하는 함수를 구현합니다. 참고로 create_task 함수는 asyncio 함수를 지정된 파라미터와 함께 곧바로 실행 가능한 Task 객체로 감싸주는 함수인데요, 자세한 내용이 궁금하시다면 예제에 대한 공식 문서를 참조해주세요.

그리고 delay 파라미터에 각각 1초, 2초를 넣어 두 번 실행하였는데 두 함수의 실행이 완료되는 데 총 2초밖에 걸리지 않았습니다.

예제 코드

import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    task1 = asyncio.create_task(say_after(1, 'hello'))
    task2 = asyncio.create_task(say_after(2, 'world'))
    print(f"started at {time.strftime('%X')}")
    await task1
    await task2
    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

실행 결과