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를 거친다. 요청과 응답이 모두 미들웨어를 거쳐간다고 생각하면 편하다.
Django의 request에 대해 더 자세하게 해부해 보자. DRF를 사용해도, 결국 (당연하게) core는 django를 사용한다. 오히려 DRF를 먼저 사용하면, Django의 core를 많이 놓치게 된다.
This QueryDict instance is immutable
라는 issue를 직면했다. Django의 core를 모르면 QueryDict class 에 대해서 알 수 없고, 당최 HttpRequest class에서 왜 QueryDict를 사용하는지 알 수 없었다. 우선 짚고 넘어가야 할 부분은 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)
# ... 생략
- 이 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가지 정도이다.
-
HttpRequest.session
- SessionMiddleware로 부터 박히는 속성이다. 가이드를 참고하자.
-
HttpRequest.site
- CurrentSiteMiddleware: An instance of Site or RequestSite as returned by get_current_site() representing the current site.
-
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.
- 그리고 QueryDict 공식 가이드 문서를 잘 정리해놓은 글이 있다. [Django] Querydict 객체
Author And Source
이 문제에 관하여(Django, DRF request - HttpRequest, QueryDict), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@qlgks1/Django-request-HttpRequest-QueryDict저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)