파이썬 비동기 프로그래밍, async와 await 기초부터
지난 글에서 타입 힌팅으로 코드를 읽기 좋게 만드는 이야기를 했었죠. 그러면서 "코드 자체가 빨라지는 차원의 이야기"는 다음에 하자고 슬쩍 미뤘는데, 오늘이 그 다음이에요.
파이썬으로 뭔가 만들다 보면 "이거 왜 이렇게 느려?" 싶은 순간이 와요. 근데 자세히 들여다보면 컴퓨터가 무거운 계산을 하느라 느린 게 아니라, 그냥 뭔가를 기다리고만 있어서 느린 경우가 대부분이거든요.
인터넷에서 데이터 받아오는 거, 파일 다운로드, API 호출 같은 거요. 이런 작업은 내 컴퓨터가 일하는 게 아니라 상대편 서버가 응답해줄 때까지 가만히 서 있는 거예요. 그 멍하니 기다리는 시간에 다른 일을 시킬 수 있다면 어떨까요? 그게 바로 오늘 이야기할 비동기 프로그래밍이에요.
동기와 비동기, 카페로 비유하면
카페 점원이 한 명 있다고 해볼게요.
동기 방식 점원은 이래요. 손님 A가 라떼를 주문하면, 우유를 데우고, 그 3분 동안 손님 A 앞에 가만히 서서 우유가 데워지길 기다려요. 우유가 다 데워지면 음료를 완성해서 건네주고, 그제서야 손님 B의 주문을 받죠.
비동기 방식 점원은 다르게 움직여요. 손님 A의 우유를 데우기 시작하고, 그 사이에 손님 B 주문을 받고, 손님 C 주문도 받아요. 우유 데우는 알람이 울리면 그때 A의 음료를 마무리하죠.
점원은 똑같이 한 명이에요. 근데 처리한 손님 수는 완전히 달라요. 비결은 단순해요. "기다리는 시간"을 일하는 시간으로 바꿨거든요.
일단 동기 코드부터 보고 갈게요
비교를 해야 감이 오니까, 먼저 평범한 동기 코드부터 볼게요.
import time # 시간 측정용
def 데이터_받기(번호):
print(f"{번호}번 요청 시작")
time.sleep(2) # 인터넷에서 받아오는 척 2초 대기
print(f"{번호}번 받기 완료")
return f"데이터{번호}"
시작 = time.time()
데이터_받기(1)
데이터_받기(2)
데이터_받기(3)
print(f"총 걸린 시간: {time.time() - 시작:.1f}초")
실행 결과는 이렇게 나와요.
1번 요청 시작
1번 받기 완료
2번 요청 시작
2번 받기 완료
3번 요청 시작
3번 받기 완료
총 걸린 시간: 6.0초
2초 × 3번 = 6초. 한 번에 하나씩 차례로 처리하니까요.
같은 일을 2초로, 파이썬 asyncio
이제 비동기로 똑같이 짜볼게요.
import asyncio # 파이썬 비동기 표준 라이브러리
import time
async def 데이터_받기(번호): # async 붙이면 비동기 함수
print(f"{번호}번 요청 시작")
await asyncio.sleep(2) # 양보하면서 2초 기다림
print(f"{번호}번 받기 완료")
return f"데이터{번호}"
async def 메인():
시작 = time.time()
await asyncio.gather( # 세 작업을 동시에 돌리기
데이터_받기(1),
데이터_받기(2),
데이터_받기(3),
)
print(f"총 걸린 시간: {time.time() - 시작:.1f}초")
asyncio.run(메인()) # 비동기 세계의 입구
결과는요.
1번 요청 시작
2번 요청 시작
3번 요청 시작
1번 받기 완료
2번 받기 완료
3번 받기 완료
총 걸린 시간: 2.0초
6초가 2초로 줄었어요. 세 작업이 동시에 "기다리기"를 했거든요.
python async await, 문법 정리
지금까지 나온 키워드만 정리하면 이래요.
async def: 비동기 함수를 만든다는 선언await: 이 작업이 끝날 때까지 양보하면서 기다린다asyncio.run(코루틴): 비동기 세계로 들어가는 입구. 보통 프로그램에 딱 한 번asyncio.gather(...): 여러 작업을 동시에 돌리고 다 끝날 때까지 기다린다
규칙 하나만 기억하세요. await를 쓰려면 그 함수도 async def여야 해요. 일반 함수 안에서 await 쓰면 바로 에러납니다.
주의할 점이 좀 있어요
처음 비동기를 만지면 다들 비슷한 데서 막혀요. 저도 그랬거든요.
첫 번째, async 함수를 그냥 호출하면 실행이 안 돼요.
async def 인사():
print("안녕")
인사() # 이게 실행이 안 됩니다
# <coroutine object 인사 at 0x...>
# RuntimeWarning: coroutine '인사' was never awaited
async def로 만든 함수는 호출만 하면 "코루틴 객체"라는 게 반환돼요. 일종의 "실행 예약권" 같은 거예요. 실제로 돌리려면 asyncio.run()이나 await이 필요하거든요. 이게 일반 함수랑 가장 큰 차이예요.
두 번째, time.sleep() 쓰면 비동기 효과가 사라져요.
async def 잘못된_예():
time.sleep(2) # 파이썬 전체가 멈춤
time.sleep()은 파이썬 자체를 통째로 멈춰버려요. 그러면 다른 코루틴도 같이 멈추니까 동시 처리가 안 되죠. 비동기 함수 안에서는 꼭 asyncio.sleep()을 써야 해요. "양보할 줄 아는 기다리기"라고 생각하시면 돼요.
세 번째, 모든 게 빨라지는 건 아니에요.
비동기는 "기다리는 시간"이 있을 때만 효과가 있어요. 네트워크 요청, 파일 입출력, DB 쿼리처럼 외부를 기다리는 작업(I/O 바운드)엔 끝내주게 잘 먹혀요. 근데 숫자 1억 번 더하는 것처럼 CPU가 진짜로 일하는 작업(CPU 바운드)은 비동기로 해도 1초도 안 빨라져요. 양보할 "기다리는 시간"이 없으니까요.
직접 해보세요
순차 실행과 병렬 실행을 같은 코드로 비교해보는 게 가장 빨리 와닿아요.
import asyncio
import time
async def 작업(이름, 시간):
print(f"{이름} 시작")
await asyncio.sleep(시간)
print(f"{이름} 끝")
async def 순차():
시작 = time.time()
await 작업("A", 2) # 끝나길 기다리고
await 작업("B", 2) # 또 끝나길 기다리고
await 작업("C", 2) # 또 기다림
print(f"순차: {time.time() - 시작:.1f}초")
async def 병렬():
시작 = time.time()
await asyncio.gather( # 셋 다 동시에
작업("A", 2),
작업("B", 2),
작업("C", 2),
)
print(f"병렬: {time.time() - 시작:.1f}초")
asyncio.run(순차()) # 6초
asyncio.run(병렬()) # 2초
직접 돌려보시고, 숫자를 바꿔서 4개, 5개로 늘리면 어떻게 되는지도 실험해보세요.
비동기는 결국 "기다리는 시간"을 활용하는 기술이에요. 인터넷에서 데이터 받아오거나 파일 읽을 때처럼 컴퓨터가 멍하니 있는 순간을 다른 일로 채우는 거죠. 근데 아까 말씀드린 것처럼 계산 자체가 무거운 작업은 비동기로 빨라지지 않잖아요. 그럴 땐 또 다른 방식의 동시성이 필요해요. 다음에는 비동기와는 결이 좀 다른 멀티스레딩 이야기를 해볼 거예요. 스레드를 여러 개 띄워서 진짜로 동시에 일을 시키는 방식이요.
제 글이 도움이 되셨다면 댓글 & 공감 부탁드려요 😀
'Application > Python' 카테고리의 다른 글
| [Python] 파이썬 requests 라이브러리로 API 호출하기 (0) | 2026.06.01 |
|---|---|
| [Python] 파이썬 멀티스레딩으로 작업 속도 확 줄이기 (0) | 2026.05.29 |
| [Python] 파이썬 타입 힌팅, 모르고 쓰면 손해예요 (0) | 2026.05.26 |
| [Python] 이터레이터와 제너레이터 이해하기 (파이썬 제너레이터 입문) (0) | 2026.05.21 |
| [Python] 파이썬 성적 관리 프로그램 만들기 — 중급 종합 실습 (0) | 2026.05.20 |