Published on

DRF ViewSet에서 endpoint과 HTTP Method 별로 FilteSet 적용하는 방법

Authors

DRF 에서 Filtset 클래스로 필터를 만들면 여러모로 편리한 점이 많습니다.

프로젝트 도메인 특성에 따라, 특정 필드에 대해 유독 자주 사용되며 이 필터를 의무적으로 적용하게 끔 하는 방법도 있습니다.

FilterSet을 이용해 required=True 옵션을 적용하며, ViewSet에서 자동으로 적용되는 detail endpoint 에서 발생한 문제점과 해결방법을 기록합니다.

1. Filterset, Viewset 상황과 문제점

Filterset, field required option

class SomeDataFilter(django_filters.FilterSet):

    some_field = django_filters.NumberFilter(required=True,..생략..)


@extend_schema_view(
    list=extend_schema(summary="some_data 목록 조회"),
    destroy=extend_schema(summary="some_data 삭제"),
)
class SomeDataViewSet(
    mixins.ListModelMixin,
    mixins.DestroyModelMixin,
    GenericViewSet,
):
    queryset = SomeData.objects.all()
    serializer_class = SomeDataSerializer
    permission_classes = [SomeDataPermission]
    pagination_class = None
    filterset_class = SomeDataFilter

1.1 endpoint

위의 viewset에 따라 생성된 아래의 두 API 가 있습니다.


- [GET API] api.com/v1/some_data/
- [DELETE API] [api.com/v1/some_data/{pk](http://api.com/v1/some_data/{pk)}


FilterSet을 통해서 some_field에 상응하는 쿼리스트링을 입력받아, 필수값을 통해 필터링 된 데이터를 받아갈 수 있습니다.


- [GET API] [api.com/v1/some_data/?some_field](http://api.com/v1/some_data/?some_field)=some_data

1.2 이 필드는 필수 항목입니다 error

의도한 바에 따라 GET endpoint에 대해서, someField를 입력받지 않으면 "someField": ["이 필드는 필수 항목입니다."] 에러가 발생합니다.

이는 의도한 에러이며, 필수적으로 데이터를 입력받게끔 하는 것이라 클라이언트 단의 API 요청에 가이드를 내려보내주는 것과 동일합니다.

하지만, 의도치 않게 아래 Detail (pk나 기타 lookup 필드를 path parameter로 입력받는) endpoint에서 "someField": ["이 필드는 필수 항목입니다."] 에러가 발생합니다.


- [DELETE API] [api.com/v1/some_data/{pk}(http://api.com/v1/some_data/{pk)}

GET API 에서만 받았어야할 요구사항이 DELETE(detail) API 에서도 요구되는 것이고, 이는 잘못된 동작 방식입니다.

2. filter_queryset

이제 의도한 endpoint 에서만 filter 로직이 동작하도록 하는 방법을 알아보겠습니다.

filter_queryset 은 주어진 쿼리셋에 필터링을 적용해줍니다.

세팅에서 filter_backends 속성에 있는 필터들을 반복문을 돌며 필터를 수행 해주는 것입니다.

소스코드는 다음과 같습니다.


    def filter_queryset(self, queryset):
        """
        Given a queryset, filter it with whichever filter backend is in use.

        You are unlikely to want to override this method, although you may need
        to call it either from a list view, or from a custom `get_object`
        method if you want to apply the configured filtering backend to the
        default queryset.
        """
        for backend in list(self.filter_backends):
            queryset = backend().filter_queryset(self.request, queryset, self)
        return queryset

이 메서드 내부의 객체를 통해 분기처리하여 FilterSet의 기본 동작을 수행할 것 인지 말 것인지, 커스터마이징 합니다.

View단에서 queryset에 필터를 적용하는 역할을 하는 filter_queryset 을 오버라이드 하는데, 비즈니스 로직에 따라 HTTP method 혹은 개별 endpoint 에 적용할 것인지 코드를 작성합니다.

2.1 request.method

self.request 객체를 가져와 이 안에 있는 method 를 통해서 GET, POST,… 과 같은 HTTP method 를 가져 올 수 있습니다.

DELETE 의 경우 필터를 하지 않게 하려면 아래처럼 특정 HTTP method 에서는 필터를 안하게 할 수 있습니다.

# views.py

    def filter_queryset(self, queryset):
        if self.request.method == "DELETE":
            return queryset
        return super().filter_queryset(queryset)

2.2 action_map

Viewset에서 엔드포인트와 HTTP Method 별로 FilterSet(혹은 기타로직)을 구분하기 좋은 방법입니다.

문서에서 권장하는 방법도 아니고 레퍼런스가 있지는 않지만, ViewSet 소스코드를 통해 특정 ViewSet에서 FilterSet을 세밀하게 컨트롤 하기위해서 사용하면 좋을 것 같습니다.

(이를 응용하면, serializer class 나 action 의 커스텀에서도 세밀하게 컨트롤 할 수 있을 듯 합니다.)

예를 들어, GET method 의 retrieve 를 추가한다고 하면, 위의 DELETE 상황처럼 "someField": ["이 필드는 필수 항목입니다."] 에러가 발생할 것 입니다.

self.action_map 은 아래처럼 HTTP Method와 action name을 dict 형태로 가져올 수 있습니다.


{"get": "list",} 이나 {"get": "retrieve",}

처럼 메서드와 endpoint 액션네임을 가져올 수 있습니다.

# views.py

    def filter_queryset(self, queryset):
        if self.action_map.get("get") == "list":
            return super().filter_queryset(queryset)
        return queryset

결론

DRF의 Swagger Schema에 따라 OpenAPI 파라미터를 적용해서 endpoint 를 설계할 수도 있습니다.

혹은 아예 FilterSet 클래스를 따로 만들거나, required 옵션을 주지 않는 등의 차선책도 있습니다.

하지만, filter_queryset 을 커스텀함으로써 필터를 맵핑하는 관리포인트가 적어지고, endpoint 별 queryset을 공통으로 묶거나 분기하는 등의 장점을 가져갈 수 있습니다.

hongreat 블로그의 글을 봐주셔서 감사합니다! 하단의 버튼을 누르시면 댓글을 달거나 보실 수 있습니다.

Buy Me A Coffee