본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 비동기 HTTP 요청 시 requests 대신 aiohttp를 써야 하는 3가지 결정적 이유와 성능 해결 방법

by Papa Martino V 2026. 3. 29.
728x90

동기(Synchronous)와 비동기(Asynchronous) HTTP 통신
동기(Synchronous)와 비동기(Asynchronous) HTTP 통신

 

현대적 파이썬 개발자를 위한 고성능 네트워크 I/O 최적화 가이드: Blocking vs Non-blocking


1. 동기(Synchronous)와 비동기(Asynchronous) HTTP 통신의 본질적 차이

파이썬 개발자들에게 가장 친숙한 라이브러리는 단연 requests일 것입니다. 인간 친화적인 API 설계 덕분에 한두 줄의 코드로 HTTP 요청을 보낼 수 있지만, 치명적인 약점이 있습니다. 바로 Blocking I/O 방식이라는 점입니다. requests.get()을 호출하는 순간, 파이썬 인터프리터는 서버로부터 응답이 올 때까지 아무런 작업도 하지 못하고 멈춰 서게 됩니다. 반면 aiohttpasyncio 라이브러리를 기반으로 하는 Non-blocking I/O 방식을 채택합니다. 이는 요청을 보낸 뒤 응답을 기다리는 동안 다른 작업을 수행하거나, 수천 개의 요청을 동시에 병렬로 처리할 수 있음을 의미합니다.

2. requests와 aiohttp 핵심 비교 분석

대규모 트래픽 처리나 빠른 데이터 수집이 필요한 환경에서 두 라이브러리가 보여주는 성능 차이는 극명합니다.

비교 항목 requests 라이브러리 aiohttp 라이브러리
I/O 모델 Blocking (동기) Non-blocking (비동기)
동시성 처리 Multi-threading 필요 (리소스 낭비) 단일 스레드 Event Loop (고효율)
속도 (1000개 요청 기준) 상대적으로 매우 느림 극도로 빠름
세션 관리 requests.Session() 제공 ClientSession() 필수 (연결 재사용 최적화)
복잡도 직관적이고 매우 낮음 async/await 이해 필요 (중간 수준)
주요 용도 단순 스크립트, 소량의 API 호출 고성능 크롤러, 비동기 API 서버, 마이크로서비스

3. 실무 적용을 위한 7가지 고성능 활용 예제 (Sample Examples)

현업에서 즉시 사용 가능한 수준의 안정적인 비동기 HTTP 패턴들입니다.

Example 1: ClientSession을 활용한 기본 비동기 GET 요청

aiohttp는 세션을 재사용하는 것이 성능의 핵심입니다.


import aiohttp
import asyncio

async def fetch_status(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            print(f"Status: {response.status} for {url}")
            return await response.text()

asyncio.run(fetch_status('https://www.google.com'))
        

Example 2: 다중 URL 동시 요청 (requests 대비 약 10배 이상 해결 방법)

asyncio.gather를 사용하여 수많은 요청을 한꺼번에 병렬로 처리합니다.


import aiohttp
import asyncio
import time

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.json()

async def main():
    urls = [f"https://jsonplaceholder.typicode.com/posts/{i}" for i in range(1, 21)]
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        responses = await asyncio.gather(*tasks)
        print(f"총 {len(responses)}개의 데이터를 수집했습니다.")

start = time.time()
asyncio.run(main())
print(f"소요 시간: {time.time() - start:.2f}초")
        

Example 3: POST 요청 시 JSON 데이터 전송 및 헤더 설정

API 연동 시 필수적인 인증 헤더와 페이로드 전송 방법입니다.


import aiohttp
import asyncio

async def send_data():
    url = 'https://httpbin.org/post'
    payload = {'key': 'value', 'name': 'Gemini'}
    headers = {'Authorization': 'Bearer YOUR_TOKEN'}
    
    async with aiohttp.ClientSession(headers=headers) as session:
        async with session.post(url, json=payload) as resp:
            data = await resp.json()
            print(data)

asyncio.run(send_data())
        

Example 4: Semaphore를 이용한 동시 접속자 수 제한 (Rate Limiting 해결)

너무 많은 요청을 한꺼번에 보내면 서버에서 차단될 수 있습니다. 이를 Semaphore로 제어합니다.


import aiohttp
import asyncio

sem = asyncio.Semaphore(5)  # 동시 요청을 5개로 제한

async def throttled_fetch(session, url):
    async with sem:
        async with session.get(url) as response:
            print(f"Fetching {url}...")
            return await response.release()

async def main():
    async with aiohttp.ClientSession() as session:
        urls = [f"https://www.example.com" for _ in range(50)]
        await asyncio.gather(*(throttled_fetch(session, url) for url in urls))

asyncio.run(main())
        

Example 5: Timeout 설정을 통한 좀비 프로세스 방지

무한정 응답을 기다리지 않도록 타임아웃을 설정하는 실무적인 방법입니다.


import aiohttp
import asyncio

async def fetch_with_timeout():
    # 전체 5초, 연결 시 2초 제한
    timeout = aiohttp.ClientTimeout(total=5, connect=2)
    try:
        async with aiohttp.ClientSession(timeout=timeout) as session:
            async with session.get('https://httpbin.org/delay/10') as resp:
                return await resp.text()
    except asyncio.TimeoutError:
        print("서버 응답 시간이 초과되었습니다.")

asyncio.run(fetch_with_timeout())
        

Example 6: 스트리밍 다운로드를 통한 대용량 파일 처리

메모리 부족 현상을 해결하기 위해 데이터를 조각(chunk) 단위로 내려받는 방법입니다.


import aiohttp
import asyncio

async def download_large_file(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            with open('large_file.zip', 'wb') as f:
                while True:
                    chunk = await resp.content.read(1024)
                    if not chunk:
                        break
                    f.write(chunk)
            print("다운로드 완료")

# URL 예시: 큰 파일 경로
# asyncio.run(download_large_file('https://example.com/huge.zip'))
        

Example 7: 사용자 정의 Exception Handling (예외 처리 통합)

네트워크 오류, HTTP 에러 등을 종합적으로 처리하는 견고한 코드 구조입니다.


import aiohttp
import asyncio

async def robust_request(url):
    async with aiohttp.ClientSession() as session:
        try:
            async with session.get(url) as resp:
                resp.raise_for_status() # 400, 500 에러 발생 시 예외 호출
                return await resp.text()
        except aiohttp.ClientConnectorError as e:
            print(f"연결 실패: {e}")
        except aiohttp.HttpProcessingError as e:
            print(f"서버 에러: {e.code}")
        except Exception as e:
            print(f"알 수 없는 오류: {e}")

asyncio.run(robust_request('https://invalid-url-example.com'))
        

4. 결론 및 SEO 최적화 제언

결론적으로, 단발성 요청이나 복잡한 비동기 구문이 부담스러운 초보적 단계라면 requests가 훌륭한 선택이 될 수 있습니다. 그러나 수백 개 이상의 API 호출을 처리해야 하거나, FastAPI와 같은 비동기 웹 프레임워크를 사용 중이라면 aiohttp는 선택이 아닌 필수입니다. 성능 최적화는 단순히 라이브러리를 바꾸는 것에서 끝나지 않습니다. 세션 재사용, 타임아웃 설정, 그리고 세마포어를 통한 동시성 제어까지 결합될 때 진정한 파이썬 비동기 프로그래밍의 가치가 발현됩니다.

내용 출처 및 기술 참조

  • aiohttp 공식 도큐먼트
  • Python asyncio 가이드
  • Real Python - "Async IO in Python: A Complete Walkthrough"
  • High Performance Python
728x90