문제 상황
새로운 프로젝트를 진행하던 중, 회사의 레거시 프로젝트를 살펴볼 기회가 있었다.
기존 방식대로 문제를 해결할 수도 있었지만, 개인적으로는 View에 모든 비즈니스 로직이 집중되면 가독성과 유지보수성이 떨어진다고 판단했다.
따라서 Django의 filter를 활용하여 파라미터별로 필요한 조건을 동적으로 적용할 수 있도록 구현하는 것이 더 적절하다고 결정했다.
사실 위의 내용들은 각각 쿼리 파라미터의 값들에 맞춰서 적절하게 조건이 나눠지면 된다.
해결 방안
from django_filters.rest_framework import filterset, filters
class UserFilter(filterset.FilterSet):
locations = filters.CharFilter(method="filter_locations", label="Locations")
category = filters.CharFilter(field_name="categories__code", lookup_expr="exact", label="Category")
keyword = filters.CharFilter(method="filter_keyword", label="Keyword")
relation = filters.ChoiceFilter(
choices=[('FOLLOW', 'FOLLOW'), ('FRIEND', 'FRIEND')],
method="filter_relation",
label="Relation",
)
ordering = filters.ChoiceFilter(
choices = [('NEWEST', 'NEWEST'), ('POPULARITY', 'POPULARITY')],
method="filter_ordering",
label="Ordering",
)
class Meta:
model = Product
fields = ['locations', 'targetUser', 'category', 'keyword', 'relation', 'ordering']
Query Parameter로 받을 값을 선언하고, 각각의 타입에 맞게 정의해준다.
이 방식은 DRF의 Serializer와 유사한 사용 패턴을 가진다.
- method: 사용자 정의 메서드를 호출하여 동작하며, 필터링 로직을 직접 작성할 수 있다.
- lookup_expr: 기본값으로 exact을 사용하면, 사용자가 전달한 값과 정확히 일치하는 데이터만 반환된다.
- choices: 미리 정의된 choices 값만 허용하여 필터링을 수행한다.
- 참고로, category는 Product 모델과 다대다(Many-to-Many) 관계로 연결되어 있으며, 카테고리의 code 필드를 기준으로 필터링할 계획이다.
위와 같은 설정을 적용하면, 기존 get_queryset에 있던 비즈니스 로직을 Filter로 분리할 수 있어 View가 더 깔끔하고 유지보수하기 쉬워진다.
예시 코드
아래 예제는 filter_ordering을 활용하여 필터링하는 방법을 보여준다.
특히, 생성 시간이나 이름과 같은 모델 내부 필드가 아닌, 보다 복잡한 로직이 필요한 경우에는 method를 활용하여 처리할 수 있다.
위 코드에서 NEWEST(최신순)는 생성 시간(created)을 기준으로 정렬하지만, POPULARITY(인기순)는 추가적인 연산이 필요했기 때문에 Model의 Manager에서 처리하도록 레이어를 분리했다.
- 사실, 필터 내부에서 처리해도 무방하지만, 유지보수성과 코드의 명확성을 고려하여 Manager로 분리하는 것이 더 적절하다고 판단했다
리팩토링이 완료된 코드는 아래와 같다.
view.py
class ProductViewSet(
viewsets.GenericViewSet,
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.DestroyModelMixin,
mixins.UpdateModelMixin,
mixins.RetrieveModelMixin,
):
queryset = Product.objects.all()
serializer_class = LinkSerializer
permission_classes = [IsRegistered]
pagination_class = MyPageCursorPagination
filterset_class = ProductFilter
def get_queryset(self):
user = self.request.user
return self.queryset.filter(user=user)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
내부 로직 분석
위와 같이 복잡한 로직이 몇 줄로 간결하게 처리되는 원리가 궁금해서 내부 동작을 살펴보았다.
일단, self.filter_queryset(self.get_queryset())을 보면:
- get_queryset() → View에서 선언한 get_queryset()이 실행된다.
- filter_queryset() → 그다음 filter_queryset()이 실행되면서, filterset_class에 설정된 필터가 적용된다.
여기서 궁금한 점은, filter_queryset()은 어디에서 호출되는 걸까?
이를 확인하기 위해 내부 구현을 더 깊이 살펴보았다.
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
DjangoFilterBackend의 역할
아래는 DjangoFilterBackend의 내부 코드이다.
class DjangoFilterBackend:
def filter_queryset(self, request, queryset, view):
filterset = self.get_filterset(request, queryset, view)
if not filterset:
return queryset
return filterset.qs
def get_filterset(self, request, queryset, view):
filterset_class = self.get_filterset_class(view, queryset)
if filterset_class is None:
return None
return filterset_class(request.GET, queryset=queryset, request=request)
def get_filterset_class(self, view, queryset):
return getattr(view, 'filterset_class', None)
위 코드를 보면, filter_queryset()이 결국 filterset_class를 가져와 필터를 적용하는 역할을 한다.
즉, View에서 선언한 **filterset_class = ProductFilter**가 자동으로 filter_queryset()에 의해 적용되는 것이다.
마무리
View에서 직접 query_params를 가져와 조건문을 활용해 필터링하는 방법도 있지만,
이 경우 View에 로직이 집중되면서 유지보수성이 떨어질 가능성이 크다.
개인적으로는 각 레이어를 분리하여 책임을 분산시키는 구조를 선호한다.
따라서, 공통적으로 적용해야 하는 쿼리는 get_queryset()에서 처리하고,
파라미터에 따라 필터링 조건이 달라지는 경우에는 Filter를 활용하는 것이 더 적절하다고 생각한다.
'프로그래밍 > Django' 카테고리의 다른 글
하나? 여러 개? 대량? 😵 DRF Serializer 활용법 총정리! (0) | 2025.02.24 |
---|---|
Django ORM 최적화: GenericForeignKey를 활용한 데이터 모델링 (0) | 2025.02.19 |
Django 대량의 list 요청시 Cursor-based Pagination (0) | 2025.02.18 |
[Django]filter_fields로 URL 파라미터 사용하기 (0) | 2022.05.13 |
[Django] 멀티 DB 라우터설정 및 연동하기 (0) | 2022.05.11 |