computer engineering

Python - 비동기 프로그래밍 실전 적용

제이훈 : 세상 모든 지식의 탐구자 2025. 5. 20. 16:01

Python 비동기 라이브러리

  • asyncio: 표준 비동기 이벤트 루프. async/await 기반
  • aiohttp: 비동기 HTTP 요청 처리. requests 비동기 버전
  • aiofiles: 비동기 파일 I/O. open() 대신 aiofiles.open()
  • aiomultiprocess: asyncio + multiprocessing. CPU 바운드용
  • trio / curio: asyncio 대체. 현대적 구조(nursery)
  • httpx.AsyncClient: aiohttp 대체 가능. requests 스타일 + async

크롤링 Task 구조 요약

  1. 콘텐츠 단위로 유저 데이터 수집
  2. 작품별 회차 수 다름 (예: 1~100화 vs 1~57화)
  3. 회차별 데이터 수량 다름 (예: 1화=1000개, 2화=500개)
  4. HTTP 요청으로 response 수신
  5. 데이터 존재 여부 판단 → 파싱
  6. 작품 단위로 DataFrame 변환
  7. 전체 콘텐츠에 대해 반복

비동기 처리 관련 오해

  • ❌ await는 기다리는 것 → ✅ 제어권 양보
  • ❌ async def만 써도 성능 좋아짐 → ✅ 병렬 예약 없으면 의미 없음
  • ❌ await 많으면 느려짐 → ✅ await 많아야 효과 큼 (I/O에 한함)
  • ❌ 이벤트 루프는 멀티스레드 → ✅ 단일 스레드 기반

비동기 처리 주의사항

  • 이벤트 루프는 큐/콜백 기반. 우선순위 없음
  • gather vs create_task → 전체 대기 vs 개별 관리
  • CPU 작업은 asyncio X → ProcessPoolExecutor 사용
  • 예외는 try/except 또는 return_exceptions=True로 처리

async def 함수의 실행 구조

  • await 전까지는 실행. await에서 중단
  • await는 제어권을 이벤트 루프에 넘김
  • 이후 완료되면 resume

Python 비동기 병렬 실행 방법

1. asyncio.gather()

results = await asyncio.gather(task1(), task2(), task3())
  • 모든 작업을 동시에 실행
  • 모두 끝날 때까지 대기

2. asyncio.create_task()


task1 = asyncio.create_task(my_coro())
await task1
  • 비동기 작업을 백그라운드로 실행
  • Task 객체로 상태 추적 가능

3. asyncio.as_completed()


for coro in asyncio.as_completed(tasks):
    result = await coro
  • 끝나는 순서대로 처리
  • 빠른 응답 우선 처리 가능

비교 요약

  • gather: 결과 순서 중요할 때
  • create_task: 추적/취소 필요할 때
  • as_completed: 빠른 결과 우선 처리

코루틴 중첩 패턴


async def inner():
    await asyncio.sleep(1)
    return 42

async def outer():
    result = await inner()
  • 코루틴 안에서 다른 코루틴 호출 가능
  • 반드시 await 또는 create_task로 실행

비동기 크롤링 실전 이슈 및 대응

1) Too many open files

  • Linux 기본: 1024
  • 확장 방법: ulimit -n 4096

2) 과도한 병렬 요청 제어

  • 타이틀 단위: asyncio.Semaphore(10)
  • 페이지 단위: asyncio.Semaphore(5)
  • TCP 연결 제한: aiohttp.TCPConnector( limit=50, limit_per_host=10, force_close=True)

3) 세션 닫힘 오류

  • ClientSession은 한 번 생성 → 모든 요청에 공유

4) 재시도 로직


for attempt in range(3):
    try:
        ...
    except:
        await asyncio.sleep(1 + attempt)

5) Server disconnected 대응

  • 과도한 연결 → 서버 차단
  • 요청 지연 → timeout
  • 커넥션 재사용 문제 → reset 발생
  • 세션이 커넥션 과다 유지 → crash 발생
  • 서버 rate limit → 요청 간 간격 조절 필요