파이썬(Python) Asyncio 비동기 크롤링으로 데이터 수집 속도 10배 높이기

[Troubleshooting] 파이썬(Python) Asyncio 비동기 크롤링으로 데이터 수집 속도 10배 높이기

"수천 개의 웹 페이지에서 상품 가격 데이터를 수집하는 스크립트를 작성했는데, 일반적인 requests 라이브러리를 사용해 순차적으로 긁어오다 보니 전체 작업 완료까지 몇 시간이 걸리는 병목 현상이 발생했습니다."

데이터 분석가나 자동화 엔지니어가 대규모 웹 스크래핑 작업을 수행할 때 마주하는 전형적인 속도 저하 문제입니다. 동기식(Synchronous) 코드는 서버의 응답이 올 때까지 프로그램이 완벽하게 멈춰 서서(Blocking) 대기하기 때문에 CPU와 네트워크 자원이 심각하게 낭비됩니다.

단순히 멀티스레딩을 적용하는 것보다 훨씬 가볍고 효율적인 Asyncio 및 aiohttp 기반의 비동기 I/O 아키텍처를 도입하면, 수많은 네트워크 요청을 동시에 처리하여 수집 시간을 극단적으로 단축할 수 있습니다. 본 가이드에서는 실무에서 즉시 활용할 수 있는 비동기 크롤링 구현 노하우를 전달합니다.

📌 이 글의 핵심 포인트

  • 동기 vs 비동기: 네트워크 대기 시간(I/O Bound) 병목 현상의 개념과 비동기 처리의 필요성
  • Aiohttp 라이브러리: 논블로킹(Non-blocking) 방식으로 HTTP 요청을 병렬 처리하는 구조
  • 실무 트러블슈팅: 타겟 서버의 과부하 방지를 위한 세마포어(Semaphore) 동시성 제어

1단계: 왜 기존의 requests 방식은 느린가?

기본적인 requests.get(url) 함수는 서버가 HTML 응답을 돌려줄 때까지 다음 줄로 넘어가지 못하고 코드가 대기합니다. 만약 평균 응답 속도가 0.5초인 웹사이트 1,000곳을 순차적으로 방문한다면, 네트워크 통신 대기 시간만 최소 500초가 소요됩니다.

하지만 실제 네트워크 통신은 대부분 상대방의 응답을 '기다리는' 시간입니다. 이 대기 시간 동안 다른 웹페이지를 동시에 찌를 수 있다면 전체 소요 시간은 단 몇 초로 줄어들 수 있습니다. 이것이 바로 파이썬의 asyncio가 지향하는 비동기 프로그래밍의 핵심 철학입니다.

2단계: 무결점 비동기 크롤링 코드 구현

asyncioaiohttp를 결합하면 이벤트 루프(Event Loop)를 기반으로 수많은 코루틴(Coroutine)을 동시에 실행할 수 있습니다. 다만 무차별적으로 수천 개의 요청을 한꺼번에 보내면 타겟 서버의 방화벽(WAF)에 의해 IP가 차단되므로, asyncio.Semaphore를 이용해 동시 접속 수를 적절히 제어해야 합니다.

import asyncio
import aiohttp

urls = [
    "https://example.com/page/1",
    "https://example.com/page/2",
    "https://example.com/page/3",
]

# 동시 접속을 최대 5개로 제한하는 세마포어
semaphore = asyncio.Semaphore(5)

async def fetch(session, url):
    async with semaphore:
        async with session.get(url) as response:
            html = await response.text()
            print(f"수집 완료: {url} (상태 코드: {response.status})")
            return html

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        await asyncio.gather(*tasks)

if __name__ == "__main__":
    asyncio.run(main())

🙋‍♂️ 자주 묻는 질문 (FAQ)

Q. 비동기 크롤링 중 403 Forbidden 에러가 자주 발생합니다.
A. 너무 빠른 속도로 수많은 요청을 보내면 타겟 서버가 봇(Bot)으로 인식해 차단하는 것입니다. 세마포어 값을 낮추고 각 요청 사이에 랜덤 딜레이를 추가해야 합니다.

Q. 기존에 작성한 BeautifulSoup 파싱 코드를 그대로 쓸 수 있나요?
A. 네, response.text()로 가져온 HTML 문자열을 기존과 동일하게 BeautifulSoup(html, 'html.parser')에 넣어 파싱하면 됩니다.

💡 핵심 정리 및 마무리

대규모 데이터를 수집하는 환경에서 동기식 스크래핑 방식은 시간 낭비일 뿐만 아니라 인프라 자원의 비효율을 초래합니다. Asyncio 기반의 비동기 I/O 아키텍처를 마스터하여 데이터 수집 파이프라인의 성능을 획기적으로 개선해 보시기 바랍니다.

댓글

이 블로그의 인기 게시물

Zapier & Make.com 자동화의 덫: 무한 루프(Infinite Loop) 에러 완벽 방어 아키텍처

엑셀 보고서의 종말: 구글 스프레드시트와 루커 스튜디오(Looker Studio)로 실시간 대시보드 구축하기

Docker OOMKilled (Exit Code 137) 에러의 진실: 컨테이너 메모 누수 방어 및 리소스 최적화 아키텍처