[Python] 비동기(async) 프로그래밍 2
목차
들어가며
1편에서 비동기 프로그래밍의 개념을 학습했다. 이번 편에서는 파이썬에서 비동기를 구현하는 핵심 키워드인 async와 await에 대해 깊이 있게 알아보자. 이 두 키워드가 어떻게 작동하는지 이해하면 비동기 프로그래밍의 절반은 정복한 것이다.
async def: 코루틴 함수 정의하기
일반 함수 vs 코루틴 함수
먼저 일반 함수와 코루틴 함수의 차이점을 살펴보자.
# 일반 함수
def 일반함수():
return "일반 함수 결과"
# 코루틴 함수
async def 코루틴함수():
return "코루틴 함수 결과"
# 함수 호출 비교
print(일반함수()) # 출력: 일반 함수 결과
print(코루틴함수()) # 출력: <coroutine object 코루틴함수 at 0x...>
일반 함수는 호출하면 즉시 실행되어 결과를 반환한다. 하지만 async def로 정의된 코루틴 함수는 호출해도 실행되지 않고 코루틴 객체를 반환한다.
코루틴 객체란 무엇인가?
코루틴 객체는 "실행 가능한 상태의 함수"를 담고 있는 컨테이너다. 마치 레시피가 적힌 요리 카드와 같다. 카드만 가지고 있다고 요리가 완성되는 것이 아니라, 실제로 요리를 해야 결과가 나온다.
import asyncio
async def 데이터베이스_조회(user_id):
print(f"사용자 {user_id} 데이터 조회 시작")
await asyncio.sleep(1) # 데이터베이스 조회 시뮬레이션
print(f"사용자 {user_id} 데이터 조회 완료")
return {"user_id": user_id, "name": "홍길동"}
# 코루틴 객체 생성 (아직 실행되지 않음)
코루틴_객체 = 데이터베이스_조회(123)
print(type(코루틴_객체)) # <class 'coroutine'>
# 실제 실행
결과 = asyncio.run(코루틴_객체)
print(결과) # {'user_id': 123, 'name': '홍길동'}
await: 비동기 작업을 기다리고 결과를 받는 마법
await의 기본 동작
await 키워드는 두 가지 핵심 기능을 수행한다:
- 비동기 대기: "이 작업이 완료될 때까지 기다리되, 기다리는 동안 다른 작업을 처리해도 된다"
- 결과 반환: 비동기 작업이 완료되면 그 결과값을 반환한다
즉, await는 코루틴의 실행을 기다리면서 동시에 그 실행 결과를 받아오는 역할을 한다.
import asyncio
import time
async def 데이터베이스_조회(user_id):
print(f"사용자 {user_id} 데이터 조회 시작")
await asyncio.sleep(1) # 데이터베이스 조회 시뮬레이션
print(f"사용자 {user_id} 데이터 조회 완료")
return {"user_id": user_id, "name": "홍길동"} # 결과 반환
async def 점수_계산(user_id):
print(f"사용자 {user_id} 점수 계산 시작")
await asyncio.sleep(0.5)
print(f"사용자 {user_id} 점수 계산 완료")
return {"user_id": user_id, "score": 95}
async def main():
시작시간 = time.time()
# await를 사용해 결과를 받아온다
사용자정보 = await 데이터베이스_조회(123) # 결과를 변수에 저장
사용자점수 = await 점수_계산(123)
# 받아온 결과를 사용
print(f"조회 결과: {사용자정보}")
print(f"점수 결과: {사용자점수}")
총시간 = time.time() - 시작시간
print(f"총 소요 시간: {총시간:.1f}초")
asyncio.run(main())
출력 결과:
사용자 123 데이터 조회 시작
사용자 123 데이터 조회 완료
사용자 123 점수 계산 시작
사용자 123 점수 계산 완료
조회 결과: {'user_id': 123, 'name': '홍길동'}
점수 결과: {'user_id': 123, 'score': 95}
총 소요 시간: 1.5초
보다시피 await는 단순히 기다리기만 하는 것이 아니라, 비동기 함수의 실행 결과를 받아와서 변수에 저장할 수 있게 해준다.
await vs 일반 함수 호출
import time
# 잘못된 예시 - time.sleep 사용
async def 잘못된_비동기함수():
print("시작")
time.sleep(2) # 블로킹 - 다른 작업이 대기해야 함
print("완료")
# 올바른 예시 - asyncio.sleep 사용
async def 올바른_비동기함수():
print("시작")
await asyncio.sleep(2) # 논블로킹 - 다른 작업이 실행될 수 있음
print("완료")
중요한 점은 await 키워드는 awaitable 객체에만 사용할 수 있다는 것이다. time.sleep()은 awaitable하지 않으므로 await를 사용할 수 없다.
await 사용 시 주의사항
1. await는 async 함수 내에서만 사용 가능
# 잘못된 예시
def 일반함수():
result = await 코루틴함수() # SyntaxError!
return result
# 올바른 예시
async def 비동기함수():
result = await 코루틴함수()
return result
2. awaitable 객체에만 await 사용 가능
Awaitable 객체의 종류:
- 코루틴 객체 (async def 함수의 반환값)
- Task 객체
- Future 객체
- __await__ 메서드를 구현한 객체
# awaitable한 것들
await asyncio.sleep(1) # 코루틴
await asyncio.create_task(코루틴함수()) # Task
await some_future # Future
# awaitable하지 않은 것들
await time.sleep(1) # TypeError!
await "문자열" # TypeError!
await 123 # TypeError!
일반적인 실수들
1. await 없이 코루틴 함수 호출
# 잘못된 방법
async def main():
result = 코루틴함수() # await 누락!
print(result) # <coroutine object ...> 출력
# 올바른 방법
async def main():
result = await 코루틴함수()
print(result)
2. 동기 함수에서 비동기 함수 호출 시도
# 잘못된 방법
def 동기함수():
result = await 비동기함수() # SyntaxError!
return result
# 올바른 방법: 함수를 비동기로 변경
async def 비동기함수_변경():
result = await 비동기함수()
return result
마무리
async와 await 키워드는 비동기 프로그래밍의 핵심이다. async def로 코루틴 함수를 정의하고, await로 비동기 작업을 기다리면서 결과를 받아온다. 이 두 키워드를 올바르게 사용하면 애플리케이션의 성능을 크게 향상시킬 수 있다.
다음 편에서는 실제 FastAPI에서 비동기를 어떻게 활용하는지, 그리고 비동기 사용 시 얻을 수 있는 구체적인 성능 향상을 살펴보겠다.
다음 편 예고
3편에서는 다음 내용을 다룰 예정이다:
- FastAPI에서 비동기 엔드포인트 작성하기
- 동기 vs 비동기 성능 비교 실험
- 외부 API 호출 시 비동기의 장점
- 실무에서 자주 사용하는 비동기 패턴들