ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] requests 모듈 retry 추가하기
    언어/파이썬 & 장고 2022. 4. 24. 22:57

    requests 모듈은 https://brownbears.tistory.com/198 와 같이 간단하게 사용할 순 있지만 retry 옵션은 존재하지 않습니다. 아래는 반복문, try-except문으로 retry 기능을 추가하는 것이 아닌 requests 모듈에서 제공하는 기능으로 retry와 알아두면 유용한 기능을 설명합니다.

    Session와 HTTPAdapter 클래스를 사용하여 retry 간단하게 구현하기

    구현에 앞서 흔히 사용하는 requests 구조를 먼저 파악해 봅니다. requests 모듈은 보통 아래와 같이 사용합니다.

    import request
    
    requests.get('http://www.naver.com')
    # 또는
    requests.request('get', 'http://www.naver.com')

    requests.get() 의 get 함수를 들어가면 아래와 같이 request 함수를 정의하고 있습니다.

    def get(url, params=None, **kwargs):
        r"""Sends a GET request.
    
        :param url: URL for the new :class:`Request` object.
        :param params: (optional) Dictionary, list of tuples or bytes to send
            in the query string for the :class:`Request`.
        :param \*\*kwargs: Optional arguments that ``request`` takes.
        :return: :class:`Response <Response>` object
        :rtype: requests.Response
        """
    
        return request('get', url, params=params, **kwargs)

    다음 request 함수를 들어가보면 아래와 같이 Session 클래스를 선언하여 호출하는 것을 알 수 있습니다.

    def request(method, url, **kwargs):
        """Constructs and sends a :class:`Request <Request>`.
    
        :param method: method for the new :class:`Request` object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``.
        :param url: URL for the new :class:`Request` object.
        :param params: (optional) Dictionary, list of tuples or bytes to send
            in the query string for the :class:`Request`.
        :param data: (optional) Dictionary, list of tuples, bytes, or file-like
            object to send in the body of the :class:`Request`.
        :param json: (optional) A JSON serializable Python object to send in the body of the :class:`Request`.
        :param headers: (optional) Dictionary of HTTP Headers to send with the :class:`Request`.
        :param cookies: (optional) Dict or CookieJar object to send with the :class:`Request`.
        :param files: (optional) Dictionary of ``'name': file-like-objects`` (or ``{'name': file-tuple}``) for multipart encoding upload.
            ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')``
            or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string
            defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers
            to add for the file.
        :param auth: (optional) Auth tuple to enable Basic/Digest/Custom HTTP Auth.
        :param timeout: (optional) How many seconds to wait for the server to send data
            before giving up, as a float, or a :ref:`(connect timeout, read
            timeout) <timeouts>` tuple.
        :type timeout: float or tuple
        :param allow_redirects: (optional) Boolean. Enable/disable GET/OPTIONS/POST/PUT/PATCH/DELETE/HEAD redirection. Defaults to ``True``.
        :type allow_redirects: bool
        :param proxies: (optional) Dictionary mapping protocol to the URL of the proxy.
        :param verify: (optional) Either a boolean, in which case it controls whether we verify
                the server's TLS certificate, or a string, in which case it must be a path
                to a CA bundle to use. Defaults to ``True``.
        :param stream: (optional) if ``False``, the response content will be immediately downloaded.
        :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair.
        :return: :class:`Response <Response>` object
        :rtype: requests.Response
    
        Usage::
    
          >>> import requests
          >>> req = requests.request('GET', 'https://httpbin.org/get')
          >>> req
          <Response [200]>
        """
    
        # By using the 'with' statement we are sure the session is closed, thus we
        # avoid leaving sockets open which can trigger a ResourceWarning in some
        # cases, and look like a memory leak in others.
        with sessions.Session() as session:
            return session.request(method=method, url=url, **kwargs)

    지금부터는 직접 Session()을 선언하고 HTTPAdapter에 retry를 지정하도록 합니다.

    from requests.sessions import Session
    from requests.adapters import HTTPAdapter
    
    method = 'get'
    url = 'http://www.naver.com'
    
    with Session() as session:
        adapter = HTTPAdapter(max_retries=5)
            # http:// 로 시작하면 adapter 내용을 적용
        session.mount("http://", adapter)
        # https:// 로 시작하면 adapter 내용을 적용
        session.mount("https://", adapter)
    
        session.request(method=method, url=url)
        # 또는
        session.get(url=url)

    여기서 HTTPAdapter 클래스에 최대 retry를 5번 지정하였고 session에 http:// 또는 https:// 로 시작하는 주소일 경우, retry가 적용하도록 처리한 예제입니다. 만약 [ftp://aaaaa.bbb](ftp://aaaaa.bbb) 와 같은 url을 호출한다면 retry가 되지 않습니다.

    retry 클래스를 활용하여 구현하기

    위의 예제는 HTTPAdapter 클래스에 간단하게 retry할 횟수만 지정했는데 Retry 클래스를 활용하면 connection 관련 에러는 몇번 재시도 할 지, 오류가 났을 때, 다음 재시도는 몇 초 지나서 할 지 등등 지정할 수 있습니다.

    from requests.sessions import Session
    from requests.adapters import HTTPAdapter, Retry
    
    method = 'get'
    url = 'http://www.naver.com'
    
    with Session() as session:
        connect = 3
        read = 5
        backoff_factor = 1
        RETRY_AFTER_STATUS_CODES = (400, 403, 500, 503)
    
        retry = Retry(
            total=(connect + read),
            connect=connect,
            read=read,
            backoff_factor=backoff_factor,
            status_forcelist=RETRY_AFTER_STATUS_CODES,
        )
    
        adapter = HTTPAdapter(max_retries=retry)
        # http:// 로 시작하면 adapter 내용을 적용
        session.mount("http://", adapter)
        # https:// 로 시작하면 adapter 내용을 적용
        session.mount("https://", adapter)
    
        session.request(method=method, url=url)
        # 또는
        session.get(url=url)

    Retry 클래스에서 connect 옵션은 connection 관련 오류가 발생했을 때, 몇 번까지 재시도 할지 정할 수 있습니다.

    read 옵션은 read 관련 오류가 발생했을 때, 몇 번까지 재시도 할지 정할 수 있습니다.

    total은 총 재시도 횟수로 connect와 read 옵션을 지정했다면 두 값의 합을 지정하거나 합보다 큰 수를 정하면 됩니다.

    backoff_factor는 오류가 났을 때, 다음 재시도를 언제 할지 정하는 값입니다. 위에서는 1초로 지정했는데 오류가 났을 때 1초마다 재시도 하는 것이 아닌 backoff_factor 계산 공식에 따라 재시도를 진행합니다.

    backoff_factor * (2 ^ (재시도횟수 - 1))

    만약 backoff_factor 값이 1이고 재시도횟수가 1번째라면 아래와 같습니다.

    1 * 2 ^ (1 - 1)
    = 1초

    재시도 횟수가 8번째라면 아래와 같습니다.

    1 * 2 ^ (8 - 1)
    = 127초

    설정은 1초로 했지만 재시도 횟수가 늘어날수록 대기하는 시간도 급격하게 늘어나므로 잘 계산해서 지정해야 합니다.

    status_forcelist 옵션은 어떤 status code일 때, 재시도할지 정하는 옵션입니다. 위 예제에서는 status code가 400, 403, 500, 503일 때 재시도를 하도록 설정했습니다.

    위에서 설명한 것 이외에도 Retry 클래스에서는 아래와 같이 여러 옵션을 지정할 수 있습니다.

    댓글