DRF ViewSet에서 put() 등의 함수를 선언하면서 Router를 이용하는 것에 대하여
#158 opened on Sep 13, 2020
Repository metrics
- Stars
- (27 stars)
- PR merge metrics
- (PR metrics pending)
Description
다음 글은 과제 1 수행 도중 DRF와 친해지며 제가 가진 의문과 그 해결 과정, 문제 제기를 포함하고 있습니다. 질문 내용만이 궁금하시다면 TL; DR로 이동하시면 됩니다. 또한 이 글은 단정적 어조로 작성되었으나 Django 입문자인 제가 작성했으므로 틀린 부분이 있을 확률이 매우 높다는 점을 밝힙니다. 오류를 발견하실 경우 아래 답변으로 지적해 주시면 감사하겠습니다.
Router
Router는 URL을 ViewSet의 각 함수에 연결해 주는 역할을 합니다.
예를 들어 과제 0에서 사용한 SimpleRouter는 {prefix}가 /api/v1/survey일 때
GET /api/v1/survey/를 list(self, request)에,
POST /api/v1/survey/를 create(self, request)에,
GET /api/v1/survey/{surveyresult_id}/를 retrieve(self, request, pk)에
연결해 주는 등 CRUD API를 작성할 때 일반적으로 필요한 URL Routing을 제공합니다.
put()의 사용
그런데 과제 1에서는 SimpleRouter의 Route나 DynamicRoute가 연결해 주지 않는 PUT /api/v1/user/의 개발을 요구하고 있고, 구현 방법 중 하나는 아래와 같이 put() 함수를 선언하는 것입니다.
# waffle_backend/user/views.py
class UserViewSet(viewsets.GenericViewSet):
(...중략...)
# PUT /api/v1/user/
def put(self, request):
if request.user.is_authenticated:
return Response(self.get_serializer(request.user).data)
else:
return Response(status=status.HTTP_403_FORBIDDEN)
저는 waffle_backend/user/urls.py에서 /api/v1/user 이하의 URL 설정을 SimpleRouter에 위임했기 때문에 SimpleRouter가 URL에 대한 single source of truth가 되어야 한다고 생각했고, SimpleRouter에는 PUT /api/v1/user/를 put(self, request)로 연결해주어야 한다는 설정이 없기 때문에 이 요청은 405 Method Not Allowed를 반환해야 한다고 생각했습니다.
그러나 실제로 실행해 보면 PUT /api/v1/user/는 put(self, request)로 연결되고 로그인 상태에 따라 200 OK 또는 403 Forbidden이 정상적으로 반환됩니다.
Router, ViewSet, APIView의 작동 방식
이로 인해 Router를 수정하지 않고 ViewSet에 함수(put)를 하나 생성하는 것이 어떻게 URL의 전달 방식에 영향을 줄 수 있을까? 라는 고민을 시작했고, viewsets.py 소스 코드 등에서 답을 찾을 수 있었습니다.
Django에서 하나의 URI는 하나의 View에 연결됩니다. 우리가 사용하는 ViewSet은 View들의 집합이고, Router는 이 View들의 집합에 UserViewSet.as_view(actions)를 사용해서 View라는 Callable을 만들게 됩니다. 이때 라우터가 actions 파라미터에 어떤 HTTP 메소드를 어떤 함수에 연결할 지 전달해 줍니다. 그런데 여기서 주목할 것은 request의 method에 관계없이 같은 URI는 같은 View에 전달된다는 사실입니다. 결국 Router는 각 URI가 어떤 View에 전달될지만 지정해줄 뿐, HTTP 메소드는 View(정확하게는 GenericViewSet의 BaseClass 중 하나인 APIView)에서 자체적으로 처리하는 것입니다.
APIView는 이를 dispatch(self, request, *args, **kwargs)라는 함수를 이용하여 처리합니다.
dispatch()는 request.method.lower()라는 이름의 함수가 View 내에 정의되어 있는지 확인하고, 있다면 그 함수에 request를 전달하고, 그런 이름의 함수가 없다면 405 Method Not Allowed를 반환합니다. 이로 인해 View가 put()이라는 함수가 정의된 것을 확인하고 request를 전달한 것입니다.
결국 viewset.as_view(actions)가 하는 일은 아래와 같이 Router에 정의된 action(예를 들어 GET을 list 함수에 연결하라)대로 함수를 self.get = self.list로 연결하는 것입니다.
# rest_framework/viewsets.py ViewSetMixin.as_view(cls, actions=None, **initkwargs) 중
for method, action in actions.items():
handler = getattr(self, action)
setattr(self, method, handler)
우려되는 부분
위의 내용을 바탕으로, 저는 ViewSet 내부에 put()이나 get()과 같은 HTTP 메소드 이름의 함수를 선언하는 것에 다음의 두 가지 문제가 있다고 생각합니다.
-
Router의 작동에 의해 GET 요청이 일어났음에도 get() 함수가 실행되지 않을 수 있습니다. 예를 들어 ViewSet에 get() 함수와 list() 함수를 모두 작성한 경우, as_view()의 작동에 의해 get()이 덮어 쓰이면서
GET {prefix}/요청을 받을 때 list() 함수가 실행됩니다. 이는 디버깅을 어렵게 할 수 있습니다. -
ViewSet에 선언한 put() 함수는 ViewSet이 연결되는 모든 URI에 대해 작동하여, 예상치 못한 결과를 낼 수 있습니다. 위의 put()의 사용처럼 SimpleRouter를 이용하는 상황에서
PUT /api/v1/user/를 구현하기 위해put(self, request)를 선언했다고 합시다. 여기까지는 별 문제가 없어 보입니다. 이후에 다른 유저의 정보를 조회하는 기능을 위해GET /api/v1/user/{username}/을def retrieve(self, request, pk=None)를 이용하여 개발했다고 합시다. 여기서PUT /api/v1/user/{username}/은 API Specification에 없으므로, 405 Method Not Allowed를 반환해야 할 것입니다. 하지만 이 요청은 위에서 선언한 put에 put(self, request, pk)의 형태로 전달됩니다. put은 두 개의 파라미터를 받는데 pk라는 이름의 파라미터가 추가로 전달되었으므로 500 Internal Error가 발생할 것이고,def put(self, request, **kwargs)으로 정의했다면 처리는 되겠지만 의도한 동작은 아닐 것입니다. 여기서PUT /api/v1/user/{username}/이 put()으로 전달되는 이유는 SimpleRouter가retrieve()가 선언된 것을 확인하고/api/v1/user/{lookup}/에 연결되는 View를UserViewSet.as_view(actions)로 생성하였고, View의 dispatch()가 클래스 내부에 put이 선언된 것을 확인하고 요청을 put()으로 전달한 것입니다. 하지만 retrieve()가 선언되지 않았다면 Router에서 /api/v1/user/{lookup}/을 처리하는 함수가 없다고 판단하고 해당 경로에 연결될 View를 만들지 않습니다. 따라서PUT /api/v1/user/{username}/은 404 Not Found를 반환할 것입니다. 이처럼 put()은 다른 함수(retrieve)의 선언에 따라 예상치 못한 접근 지점을 만들어낼 수 있습니다.
TL; DR
- ViewSet과 Router를 이용하면 기본적으로 Router에 정의된 대로 요청이 ViewSet의 함수로 전달됩니다.
- ViewSet에 HTTP Method 이름의 함수 put(), get() 등을 정의하면 Router에서 연결해 주는 URI이지만 Router에서 연결해 주는 Method가 아닐 경우 작동합니다.
질문
- ViewSet에서 put(), get() 등을 정의하는 것이 위와 같은 Complex Behavior를 야기하는데, ViewSet에서 put(), get()을 정의하는 것이 DRF에서 의도한 문법이거나 일반적으로 사용하는 문법인가요?
- 현업 개발에서도 ViewSet에 HTTP 메소드 이름의 함수를 작성하나요? 만약 그렇다면 개발자 모두가 put() 함수가 언제 작동하고 언제 작동하지 않는지 잘 알고 있어서 크게 문제가 되지 않는 것일까요?