콘텐츠로 이동

FilterSchema 소개#

Django Ninja 입문 가이드

Django Ninja 입문 가이드의 26번째 글이에요.

"조회(Query)"는 API에서 자주 요구되는 부가 기능으로, 본질적으로는 데이터의 필터링(filtering)과 선별을 의미해요.

게시글이나 상품을 필터링하든 사용자를 조회하든, 다양한 조건에 따라 데이터를 걸러내어 결과를 얻는 것은 대부분의 프로젝트에서 필수적인 기능이라고 할 수 있죠.

뷰 함수에서 조회를 구현하는 가장 간단한 방법은 Django ORM의 필터링 메서드를 사용하는 거예요. 예를 들어, filter 메서드를 사용해 특정 조건에 따라 QuerySet을 필터링할 수 있죠.

이 방법은 단순하고 직관적이어서 기본적인 조회 요구사항에 적합해요. 하지만 한계도 있어요. 필드와 요구사항이 늘어날수록 조회 조건이 점점 더 복잡해지고, 결과적으로 코드가 길어지고 유지보수하기 어려워질 수 있거든요.

이러한 문제를 해결하기 위해 Django Ninja는 FilterSchema를 제공하여 더 "구조화된" 방식으로 조회 조건을 정의하고 관리할 수 있게 해줘요.

이번 글에서는 FilterSchema를 소개하고, 한 단계씩 구현하고 설명하면서 Django Ninja에서 FilterSchema를 사용하여 더욱 유연하고 모듈화된 API 조회 기능을 구현하는 방법을 알려드릴게요.

이번 글의 모든 코드 변경 사항은 이 PR을 참고해 주세요.

GitHub 예제 프로젝트#

👉 Django-Ninja-Tutorial


기존 조회 방법과 문제점#

이전 글에서 언급했던 "게시글 목록 조회" API 기억하시나요? <11편: 요청(3) 쿼리 매개변수 - Query Parameters>에서 추가했던 "게시글 제목으로 조회" 기능 말이에요.

현재 코드는 이래요: (쿼리 매개변수 이름인 title에 주목해 주세요)

...
def get_posts(
    request: HttpRequest,
    title: None | str = Query(None, min_length=2, max_length=10),
) -> QuerySet[Post]:
    """
    게시글 목록 조회
    """
    posts = Post.objects.all()
    if title:
        posts = posts.filter(
            title__icontains=title).select_related('author')
    return posts

그때 우리는 Django ORM의 filter 메서드를 사용했어요!

posts.filter(title__icontains=title).select_related('author')

끝에 있는 select_related('author')는 "N+1" 문제를 피하기 위한 것으로, 조회 로직과는 관계가 없으니 일단 무시하셔도 돼요.

잠재적인 문제#

이러한 작성 방식은 매우 직관적이어서 간단한 조회 요구사항에는 아주 효과적이에요.

하지만 프로젝트 규모가 커지고 조회 요구사항이 복잡해지면 다음과 같은 어려움이 생겨요:

  • 코드 중복: 비슷한 필터링을 여러 곳에서 수행해야 할 때, 동일한 필터링 로직이 반복되는 것을 보게 될 수 있어요.
  • 유지보수의 어려움: 필터링 조건이 추가될수록 뷰 함수는 유지보수하기 힘들어져요. 새로운 필터링 조건을 하나 추가할 때마다 여러 곳의 코드를 수정해야 할 수도 있거든요.
  • 데이터 검증 및 변환: 데이터 검증과 변환 문제를 수동으로 처리해야 하므로, 오류 가능성이 높아질 뿐만 아니라 개발 복잡도도 증가해요.
  • 확장성: 범위 조회나 다중 조건 조합 조회 등 더 복잡한 필터링 조건을 지원해야 할 때, 수동으로 ORM 조회를 조합하는 방법은 매우 번거롭고 관리하기 어려울 수 있어요.

따라서 복잡한 조회 상황에서는 ORM의 filter 메서드를 직접 사용하여 조회 조건을 조합하는 것을 권장하지 않아요.


새로운 요구사항: 작성자 이름 동시 조회#

새로운 요구사항은 하나의 동일한 키워드게시글의 제목이나 작성자의 이름을 동시에 조회하고, 둘 중 하나라도 일치하면 결과에 표시하는 거예요. (둘 중 하나만 만족해도 되고, 둘 다 만족해도 돼요)

이 요구사항은 iThome 철인 대회 공식 웹사이트의 이 조회 기능과 비슷해요:

우리는 2가지만 조회할 수 있지만, 저 웹사이트는 주제, 요약, 참가자 닉네임의 3가지를 동시에 조회할 수 있죠.

하지만 본질적으로는 같아요.

기존 방법 + Q 객체를 이용한 구현#

기존 방법과 Django의 Q 객체를 사용하여 구현한다면 조회 코드는 다음과 같을 거예요:

from django.db.models import Q
...

posts = posts.filter(
    Q(title__icontains=title) | Q(author__name__icontains=title)
).select_related('author')

이 경우 쿼리 매개변수 이름을 title로 하는 것은 더 이상 적절하지 않아요. 두 개의 필드를 조회해야 하니까요. 괜찮아요, 나중에 query로 바꿀 거니까요.

이 코드에는 두 가지 핵심이 있어요:

  1. 조회 코드가 확실히 길어졌어요! 나중에 새로운 조회 조건이 또 추가된다면 얼마나 더 길어질까요...
  2. Q는 대체 뭐죠?

Q는 Django ORM의 Q 객체로, 복잡한 조회 로직에서 중요한 역할을 해요. 따라서 먼저 간단히 소개해 드릴 필요가 있어요.


Django Q 객체 소개#

다중 조건 조회 시 프로그램 구조가 복잡해지는 문제를 개선하기 위해 Django는 Q 객체를 제공해요.

Q 객체는 논리 연산자(&, | 등)를 사용하여 조건을 결합함으로써 조회 조건을 유연하게 구성할 수 있게 해줘요. 복잡한 조건 필터링을 처리할 때 매우 유용하죠.

예를 들어, 제목에 'Ninja'가 포함되어 있고 작성자 이름에 'Alice'가 포함된 게시글을 필터링하려면 다음과 같이 작성할 수 있어요:

posts = posts.filter(
    Q(title__icontains='Ninja') & Q(author__name__icontains='Alice')
)

위의 코드는 우리가 흔히 보는 다음과 동일해요:

posts = posts.filter(
    title__icontains='Ninja', author__name__icontains='Alice'
)

그래서 대개 "AND" 요구사항일 때는 Q 객체를 잘 사용하지 않아요.

"OR" 조회 조건이야말로 Q전형적인 활용 사례예요.

이제 조건을 바꿔서, 게시글 제목 '또는' 작성자 이름에 'Alice'가 포함되어 있으면 된다고 해보죠. | 연산자를 사용할 수 있어요:

posts = posts.filter(
    Q(title__icontains='Ninja') | Q(author__name__icontains='Alice')
)

Q 객체를 사용하면 조회가 더욱 유연하고 명확해져요. 특히 여러 선택 가능한 조건이 주어질 때 진가를 발휘하죠.


FilterSchema를 사용하여 조회 개선하기#

기존 조회 방법이 어떻게 코드를 길어지게 하는지 이해하고 Q 객체의 기초를 배웠으니, 오늘 주인공인 FilterSchema를 소개할 차례예요.

Django Ninja가 제공하는 FilterSchema의 주요 기능은 조회 구문을 보다 구조화되고 모듈화되게 만들어서 뷰 함수가 너무 길어지거나 읽기 어려워지는 것을 방지하는 거예요.

게다가 Schema의 검증 방법과 마찬가지로, 조회 로직을 뷰 함수에서 분리해 냄으로써 어느 정도 "관심사의 분리(Separation of Concerns)" 원칙을 실현하기도 해요.

하지만 서두르지 않고, 코드를 단계적으로 개선해 나가 볼게요.

약간 둔해 보일 수도 있지만, 이렇게 하면 FilterSchema와 복잡한 조회의 구현에 대해 더 깊이 이해하실 수 있을 거예요.

첫 번째 버전 "개선"#

먼저 FilterSchema를 사용하여 앞서 말한 "새로운 요구사항: 작성자 이름 동시 조회"를 구현해 볼게요.

schemas.py에 새로운 Schema를 만드는데, 이번에는 FilterSchema예요:

# post/schemas.py
from ninja import Field, FilterSchema, Schema
...

class PostFilterSchema(FilterSchema):
    query: str | None = Field(None, min_length=2, max_length=10)

이 FilterSchema는 사실 "쿼리 매개변수(query parameters)"를 위해 사용하는 거예요. 따라서 필드(속성) 이름은 클라이언트가 사용해야 한다고 생각하는 쿼리 매개변수 이름이 돼요.

"게시글 제목"과 "작성자 이름"을 동시에 조회해야 하므로 이름을 query로 지었어요.

다음으로, 이 Schema를 뷰 함수에서 사용해 볼게요:

@router.get(...)
@paginate(CustomPagination)
def get_posts(
    request: HttpRequest,
    filters: PostFilterSchema = Query(),  # FilterSchema 사용
) -> QuerySet[Post]:
    """
    게시글 목록 조회
    """
    posts = Post.objects.all()
    if filters.query:
        q = Q(title__icontains=filters.query) | \
            Q(author__username__icontains=filters.query)
        posts = posts.filter(q)
    return posts

PS: 여기에 있는 프로젝트 예제 코드에는 오류가 있어요. 두 번째 Q 조회가 content__icontains로 잘못 쓰여 있으니 주의해 주세요. 이 부분은 다음 브랜치에서 수정해 두었어요.

이 새로운 뷰 함수를 보고 나면 속으로 이런 생각이 드실지도 몰라요:

장난하는 건가? 하나도 안 간단해졌잖아!

맞아요. 왜냐하면 이건 FilterSchema의 "미완성작"일 뿐이거든요. 그래서 사용하지 않았을 때보다 오히려 더 길어 보이죠.

그럼에도 불구하고 이 코드에는 우리가 알아두어야 할 주목할 만한 점들이 있어요.


핵심 분석#

q = Q(title__icontains=filters.query) | \
    Q(content__icontains=filters.query)
posts = posts.filter(q)

이 부분에서 알 수 있듯이, Q 객체는 개별적으로 다양한 결합 연산을 수행한 후 마지막에 Django의 filter 메서드에 매개변수로 전달될 수 있어요.

Django Ninja에서 흔히 쓰이는 구문#

이번 예제에서 사용된 이와 같은 view 함수 매개변수 "구문"은 Django Ninja에서 매우 흔하게 쓰여요:

filters: PostFilterSchema = Query()

초보자는 이걸 보고 쉽게 "오해"할 수 있어요.

왜일까요? 아마도 filters의 타입이 PostFilterSchema이고(이건 맞아요), 그것의 기본값Query()라고 생각할 수 있기 때문이에요. Python 함수는 보통 그렇게 정의되니까요.

하지만 아니에요.

Query()filters 매개변수의 기본값이 아니에요. 만약 그렇다면 타입 힌트가 Query여야 하지 않을까요?

사실 = Query()라는 표시는 여러분을 위한 것이 아니라 Django Ninja를 위한 것이며, Django Ninja에게 다음과 같이 말해주는 것과 같아요:

이 매개변수의 내용은 body나 path가 아니라 HTTP 요청의 쿼리 매개변수(query parameters)에서 가져와야 해.

이렇게 생각하면 훨씬 이해하기 쉬워요.

Django Ninja는 쿼리 매개변수에서 문자열을 가져와서(여러 개가 있다면 분해하여) 하나씩 PostFilterSchema에 전달하여 초기화 및 검증을 수행해요:

  1. 검증 실패: 422 에러 반환.
  2. 검증 성공: Schema 객체를 뷰 함수의 함수 매개변수(지역 변수)로 전달.

조회 결과#

'Alice'를 키워드로 사용한 조회 결과는 다음과 같아요:

총 30개의 게시글이 조회되었으며 모두 작성자 이름에 'Alice'가 포함된 사용자들의 글이에요.


요약 및 다음 단계#

이번 글은 여기까지예요. 우리는 Q 객체와 FilterSchema라는 두 가지 새로운 개념을 접했어요.

그리고 프레임워크의 사용 방식과 관례를 이해하는 데 매우 중요한 Django Ninja의 흔한 뷰 함수 매개변수 "구문"도 분석해 보았죠.

이러한 개념들을 소화하는 데 시간이 조금 걸리겠지만, 이렇게 차근차근 나아가는 것이 가치가 있다고 확신해요.

FilterSchema의 고급 사용법으로 바로 뛰어드는 것보다 이렇게 순서대로 학습하는 것이 이해하는 데 훨씬 도움이 될 거예요.

다음 글에서는 Q 객체를 아는 것이 왜 중요한지, 그리고 FilterSchema를 사용하여 "관심사의 분리" 원칙에 맞는 구조화된 다중 필드 조회를 구축하는 방법을 확인해 볼게요.

다음 편에서 만나요!