-
[Python] FAST API에서 async def, def의 차이개발 공부의 시작/python 2024. 1. 18. 15:12728x90
FastAPI의 "def"와 "async def"는 결국 "어느 쪽을 쓰면 좋을까?"
개요
FastAPI를 시작하면서 async/await 구문이 있고, 파이썬에도 async/await 구문이 있구나~ 하고 처음 그 존재를 알게 되었다.
하지만 FastAPI의 샘플 코드나 인터넷에 공개된 코드를 보면 async def와 def를 어떻게 사용하는지 잘 모르겠고, 결국 '어느 쪽을 쓰면 좋을까'라는 생각이 들었다. 라는 생각이 들어 async/await, 동기/비동기를 살펴보면서 결론을 도출해보기로 했다.
정리
Path operation 함수의 경우 async def가 아닌 def로 기본적으로 구현한다.
def만으로도 외부 스레드 풀에서 비동기 처리되도록 프레임워크로 구현되어 있다고 한다.
async def를 사용하는 것이 좋은 경우는 다음과 같은 2가지 경우이다.
- async/await를 지원하는 라이브러리를 이용하고 싶을 때,
- 'I/O 바운드'가 발생하지 않는 경우,
아래 코드에서 @app.get('/')로 시작하는 함수를 Path operation 함수라고 부른다.
@app.get('/') def results(): results = some_library() return results동기/비동기(병렬처리)란 무엇인가?
우선, 애초에 동기/비동기(병렬처리)를 복습하지 않으면 async/await 구문을 제대로 이해할 수 없을 것 같아서 동기/비동기를 복습한다.
동기
'태스크1'과 '데이터 조회'를 동기화 처리하는 애플리케이션이 있다고 가정해보자.
이 애플리케이션이 사용자 A로부터 요청을 받은 경우를 예로 들어 설명한다.
프로그램에 쓰여진 순서대로 태스크가 처리되기 때문에 데이터 조회가 끝날 때까지 태스크1의 처리가 중단되어 사용자 입장에서는 화면이 멈춰있는 것처럼 보인다.

비동기 처리(병렬 처리)란?
한 작업을 실행하는 동안 실행 중인 작업을 멈추지 않고 다른 새로운 작업을 실행할 수 있는 프로세스
Python의 async/await는 병렬 처리가 싱글 스레드로 동작하며, 멀티 스레드가 아님에 유의해야 한다.
아까 전의 작업들을 비동기 처리하는 애플리케이션에 사용자 A가 '태스크 1과 데이터 조회'를 모두 처리해달라는 요청과 사용자 B가 '태스크 2'만 처리해달라는 요청이 있는 경우로 설명한다.
비동기 처리는 한 태스크를 실행하는 도중 그 처리를 멈추지 않고 다른 처리를 실행하기 때문에 아래 그림과 같이 사용자 A의 요청을 처리하는 도중 사용자 B의 요청이 있어도 사용자 B는 사용자 A의 처리 완료를 기다리지 않고 결과를 받을 수 있다.

비동기 처리가 필요한 이유
I/O 작업의 대기 시간 동안 작업을 병렬로 실행하여 전체 처리 시간을 줄이고 클라이언트에 대한 응답을 개선한다.
특히 FastAPI와 같은 웹 애플리케이션 프레임워크에서는 'I/O 바운드'가 많기 때문에 비동기 처리가 성능 상 중요해진다.
실행시간의 대부분을 CPU가 아닌 데이터 입출력(Input/Output) 대기시간이 차지하는 처리를 I/O 바운드라고 한다.
대표적인 I/O 바운드 예시
디스크 작동
- 데이터를 파일에 저장하거나 파일에서 데이터를 불러오는 작업
- 데이터베이스에서 CRUD 처리한 경우
네트워크
- 네트워크를 통해 외부 웹 API에 요청하고 응답을 받는 과정
등등..
async / await란?
비동기 처리(병렬 처리)를 지원하는 구문
- 단, 동시처리를 지원하는 것이지, 병렬 처리를 지원하는 것은 아니다.
- async는 asynchronous의 줄임말로 비동기식이라는 뜻이다.
async : 비동기 대응 메서드를 정의
await : 비동기 대응 메서드를 호출할 때 선언
async/await 구문을 사용하기 위해서는 동시 처리를 위한 asyncio라는 라이브러리를 사용한다.
구현 예시
다음은 동기 처리 시 코드와 비동기 처리 시 코드를 나타낸다.
동기화 처리의 경우
태스크 1이 4초, 태스크 2가 2초, 총 6초의 실행 시간.
import time def say_after(delay, what): print(f"started say_after {delay} {what}") time.sleep(delay) print(what) def main(): print(f"started at {time.strftime('%X')}") say_after(2, "task1") say_after(4, "task2") print(f"finished at {time.strftime('%X')}") main() >>> started at 14:49:02 started say_after 2 task1 task1 started say_after 4 task2 task2 finished at 14:49:08비동기 처리의 경우(async/await)
태스크 1이 4초, 태스크 2가 2초, 병렬로 처리되기 때문에 총 4초의 실행 시간.
import asyncio import time async def say_after(delay, what): print(f"started say_after {delay} {what}") await asyncio.sleep(delay) print(what) async def main(): task1 = asyncio.create_task( say_after(2, 'task1')) task2 = asyncio.create_task( say_after(4, 'task2')) print(f"started at {time.strftime('%X')}") await task1 await task2 print(f"finished at {time.strftime('%X')}") asyncio.run(main()) >>> started at 14:50:26 started say_after 2 task1 started say_after 4 task2 task1 task2 finished at 14:50:30결국 FastAPI에서 async def는 사용하는 것이 좋을까?
결론
Path operation 함수의 경우 async를 붙이지 않고 일반 def로 구현한다.
'I/O 바운드가 발생하지 않는 경우' 또는 'async/await를 지원하는 라이브러리를 사용하는 경우'에는 async def를 사용해야 한다.
근거
💡 데이터베이스, API, 파일 시스템 등과 통신하고 await 사용을 지원하지 않는 서드파티 라이브러리(몇 데이터베이스 라이브러리에 해당)를 사용하는 경우, 다음과 같이 def를 사용하여 평소처럼 path operation 함수를 선언하면 된다.
라고 적혀있으며, async를 붙이지 않은 일반 def를 권장하고 있다.
또한,
💡 async def 대신 일반 def로 선언하면 (서버를 차단하기 때문에) 직접 호출하는 대신 외부 스레드 풀(await)에서 실행된다.
라고 적혀 있고, FastAPI의 프레임워크로서 await를 하기 때문에, async def를 사용하지 않아도 된다고 적혀 있다.
그렇다면 어떤 경우에 사용할 수 있을까?
path operation 함수가 블로킹 I/O를 수행하지 않는다면, async def를 사용하는 것이 좋다.
그래서 대기시간이 발생하지 않는 메서드는 async def를 붙이는 것이 좋다고 한다.
728x90'개발 공부의 시작 > python' 카테고리의 다른 글
파이썬 비동기 프로그래밍 - (1) 코루틴 (0) 2024.01.18 Iterators와 Generators (1) 2024.01.11 [Python] Del의 시간복잡도 (1) 2023.12.03