📌 1. 비동기 프로그래밍이란?
대부분의 백엔드 개발 환경에서는 웹 요청, 파일 입출력, DB 쿼리 등 입출력(I/O) 대기 시간이 긴 작업들이 많습니다. 이런 상황에서 쓰레드를 무작정 늘리는 방식은 비용도 크고 한계가 있습니다.
Python의 비동기 프로그래밍은 async/await 문법을 기반으로, 동시성(concurrency) 을 효율적으로 처리해줍니다.
✅ 동기 vs 비동기
방식 | 설명 | 예시 |
동기 | 순차적으로 처리 | DB 조회 → 응답 대기 → 다음 요청 |
비동기 | 대기 시간 중 다른 작업 수행 | DB 요청 중 파일 읽기 진행 |
✅ 기본 개념 정리
용어 | 설명 |
Coroutine | async def로 정의된 함수. 호출 시 실행되지 않고, 코루틴 객체를 반환함 |
await | 코루틴이나 awaitable 객체의 실행을 일시 중단하고, 다른 작업 수행 기회를 줌 |
이벤트 루프 (Event Loop) | 대기 중인 비동기 작업들을 순차적으로 실행해주는 중심 컨트롤러 |
Task | 코루틴을 이벤트 루프에 등록하여 동시 실행이 가능하도록 만든 객체 |
Future | 비동기 작업의 결과나 예외를 담는 그릇 |
🧭 2. 언제 비동기를 써야 할까?
실무에서는 다음과 같은 상황에서 비동기가 큰 효과를 발휘합니다:
- 외부 API 연동: 여러 외부 API를 동시에 호출해야 할 때
- DB + 파일 저장: DB 저장과 동시에 로그 파일도 써야 할 때
- 웹 크롤링: 수백 개 URL을 빠르게 요청해야 할 때
단, CPU 연산이 많은 작업(예: 이미지 처리, 대용량 통계 분석 등)은 asyncio 보단 멀티프로세싱이 적합합니다.
🧩 3. 전체 흐름 예제
아래 코드를 기준으로 설명드릴게요:
import asyncio
async def say_after(delay, what):
print(f"start: {what}")
await asyncio.sleep(delay)
print(f"done: {what}")
async def main():
task1 = asyncio.create_task(say_after(2, 'hello'))
task2 = asyncio.create_task(say_after(1, 'world'))
print("Tasks created")
await task1
await task2
asyncio.run(main())
🔍 단계별 흐름 설명
1️⃣ asyncio.run(main()) 호출
- 이벤트 루프가 시작됩니다.
- main()은 코루틴 함수 → 실행되지 않고 코루틴 객체가 반환됨.
- 이벤트 루프는 해당 코루틴을 Task로 감싸 실행합니다.
✅ 내부적으로는 loop.create_task(main()) → loop.run_until_complete(task) 형태로 실행
2️⃣ main() 실행
task1 = asyncio.create_task(say_after(2, 'hello'))
task2 = asyncio.create_task(say_after(1, 'world'))
- say_after()도 async def → 실행 X → 코루틴 객체 반환
- create_task()는 해당 코루틴을 Task로 등록 → 즉시 실행 가능 상태로 만듬
- 이 시점에 두 작업이 이벤트 루프 큐에 등록됨 (둘 다 동시 실행 가능)
이벤트 루프 큐:
Task1: say_after(2, "hello")
Task2: say_after(1, "world")
3️⃣ await task1
- main()은 task1이 끝나길 기다림 → 여기서 중단점 발생
- task1은 say_after(2, 'hello')의 실행을 시작함
- print("start: hello")
- await asyncio.sleep(2)에서 sleep task 등록 후 일시 중단
- 이때 루프는 다른 task(task2)로 넘어감
4️⃣ task2 실행
- say_after(1, 'world') 실행됨
- print("start: world")
- await asyncio.sleep(1)에서 1초 타이머 등록 후 일시 중단
→ 이제 루프는 1초 대기 후 다시 wake up.
5️⃣ 1초 후: task2 재개
- await asyncio.sleep(1) 완료 → print("done: world")
- task2 종료됨
6️⃣ 2초 후: task1 재개
- await asyncio.sleep(2) 완료 → print("done: hello")
- task1 종료됨
7️⃣ task1, task2 모두 완료 → main() 종료 → asyncio.run() 종료
📊 실행 결과 (타이밍 기준)
start: hello
start: world
Tasks created
done: world ← 1초 후
done: hello ← 2초 후
🔁 전체 처리 흐름 정리
1. asyncio.run(main()) -> 이벤트 루프 시작
2. main() 진입 → task1, task2 생성 및 등록
3. task1 await → 실행 잠시 멈춤 → 루프가 task2 실행
4. task2 await → 멈춤 → 루프가 1초 대기
5. 1초 후 task2 resume → 종료
6. 2초 후 task1 resume → 종료
7. main resume → 종료
8. 루프 종료
🧠 실무 응용 팁
- create_task()를 쓰면 작업을 병렬 실행할 수 있음.
- await는 현재 task를 중단시키고, 루프에게 다른 작업 기회를 줌
- asyncio.sleep()도 I/O 작업의 예시임 (DB, API, 파일 등으로 대체 가능)
- 동시 처리할 작업이 많을 때는 gather()로 묶어서 처리하면 좋음
⚠️ 5. 비동기 사용 시 주의할 점
❌ 블로킹 코드와 혼용 금지
아래처럼 time.sleep() 같은 동기 함수를 사용하면 비동기 코드가 무력화됩니다.
# 잘못된 예시
def slow():
time.sleep(3)
# 올바른 방식
async def slow():
await asyncio.sleep(3)
💥 예외처리도 신경 써야
gather()를 쓸 땐 return_exceptions=True를 넣어 예외를 개별로 다루세요:
results = await asyncio.gather(fetch(url1), fetch(url2), return_exceptions=True)
이벤트 루프 중복 실행 방지
- asyncio.run()은 루프를 하나만 실행 가능
- FastAPI/Quart 등 프레임워크 내 루프와 충돌할 수 있음 → 루프 중복 확인 필수
🧰 7. 추천 비동기 라이브러리
라이브러리 | 용도 | 특징 |
aiohttp | HTTP 요청 | FastAPI보다 가볍고 빠름 |
aiomysql | MySQL 비동기 클라이언트 | 커넥션 풀 관리 지원 |
aioredis | Redis 비동기 클라이언트 | pub/sub, stream 지원 |
anyio | 다양한 백엔드 호환 | Trio, asyncio 모두 지원 |
🧾 마무리하며
Python의 asyncio는 초반 진입장벽이 다소 있지만, 실무에서 병렬 처리 성능을 극대화하고 싶다면 반드시 익혀야 할 기술입니다. 특히 다음과 같은 경우에 비동기는 큰 도움이 됩니다:
- 다수의 API 호출을 병렬로 처리해야 할 때
- 파일, DB, 외부 서비스 요청이 복합적으로 얽힌 로직에서
- 웹 서버에서 처리량(Throughput)을 높이고 싶을 때
동기 방식으로 처리하면 막히는 부분에서 시스템 전체가 느려질 수 있지만, 비동기는 그런 병목 구간을 유연하게 넘길 수 있는 무기가 되어줍니다.
실무에서는 비동기와 블로킹 코드의 혼용, 세션 관리 누락, 예외 처리 미흡 등으로 예상치 못한 문제가 종종 발생하므로, 작동 원리를 정확히 이해하고 설계에 반영하는 것이 중요합니다.
'Python' 카테고리의 다른 글
UV(Ultraviolet)로 빠르게 효율적으로 파이썬 패키지 관리하기 (0) | 2025.05.14 |
---|---|
[Python] @contextmanager 데코레이터 에 대해서 (0) | 2025.04.01 |
[FastAPI] Server Send Event(SSE) 구현 (0) | 2025.02.10 |
[Python] Python Portable( 무설치 파이썬 ) 설치 방법 (0) | 2025.01.14 |
plot 한글 깨지는 현상 해결 ( Python, Pandas, matplotlib ) (0) | 2023.11.24 |