aiohttp 비동기로 API 호출하기

프로필 사진mingke

Python Logo

목차

소개

FastAPI로 개발하면서 외부로 API호출 할 때 httpx 라이브러리를 많이 사용합니다. httpx는 동기, 비동기 방식을 모두 지원하고 공식문서에서도 httpx를 사용하고 있습니다.

하지만 오늘은 aiohttp를 이용해서 비동기 API호출 방법에 대해서 알아보겠습니다.

aiohttp란

Python의 비동기 HTTP 클라이언트/서버 프레임워크입니다. 이것은 Python의 비동기 프로그래밍 기능을 활용하여, 동시에 여러 HTTP 요청을 처리할 수 있게 해주는 강력한 도구입니다. 동시에 여러 네트워크 요청을 처리할 때 I/O 대기 시간을 효율적으로 관리할 수 있게 해줍니다.

클라이언트와 서버사이드 양쪽의 기능을 모두 제공하지만 오늘은 클라이언트 사이드에서 비동기 API호출 방식에 대해서만 알아보겠습니다. 서버는 굳이 aiohttp를 사용할 필요가 없을 것 같습니다.

구현

기본적으로 다음과 같이 사용할 수 있습니다. with문을 사용해서 컨텍스트안에서 세션을 만들어 사용할 수 있습니다.

import aiohttp
 
async def request():
    try:
        async with aiohttp.ClientSession() as session:
            URI = "https://example.com"
                headers = {"example": "example"}
            async with session.post(URI, headers=headers) as res:
                response = await res.json()
                return response
    except aiohttp.ClientError as e:
        raise e

위 방식대로 하면 request를 여러번 실행했을 때 session을 계속 열었다 닫았다 해야해서 저는 다음과 같이 많이 사용합니다. 최근에 FastAPI를 많이 쓰기 때문에 FastAPI에서 lifespan에 api_client를 주입해서 사용할 수도 있습니다.

import aiohttp
import warnings
from typing import Callable, Any, Coroutine, TypeVar
from functools import wraps
 
T = TypeVar("T", bound="APIClient")
 
# 세션이 실행되지 않았을 때 에러를 발생시키는 데코레이터
def ensure_session(
    func: Callable[..., Coroutine[Any, Any, aiohttp.ClientResponse]]
) -> Callable[..., Coroutine[Any, Any, aiohttp.ClientResponse]]:
    @wraps(func)
    async def wrapper(self: T, *args: Any, **kwargs: Any) -> aiohttp.ClientResponse:
        if self.session is None:
            raise RuntimeError("세션이 시작되지 않았습니다. start() 메소드를 호출하세요.")
        return await func(self, *args, **kwargs)
 
    return wrapper
 
class APIClient:
    """
    APIClient객체를 단독적으로 사용할 수도 있고 다른 클래스에 주입하여 사용할 수 도 있음
    """
 
    def __init__(self) -> None:
        self.session = None
 
    async def start(self) -> None:
        if not self.session:
            self.session = aiohttp.ClientSession()
 
    async def close(self) -> None:
        if self.session:
            await self.session.close()
            self.session = None
 
    @ensure_session
    async def get(
        self, url: str, params: dict[str, Any] | None = None, **kwargs: Any
    ) -> aiohttp.ClientResponse:
        response = await self.session.get(url, params=params, **kwargs)
        return response
    ...
    # 같은 방식으로 다른 메소드 구현
 
    def __del__(self) -> None:
        if self.session and not self.session.closed:
            warnings.warn("APIClient 객체가 close되지 않았습니다.")
async def test():
    api_client = APIClient()
    await api_client.start()
    res1 = await api_client.get("https://www.naver.com")
    res2 = await api_client.get("https://www.naver.com")
    await api_client.close()
    return res1, res2
 
res_1, res_2 = await test()
 
class Test:
	"""
    some_method를 시작하고 닫을 때 start와 close를 반드시 실행
    """
    def __init__(self) -> None:
        self.api_client = APIClient()
 
    async def some_method(self, ...):
        response = await api_client.get("https://www.google.com")
    ...

오늘은 간단하게 aiohttp client로 API호출하는 방법을 알아봤습니다. 비동기 처리 능력은 현대 웹 애플리케이션 개발에 있어 중요한 요소이기 때문에 알아두면 좋다고 생각합니다. 이상으로 간략히 알아본 aiohttp 비동기로 API 호출하기 포스팅을 마치겠습니다.