Django, DRF request - HttpRequest, QueryDict

Django request

Django의 request에 대해 더 자세하게 해부해 보자. DRF를 사용해도, 결국 (당연하게) core는 django를 사용한다. 오히려 DRF를 먼저 사용하면, Django의 core를 많이 놓치게 된다.

  • This QueryDict instance is immutable 라는 issue를 직면했다. Django의 core를 모르면 QueryDict class 에 대해서 알 수 없고, 당최 HttpRequest class에서 왜 QueryDict를 사용하는지 알 수 없었다.

HttpRequest

  • 우선 짚고 넘어가야 할 부분은 DRF에서(정확하겐 rest_framework의 generics) 우리가 비즈니스 로직에서 가장 많이 마주하게 되는 매개변수는 "request"가 될 것이다. 이 request는 HttpRequest이다!

  • 아니 정확하겐 <class 'rest_framework.request.Request'> 이다! 그리고 Request의 instance는 The request argument must be an instance of django.http.HttpRequest 라고 명시되어 있다.

  • 그리고 django.http.request의 HttpRequest의 선언부는 아래와 같다.

class HttpRequest:
	"""A basic HTTP request."""
    
    # ... 생략
    
	def __init__(self):
        self.GET = QueryDict(mutable=True)
        self.POST = QueryDict(mutable=True)
        # ... 생략
  • 우선 client로 들어오는 모든 요청은 HttpRequest instance에 담겨진다. 그리고 그 모든 요청은 app에 도착하기 전에 middleware를 거친다. 요청과 응답이 모두 미들웨어를 거쳐간다고 생각하면 편하다.

  • 이 HttpRequest는 공식가이드에 기깔나게 정리가 되어있다. 3.0version 기준인 점은 유의 부탁한다.

주요 Attribute

HttpRequest.headers # request의 headers 객체
HttpRequest.body  	# request의 body 객체
HttpRequest.COOKIES # 모든 쿠키를 담고 있는 딕셔너리 객체
HttpRequest.method 	# reqeust의 메소드 타입
HttpRequest.FILES	# <input type='file'> 로 보낸 UploadFile!
HttpRequest.META	# HTTP 헤더가 포함하는 모든 접근 가능한 정보를 담고 있는 dict, 메타 정보는 (당연히) web-server에 영향을 받는다.
HttpRequest.GET 	# GET 파라미터를 담고 있는 QueryDict instance
HTTpRequest.POST 	# POST 파라미터를 담고 있는 QueryDict instance

# 이하 생략

HttpRequest.headers

  • client가 http 요청을 할 때 담고있는 헤더값에 접근하며, 아래와 같이 접근 할 수 있다.
>>> request.headers
{'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6', ...}

>>> 'User-Agent' in request.headers
True
>>> 'user-agent' in request.headers
True

>>> request.headers['User-Agent']
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)
>>> request.headers['user-agent']
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)

>>> request.headers.get('User-Agent')
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)
>>> request.headers.get('user-agent')
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6)

HttpRequest.method

  • client의 http요청 메소드가 무엇인지 판별한다.
if request.method == 'GET':
    do_something()
elif request.method == 'POST':
    do_something_else()

HttpRequest.META

CONTENT_LENGTH – The length of the request body (as a string).
CONTENT_TYPE – The MIME type of the request body.
HTTP_ACCEPT – Acceptable content types for the response.
HTTP_ACCEPT_ENCODING – Acceptable encodings for the response.
HTTP_ACCEPT_LANGUAGE – Acceptable languages for the response.
HTTP_HOST – The HTTP Host header sent by the client.
HTTP_REFERER – The referring page, if any.
HTTP_USER_AGENT – The client’s user-agent string.
QUERY_STRING – The query string, as a single (unparsed) string.
REMOTE_ADDR – The IP address of the client.
REMOTE_HOST – The hostname of the client.
REMOTE_USER – The user authenticated by the Web server, if any.
REQUEST_METHOD – A string such as "GET" or "POST".
SERVER_NAME – The hostname of the server.
SERVER_PORT – The port of the server (as a string).

  • CONTENTLENGTE와 CONTENT_TYPE을 제외하고 요청의 모든 HTTP 헤더는 모든 문자를 대문자로 변환하고 하이픈을 밑줄로 바꾸고 이름에 HTTP 접두사를 추가하여 META 키로 변환된다.

  • 예를 들어, X-Bender라는 헤더는 META 키 HTTP_X_BENDER에 매핑된다!

  • 하지만 우리가 흔히 로컬 테스트를 위해 python manage.py runserver로 돌릴 땐 META를 확인할 수 없다. 밑줄과 대시 사이의 모호성에 기반한 헤더 스푸핑이 WSGI 환경 변수의 밑줄로 정규화되는 것을 방지하기 위해서다. Nginx와 Apache 2.4+와 같은 웹 서버의 동작과 일치한다고 한다.

HttpRequest.FILES

  • FILES의 key는 <input type="file" name="">에서 따온 name이다. value는 업로드된 파일. -> 자세한 내용은 파일 관리를 참조

  • 요청 메서드가 POST이고 요청에 게시된 <form>enctype="multipart/form-data"가 있는 경우에만 FILES에 데이터가 포함된다!

HttpRequest.GET과 HttpRequest.POST는 QueryDict를 살펴보면서 같이 살펴보자.


QueryDict

QueryDict는 django.http.QueryDict의 instance 이다. 내가 직면한 issue는 QueryDict instance인 request -> POST data를 변경하려고 했기 때문이다.

  • 동일한 key값에 multiple value를 처리하기 위해 커스터마이징된 dict라고 소개를 한다. 대표적으로 <select muliple>의 html 요소가 같은 키의 여러 값을 던져준다.

  • 코어 접근해서 코드를 보면 알겠지만 _mutable = True 값 기반으로 mutable 하다.

    • mutable vs immutable, 사실 Call-By-Value, Call-By-Reference를 알면 바로 이해할 수 있을 것이다.
  • 아래 예시를 보면 QueryDict를 왜 만들었는지 바로 알 수 있다.

>>> QueryDict('a=1&a=2&c=3')
<QueryDict: {'a': ['1', '2'], 'c': ['3']}>

주요 method

  • 다 살펴보진 않겠다. 더 자세한 사항은 가이드에서 QueryDict objects를 뜯어보면 된다.

QueryDict.getitem(key)

  • 지정된 키에 대해 값을 돌려준다. 하나의 키에 복수의 값이 존재하는 경우, getitem()은 리스트의 끝에 값을 돌려준다. 키에 대응하는 값이 없으면 django.utils.datastructure.MutivalueDictKeyError 에러가 보여진다.

QueryDict.get(key, default = None)

  • 위의 getitem()와 같은 로직이지만 키에 대응하는 값이 없을 때 기본값을 돌려주는 후크가 있다.

  • 그래서 우리는 request.data.get(key, default) 값을 좋은 예시로 여긴다. 또는 get -> __getitem__ 을 try - except으로 감싸면 된다. 좋은 예시라고 말은 하지만 솔직히 기본적으로 지켜야할 소양이다.

QueryDict.copy()

  • Python 표준 라이브러리의 copy.deepcopy()를 사용하여 객체의 복제를 생성하여 리턴한다. 복제는 변경가능하므로 값을 변경할 수 있다.

QueryDict.update(other_dict)

  • QueryDict 혹은 표준 사전형을 인수로 취한다. 표준 사전형의 update()메소드와 동일하지만, 현재의 값을 대체하지 않고 현재 값을 리스트에 추가한다.

QueryDict.setitem(key, value)

  • key에 대한 value를 (value라는 값이 1개 들어 있는 리스트)로 한다. 다른 함수와 동일하게 이 메소드를 호출하는 것은 (copy()를 사용해 생성한 객체와 같이) 변환 가능한 QueryDict뿐이다.

  • django -> http -> request에서 QueryDict를 보자. class _mutable 값은 True로 되어 있다. 하지만 classmethod인 fromkeys 메소드를 보면 _mutable = False 로 바꿔주며 _assert_mutable 메소드에서는 False를 걸러낸다. 즉 Ture를 유지하게 한다. -> QueryDicts at request.POST and request.GET will be immutable when accessed in a normal request/response cycle.

  • 즉 위와 같이 코어를 보면 QueryDict는 mutable = False 로 인해 immutable 한 object임을 알았을 것이다. 그렇기 때문에 request.data[key] = "임의의값" 으로 바꾸면 안된다.

DRF와 QueryDict 현명하게 콜라보 하기

  • 일단, 개인적인 견해로는 접근을 안하는게 가장 베스트다.

  • 내가 request.data[key]를 접근하게 된 이유는 DRF의 generics.ListCreateAPIView를 상속받은 API class에서 create override해서 request.data 만 바꾸고 super().create(request, *args, **kwargs)를 해주고 싶었기 때문이다. (시리얼라이저, 벨리데이션, 에러 핸들링의 이유로)

  • 즉 FE(client side)에서는 user정보는 Token만 주는데, middleware에 의해 그 user정보는 request.user에 저장된다. 🔥 나는 request.data['user']에 request.user.pk 값을 담고 싶었을 뿐이다. 🔥 model엔 user_id가 필요했고, 기본 create 메서드만으로는 그 값을 박아줄 수 없었기 때문이다!!

  • 그래서 아래와 같이 임시 방편으로 해결을 했었다.

  request.data._mutable = True
  request.data['buyer'] = request.user.pk
  request.data._mutable = False
  return super().create(request, *args, **kwargs)
  • 하지만 _mutable를 강제로 True로 바꾸고 값을 insert 치는 것은 위험하다. 위 처럼 간단하면 상관없지만, 항상 immputable을 유지해야 하는데 중간에 다른 곳에서 data접근해서 뒷단에서 (serializer)에서 허용하는 다른 값이 들어가면 바로 500을 뱉기 때문이다.

  • 그래서 사실 super().create로 바로 넘기지 말고, 결국엔 전체를 오버라이딩 해서 쓰는게 가장 깔끔하다. 그리고 copy (deep-copy)와 update를, 잘 사용하는 것이다, 아래 예시는 deep-copy 대신 update로만 할 수 도 있다.

        request_data = request.data.copy() # 또는 아래와 같이
        # request_data = OrderedDict()
        request_data.update(request.data)
        request_data['buyer'] = request.user.pk

        serializer = self.get_serializer(data=request_data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
  • copy 대체 방법으로 왜 OrderedDict를 사용했냐? -> 미연의 버그 방지를 위해서다

  • 오히려 이 error에 감사하다. 얼마나 날먹해서 코드를 짜려고 했는지 스스로 되돌아보게 된다. 수많은 부분이 추상화 되어 압축된 부분 마저 코드가 길어보여 오버라이딩도 안하는 지경이라니,, 오늘도 1 반성 한다.


추가로 살펴봐야 할 사항

middleware에 의해 추가되는 HttpRequest의 주요 속성들이 있다. 가장 흔히 우리가 보는 user가 가장 대표적이다. 각 부분은 또 방대하기 때문에 모두 여기서 다룰 것은 아니다.

  • 기본적으로 사용하는 middleware에 의해 박히는 가장 대표적인 attribute는 아래 3가지 정도이다.
  1. HttpRequest.session

    • SessionMiddleware로 부터 박히는 속성이다. 가이드를 참고하자.
  2. HttpRequest.site

    • CurrentSiteMiddleware: An instance of Site or RequestSite as returned by get_current_site() representing the current site.
  3. HttpRequest.user

    • AuthenticationMiddleware: An instance of AUTH_USER_MODEL representing the currently logged-in user.
    • 우리가 가장 많이 만나는 middleware에 의해 추가되는 속성이다.
    • Auth를 컨트롤하기 때문에 api 접근 제한으로 가장 기본적으로 속성을 접근해 판단한다. 그리고 DRF에서는 rest_framework.permissions - IsAdminUser, AllowAny, IsAuthenticated 등으로 접근 제한을 APIView의 permission_classes 값을 통해 제어가 가능하다.
    • 이 Auth만 따로 봐야할 정도로 중요하고, 깊은 이해를 통한 사용이 필요한 부분이다. 그리고 사실 꼭 core 기반으로 커스텀하고 프로젝트를 시작해야한다고 생각한다.
    • If the user isn’t currently logged in, user will be set to an instance of AnonymousUser. You can tell them apart with is_authenticated, like so:
if request.user.is_authenticated:
    ... # Do something for logged-in users.
else:
    ... # Do something for anonymous users.

좋은 웹페이지 즐겨찾기