
디자인 패턴이란 특정 문맥에서 공통적으로 발생하는 문제에 대해 재사용 가능한 해결책이다. 규모가 큰 프로젝트 일수록 코드 간 분리와 재사용을 위해 적절한 디자인 패턴을 반드시 도입해야 한다. requests.py 에도 객체 지향을 위한 몇 가지 디자인 패턴들이 적용되어 있다.
파일: session.py, models.py
Mixin 은 특정 코드를 다른 클래스에 삽입 할 수 있도록 하는 디자인 패턴이다. Mixin 클래스는 필요로 하는 기능들을 포함한 상위 클래스이고, 하위 클래스는 이 기능을 상속해 재사용할 수 있다. Mixin 은 Interface, Abstract Class 와 마찬가지로 단독으로 사용할 수 없다.
class SessionRedirectMixin
class Session(SessionRedirectMixin)
session.py 에서는 SessionRedirectMixin 을 Session 에 삽입해 Redirection 기능을 제공한다. 만약 Session 이 이를 상속받지 않을 경우 Redirection 은 지원되지 않는다.
class RequestEncodingMixin
class RequestHooksMixin
class Request(RequestHooksMixin)
class PreparedRequest(RequestEncodingMixin, RequestHooksMixin)
model.py 는 RequestEncodingMixin 과 RequestHooksMixin 이 있다. 이들은 Requst, PreparedRequest 에 삽입되어 encoding과 hook을 제공한다. 파이썬은 다중 상속을 지원하기 때문에 PreparedRequest 에서 두 Mixin 을 모두 상속 받을 수 있다.
import requests
req = requests.Request('GET', 'https://httpbin.org/get')
r = req.prepare()
<PreparedRequest [GET]>
s = requests.Session()
s.send(r)
<Response [200]>
위와 같이 사용자가 Request 객체를 만들더라도 prepare 된 PreparedRequest 만 서버로 전송이 가능하다. 따라서 string to byte 를 담당하는 RequestEncodingMixin 은 PreparedRequest 에만 추가되었다.
class SessionRedirectMixin:
def get_redirect_target(self, resp):
...
def should_strip_auth(self, old_url, new_url):
...
...
Mixin 은 Interface 와 유사하게 내부 상태를 저장하지 않는다. 동시에 Abstract method 와 유사하게 구현된 메서드(concrete method)를 가진다. session.py 의 SessionRedirectMixin 에서도 내부 상태는 존재하지 않으며 method 가 정의되어있다. Mixin 은 단독으로 인스턴스화 될 수 없는데, 각 파일의 Mixin 을 보면 생성자 메서드 init 가 정의되어 있지 않은 것을 확인할 수 있다.
저자는 왜 Mixin 을 사용했을까? Mixin 은 클래스에 쉽게 기능을 추가할 수 있게 만든다. 만일 PreparedRequest 에 새로운 CustomMixin 을 추가할 경우 PreparedRequest(CustomMixin, RequestEncodingMixin, RequestHooksMixin) 으로 재작성하면 된다. 또는 새로운 종류의 CustomRequest 를 정의할 때 hook 을 지원하고 싶을 경우 기존에 RequestHooksMixin 을 상속받아 간단히 기능을 추가할 수 있다.
파일: adapters.py
Adapter 는 호환되지 않는 인터페이스를 가진 객체가 협업할 수 있도록 하는 Structural 디자인 패턴이다. Adapter 는 서로 소통해야 하는 Client 객체와 Service 객체의 인터페이스가 호환되지 않을 때 이들이 호환되도록 중간에서 인터페이스를 변환하는 역할을 한다. 이는 마치 220v 플러그와 110v 콘센트가 호환되지 않을 때 사용하는 220v to 110v 어댑터와 같다.
requests 는 자체적으로 HTTP request 를 보내지 않는 라이브러리로 직접 low-level 을 다루지 않는다. 대신 urllib3, httplib 등 HTTP 라이브러리를 사용하기 쉽게 래핑하였다. 이 래핑 과정에서 Adapter 패턴이 필요하다.
class Session(SessionRedirectMixin):
...
def send(self, request, **kwargs):
...
adapter = self.get_adapter(url=request.url)
r = adapter.send(request, **kwargs)
...
class BaseAdapter:
...
def send(
self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
):
def close(self):
...
사용자가 HTTP 요청시 requests.py는 session 객체를 생성한 후 요청한 url 에 맞는 adapter 를 찾아 요청을 전송하게 된다. session.py 에서 바로 urllib3 의 HTTP method를 호출해야 한다면 필요한 파라미터들을 준비해야 하는데, 이는 session 객체의 원래 목적인 세션 유지와는 거리가 멀어 응집성이 떨어질 수 있다. 따라서, adapter 패턴을 사용해 session.py 는 BaseAdapter 에 명시된 send 메서드만 사용하면 되고, 구현은 BaseAdapter 를 구현한 구현체 어댑터 (HttpAdapter 등) 에 위임한다.
requests 는 Adapter 패턴을 사용해 session 과 관련없는 HTTP 요청에 관한 내용을 추상화했다. 하지만 모든 HTTP 요청은 타 라이브러리로 위임되기 때문에 requests 는 사실상 wrapper 역할을 하고 있다. 물론 low-level 을 다루는 urllib 와 비교했을 때 더 user-friendly 한 인터페이스를 제공하고 있긴 하지만, urllib3 가 더 많은 기능을 제공하고 low-level 에서 세밀하게 조작할 수 있다는 점에서 보아 사용자의 요구사항에 따라 맞는 라이브버리를 선택할 필요가 있다.
Strategy 패턴은 특정한 계열의 알고리즘들을 정의하고 이 알고리즘들을 해당 계열 안에서 상호 교환할 수 있도록 하는 Behavioral 디자인 패턴이다. Context를 변경하지 않고도 새로운 strategy 를 도입할 수 있고(OCP, Open-Closed Principle), Inheritance을 Composition 으로 대체해 객체간 결합도를 낮추고, 런타임에 strategy 를 쉽게 변경할 수 있기 때문에 자주 쓰이는 디자인 패턴 중 하나다.
class AuthBase:
"""Base class that all auth implementations derive from"""
def __call__(self, r):
raise NotImplementedError("Auth hooks must be callable.")
class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
...
def prepare_auth(self, auth, url=""):
...
r = auth(self)
...

requests 는 auth.py, model.py 에서 Strategy 패턴을 사용한다. auth.py 는 인증을 담당하는 모듈로 인증 계열의 알고리즘은 AuthBase 를 상속하도록 한다. 그리고 AuthBase 객체를 최종적으로 call 하는 객체인 model.py 의 PreparedRequest 가 곧 Strategy 패턴의 Context 가 된다. requests 에서 기본으로 제공하는 인증 방식(전략)으로 HTTPBasicAuth, HTTPDigestAuth, HTTPProxyAuth 가 있다.
from requests.auth import AuthBase
class PizzaAuth(AuthBase):
"""Attaches HTTP Pizza Authentication to the given Request object."""
def __init__(self, username):
# setup any auth-related data here
self.username = username
def __call__(self, r):
# modify and return the request
r.headers['X-Pizza'] = self.username
return r
# 기본 전략
requests.get('http:/basicbin.org/admin', auth=HTTPBasicAuth())
# 런타임에 전략 변경
requests.get('http:/basicbin.org/admin', auth=HTTPProxyAuth())
# 새 PizzaAuth 전략 추가
requests.get('http://pizzabin.org/admin', auth=PizzaAuth('kenneth'))
Strategy 패턴을 통해 사용자가 런타임에 인증 전략을 바꾸고 싶을시 쉽게 전환할 수 있다. 또한 새 인증 방식이 추가될 경우도 유연하게 대처할 수 있다. 만약 PizzaAuth 라는 새로운 인증 방식이 추가된다고 했을 때 사용자는 기존 코드에 대한 변경 없이 AuthBase 를 상속한 PizzaAuth 클래스를 만들어 기능을 추가할 수 있다.
디자인 패턴에 절대적인 정답은 없다. 혹자는 “Mixin(Multiple Inheritance) 대신 Composition 을 사용하면 기술적으로 이러이러한 점이 더 좋아요!” “이렇게 구조를 고치는건 어떨까요?” 라고 제안할 수 있다. 물론 기술적으로는 정말 그게 더 나은 방안일 수 있다! 하지만 requests 라이브러리는 요구사항을 모두 만족해 코드 성장을 멈췄으며, 오래 전에 인터페이스가 고정이 되었다. 그래서 인터페이스를 변경하기보다 그대로 유지하는 것이 사용자들에게 더 좋은 방향일 것이다.