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.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
# 이하 생략
>>> 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)
if request.method == 'GET':
do_something()
elif request.method == 'POST':
do_something_else()
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+와 같은 웹 서버의 동작과 일치한다고 한다.
FILES의 key는 <input type="file" name="">
에서 따온 name이다. value는 업로드된 파일. -> 자세한 내용은 파일 관리를 참조
요청 메서드가 POST이고 요청에 게시된 <form>
에 enctype="multipart/form-data"
가 있는 경우에만 FILES에 데이터가 포함된다!
QueryDict는 django.http.QueryDict의 instance 이다. 내가 직면한 issue는 QueryDict instance인 request -> POST data를 변경하려고 했기 때문이다.
동일한 key값에 multiple value를 처리하기 위해 커스터마이징된 dict라고 소개를 한다. 대표적으로 <select muliple>
의 html 요소가 같은 키의 여러 값을 던져준다.
코어 접근해서 코드를 보면 알겠지만 _mutable = True
값 기반으로 mutable 하다.
아래 예시를 보면 QueryDict를 왜 만들었는지 바로 알 수 있다.
>>> QueryDict('a=1&a=2&c=3')
<QueryDict: {'a': ['1', '2'], 'c': ['3']}>
위의 getitem()와 같은 로직이지만 키에 대응하는 값이 없을 때 기본값을 돌려주는 후크가 있다.
그래서 우리는 request.data.get(key, default) 값을 좋은 예시로 여긴다. 또는 get -> __getitem__
을 try - except으로 감싸면 된다. 좋은 예시라고 말은 하지만 솔직히 기본적으로 지켜야할 소양이다.
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] = "임의의값"
으로 바꾸면 안된다.
일단, 개인적인 견해로는 접근을 안하는게 가장 베스트다.
내가 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가 가장 대표적이다. 각 부분은 또 방대하기 때문에 모두 여기서 다룰 것은 아니다.
HttpRequest.session
HttpRequest.site
HttpRequest.user
rest_framework.permissions - IsAdminUser, AllowAny, IsAuthenticated
등으로 접근 제한을 APIView의 permission_classes
값을 통해 제어가 가능하다. if request.user.is_authenticated:
... # Do something for logged-in users.
else:
... # Do something for anonymous users.