목차
1. FBV & CBV
2. End-Point
3. Request
4. Response
5. APIView
6. request.user에 대한 고찰
1. FBV & CBV
Django는 urls.py에 규정된 경로를 통해 들어온 요청을 views.py에 넘겨 처리한다.
View를 처리하는 방법은 2가지인데 함수(FBV)와 클래스(CBV)로 처리하는 방법이다.
1. Function-Base Views
함수 기반 뷰는 심플하고 가속성이 좋다.
가장 근본적인 단위로 내려가서 작업을 처리하는 것은 프레임 워크의 제약에서 가장 많이 벗어난 상태이기 때문에 로직을 구현하는 것도 개발자의 실력에 따라 성능이 크게 영향을 받는다.
urlpatterns = [
path('', exampleView, name='example-list'),
path('<int:pk>/' exampleDetailView, name='example-detail')
]
@api_view(["GET", "POST"])
def exampleView(request):
if request.method == "GET":
queryset = Example.objects.filter(something=True)
serializer = ExampleSerializer(queryset, many=True)
return Response(serializer.data)
else:
serializer = ExampleSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
프레임 워크의 제약에서 벗어나는 것은 자유롭지만, 반대로 말하면 강력한 기능들을 지원받을 수 없음을 의미한다.
애초에 클래스를 사용하지 않는다면 코드 확장이나 재활용에서 어려움을 겪을 수밖에 없다.
2. Class-Base Views
클래스 기반 뷰를 사용하면 프레임 워크에서 지원하는 상속과 Mixin 기능들을 이용하여 손쉽고 간편하게 재활용이 용이한 뷰를 체계적으로 구성할 수 있다.
특히 여러 Mixin을 다중상속받은 여러 클래스들을 상속받아서 코드를 간결하게 작성할 수 있다.
단, 403, 404, 500 에러 핸들링은 CBV가 아니라 FBV를 이용해야 한다는 가이드 라인이 있다.
상속받는 View 클래스 중에서는 APIView가 가장 함수형 뷰에 가깝다.
2. End-Point
엔드 포인트라는 것은 api를 요청하기 위한 규악들이며, 이후 drf가 url을 통해 얻을 수 있는 정보들을 넘겨준다.
GET 요청은 body를 포함하지 않기 때문에 클라이언트로 부터 별다른 정보를 얻지 못 한다.
하지만 요청을 한 경로를 통해서 적어도 pk값을 알아낼 수는 있다.
urlpatterns = [
path('', exampleView, name='example-list'), # detail == False
path('<int:pk>/' exampleDetailView, name='example-detail') # detail == True
]
보통 get 요청의 list나 post의 create를 수행하는 뷰의 경우엔 pk 정보가 필요없어서 Detail=False라고 하고,
이외에 Retrieve, Update, Delete같은 경우엔 Detail=True로 지정한다.
하지만 list 메서드라고 해서 pk값이 무조건 필요없는 것도 아니다.
예를 들어, 중첩된 url의 케이스를 생각해보자.
urlpatterns = [
path("example/<int:pk>/event/", ...)
]
전체 event가 아닌 example의 pk값을 참조하는 레코드만 불러와야 하므로 pk 정보가 필수적으로 선행되어야 한다.
즉, 분명히 list 메서드를 사용하고는 있지만 detail 요소가 필요하다.
하지만 그것이 event의 detail 정보를 필요로 하는 것은 아니기 때문에 마찬가지로 detail=False라고 간주하는 것이다.
3. Request
공식문서의 내용을 살펴보자.
DRF에서 HTTP 요청 객체로써 HttpRequest 객체를 확장시킨 Request 객체를 사용한다.
Request는 HttpRequest 객체보다 보다 유연하게 요청 내용을 파싱할 수 있도록 도와준다.
- Request Parsing
- request.data : Post, Put, Patch 메서드에서 Body에 담긴 데이터를 리턴한다.
- request.query_params : 쿼리 스트링으로 전달되는 데이터를 리턴한다.
- Request Authentication
- request.user : django.contrib.auth.models.User 객체를 리턴한다.
- request.auth : 해당 객체의 Token 혹은 None을 리턴한다.
- Browser enhancements
- request.method : 현재 요청된 request method를 리턴한다.
- request.content_type : requset의 컨텐트 타입을 리턴한다.
Postman으로 Body에 해당 JSON 타입의 데이터를 보냈을 때, request의 body를 확인하는 방법은 아래와 같다.
class PetViewSet(ModelViewSet):
queryset = Pet.objects.all()
serializer_class = PetSerializer
def create(self, request):
print(request.data)
4. Response
렌더링되지 않은 내용을 읽어서 클라이언트가 요청한 컨텐츠 타입에 맞는 형식으로 자동 렌더링 해준다.
Django와 달리 DRF에선 Response 객체가 알아서 데이터 타입을 판단하여 렌더링 시켜버린다.
# Signature
Response(data, status=None, template_name=None, headers=None, content_type=None)
# use case
return Response(data, status.HTTP_201_CREATED)
📌 status
처음 DRF를 만지작 거릴 때, Response 객체 안에 던져주는 status는 멋있어 보이기 위한 걸까? 싶었는데
리액트와 연동하면서 그 중요성을 어느정도 이해하게 되었다.
status 모듈 안에는 각각의 상태 정보를 속성으로 담고 있기 때문에 status.[상태 속성값]으로 상황에 맞는 상태 값을 전달해준다.
사실 이 기능은 성공했을 때보다 에러가 났을 때 중요하다고 생각한다.
에러의 종류는 너무나도 다양하다. 권한이나 인증 문제, 잘못된 body 정보 등등.
DRF는 그냥 에러처리를 해버리면 그만이지만 리액트는 왜 에러가 났는지를 구분해서 사용자에게 알려주어야 한다.
따라서 DRF에선 어느 부분에서 예외 처리가 발생했는지 status를 넘겨주어야 프론트에서 적절한 조치를 취할 수 있다.
5. APIView
APIView에만 해당되는 내용인 것처럼 이름을 정했지만 api_view도 마찬가지다.
두 종류의 뷰 모두 여러 기본값을 가지는 설정들이 있다.
개발자는 필요에 의해 해당 값들을 수정할 수 있다.
- 직렬화 클래스 지정
- renderer_classes
- default
- JSON 직렬화 : rest_framework.renderers.JSONRenderer
- HTML 페이지 직렬화 : rest_framework.renderers.TemplateHTMLRenderer
- 비직렬화 클래스 지정
- parser_classes
- default
- JSON 포맷 처리 : rest_framework.parsers.JSONParser
- FormParser : rest_framework.parsers.FormParser
- MultiPartParser : rest_framework.parsers.MultiPartParser
- 인증 클래스 지정
- authentication_classes
- default
- 세션기반인증 : rest_framework.authentication.SessionAuthentication
- HTTP basic 인증 : rest_framework.authentication.BasicAuthentication
- 사용량 제한 클래스 지정
- throttle_classes
- default
- 빈 튜플
- 권한 클래스 지정
- permission_classes
- default
- 누구라도 접근 허용 : rest_framework.permissions.AllowAny
- 요청에 따라 적절한 직렬화/비직렬화 선택
- content_negotiation_class
- 같은 URL 요청에 대해서 JSON 응답을 할 지, HTML 응답을 할 지 판단
- default
- rest_framework.negotiation.DefaultContentNegotiation
- 요청 내역에서 API 버전 정보를 탐지할 클래스 지정
- versioning_class
- 요청 URL의 HEADER에서 버전 정보를 탐지하여 맞는 버전을 호출
- default
- 버전 정보를 탐지하지 않습니다. : None
APIView는 하나의 url에 대해서 작업을 수행할 수 있는데 내용은 아래와 같다.
- /example/ 에 대한 CBV
- get : 포스팅 목록
- post : 새 포스팅 생성
- /example/<int:pk>/ 에 대한 CBV
- get : pk 번 포스팅 내용
- put : pk번 포스팅 수정
- delete : pk번 포스팅 삭제
class CreateMeetingView(APIView):
permission_classes = [IsAuthenticated, GroupMemberPermission]
def post(self, request, *args, **kwargs):
group_id = kwargs.pop('pk', False)
data = {"group_id" : group_id, "create_dt": request.data['date']}
serializer = CreateMeetingSerializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
class DetailMeetingView(APIView):
permission_classes = [IsAuthenticated, GroupMemberPermission]
def get(self, request, *args, **kwargs):
meeting_id = kwargs.pop('m_pk', False)
meeting = Meeting.objects.get(meeting_id=meeting_id)
serializer_meeting = MeetingSerializer(meeting)
receipts = meeting.receipt_set.all()
serializer_receipt = ListReceiptSerializer(instance=receipts, many=True)
return Response({
"meeting_info": serializer_meeting.data,
"receipts_info": serializer_receipt.data
})
예전에 작성했던 코드긴 한데 ㅎㅎ 뭐 대충 이런 식으로 굴러갈 수 있다는 것만 알 수 있으면 된다.
Create의 경우 post 요청을 처리하고 있고, Detail 정보를 보여주는 경우엔 get 요청을 처리하고 있다.
6. request.user에 대한 고찰
여기서부턴 개발하다가 궁금했던 내용을 정리하는 내용이라 어려울 수 있다.
만약 내용을 읽어보려면 기본적으로 JWT에 대한 개념을 숙지하고 있어야 한다.
공식 문서에 따르면 Authentication in Web request에서 Django는 session과 middleware를 사용하여 인증 시스템을 request 객체에 연결시킨다.
덕분에 세션DB가 됐건 JWT가 됐건 로그인이 되어있는 유저라면 request.user로 요청을 보낸 유저가 누구인지 알 수 있다.
request.user는 현재 사용자가 로그인 되어 있지 않다면 AnonymousUser 클래스의 인스턴스로 설정된다.
미들웨어는 settings에 선언된 순서대로 동작한다.
session과 auth가 실행되고 나면 request.user는 AnonymousUser로 세팅된다.
JWT의 경우에도 cookie값에 따로 session 데이터가 없으므로 AnonymousUser로 세팅된다.
django와 RESTFramework의 연동을 위해서 일단 django의 request 미들웨어가 실행된다.
#django.core.handlers.base.BaseHandler
def resolve_request(self, request):
"""
Retrieve/set the urlconf for the request. Return the view resolved,
with its args and kwargs.
"""
# Work out the resolver.
if hasattr(request, 'urlconf'):
urlconf = request.urlconf
set_urlconf(urlconf)
resolver = get_resolver(urlconf)
else:
resolver = get_resolver()
# Resolve the view, and assign the match object back to the request.
resolver_match = resolver.resolve(request.path_info)
request.resolver_match = resolver_match
return resolver_match
그다음 view 미들웨어(cors, csrf 미들웨어)가 실행되면 make_view_atomic을 통해 rest_framework의 view를 실행한다.
#django.core.handlers.base.BaseHandler
def make_view_atomic(self, view):
non_atomic_requests = getattr(view, '_non_atomic_requests', set())
for db in connections.all():
if db.settings_dict['ATOMIC_REQUESTS'] and db.alias not in non_atomic_requests:
if asyncio.iscoroutinefunction(view):
raise RuntimeError(
'You cannot use ATOMIC_REQUESTS with async views.'
)
view = transaction.atomic(using=db.alias)(view)
return view
rest_framework의 뷰가 실행되는 과정은 url을 통해 명시한 view가 실행되도록 한다.
# rest_framework.views.APIView
def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
but with extra hooks for startup, finalize, and exception handling.
"""
self.args = args
self.kwargs = kwargs
request = self.initialize_request(request, *args, **kwargs)
self.request = request
self.headers = self.default_response_headers # deprecate?
try:
self.initial(request, *args, **kwargs)
# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response = handler(request, *args, **kwargs)
except Exception as exc:
response = self.handle_exception(exc)
self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response
def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)
return Request(
request,
parsers=self.get_parsers(),
authenticators=self.get_authenticators(),
negotiator=self.get_content_negotiator(),
parser_context=parser_context
)
dispatch가 작동하고 난 후 initialize_request(request)에서 request를 초기화하는데 여기서 authenticators의 파라미터로 settings에 설정했던 DEFAULT_AUTHENTICATION_CLASS의 미들웨어가 실행된다.
난 simplejwt를 이용한 미들웨어 하나밖에 없다.
# rest_framework.views.APIView
def get_authenticators(self):
"""
Instantiates and returns the list of authenticators that this view can use.
"""
return [auth() for auth in self.authentication_classes]
만약 여러개라면 auth 미들웨어가 for문으로 하나씩 실행하게 된다.
auth 미들웨어에서 만약 user를 찾아내면 return 시킨다.
simpleJWT를 사용하고 있으므로, JWTAuthentication에서 user를 찾아낸다.
# rest_framework.request.Request._authenticate
def _authenticate(self):
"""
Attempt to authenticate the request using each authentication instance
in turn.
"""
for authenticator in self.authenticators:
try:
user_auth_tuple = authenticator.authenticate(self)
except exceptions.APIException:
self._not_authenticated()
raise
if user_auth_tuple is not None:
self._authenticator = authenticator
self.user, self.auth = user_auth_tuple
return
우선 header의 token을 얻어온다.
# rest_framework_simplejwt.authentication.JWTAuthentication
def authenticate(self, request):
header = self.get_header(request)
if header is None:
return None
raw_token = self.get_raw_token(header)
if raw_token is None:
return None
validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token
def authenticate_header(self, request):
return '{} realm="{}"'.format(
AUTH_HEADER_TYPES[0],
self.www_authenticate_realm,
)
def get_header(self, request):
"""
Extracts the header containing the JSON web token from the given
request.
"""
header = request.META.get(api_settings.AUTH_HEADER_NAME)
if isinstance(header, str):
# Work around django test client oddness
header = header.encode(HTTP_HEADER_ENCODING)
return header
마지막으로 get_user메서드에서 유저 정보를 리턴한다.
def get_user(self, validated_token):
"""
Attempts to find and return a user using the given validated token.
"""
try:
user_id = validated_token[api_settings.USER_ID_CLAIM]
except KeyError:
raise InvalidToken(_("Token contained no recognizable user identification"))
try:
user = self.user_model.objects.get(**{api_settings.USER_ID_FIELD: user_id})
except self.user_model.DoesNotExist:
raise AuthenticationFailed(_("User not found"), code="user_not_found")
if not user.is_active:
raise AuthenticationFailed(_("User is inactive"), code="user_inactive")
return user
중간부터는 혼자 찾다가 막혔었는데 아래 블로그에서 많은 도움을 얻었다!!