콘텐츠로 이동

Resolver 및 필드 포맷팅#

2024 iThome 철인 대회

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

이전 글에서 API 응답은 종종 Django Model 객체 내용에 대한 필터링과 가공을 거쳐 JSON으로 직렬화된다고 이야기했어요.

그중 "가공" 단계는 더 전문적인 용어로 표현하자면 "데이터 포맷팅(Data Formatting)"에 가깝다고 할 수 있어요. 일정한 규칙에 따라 출력 데이터를 어떠한 형태로 변환하거나 재구성하여, 특정한 출력 형식에 맞추는 작업이죠.

데이터 포맷팅에는 여러 종류가 있어요. 예를 들면:

  1. 시간 형식 변환: 데이터베이스에 저장된 타임스탬프(timestamp)를 사람이 더 읽기 쉬운 형태로 바꿔줘요.
  2. 수치 변환: 숫자를 화폐 단위로 변환하거나, 소수점 자릿수를 반올림해요.
  3. 문자열 처리: 길이가 너무 긴 텍스트를 자르거나, 통일된 접두어(prefix)를 덧붙여요.

이유가 무엇이든 간에, 대부분의 경우 데이터의 "가독성"을 높이거나 특정 비즈니스 규칙에 맞추기 위해 사용돼요.

이처럼 데이터 포맷팅에 대한 요구는 실무에서 중요할 뿐만 아니라 API 개발에서도 무척 흔하게 발생하기 때문에, 이번 글 하나를 통째로 할애하여 꼼꼼하게 다루어 볼 만한 가치가 충분해요.

이번 글의 모든 코드 변경 사항은 이 PR에서 확인하실 수 있어요.

GitHub 예제 프로젝트#

👉 Django-Ninja-Tutorial


상황과 요구사항#

다시 한번 "단일 글 정보 가져오기" API로 돌아가 볼까요. 현재 응답 형식은 다음과 같아요:

// http://127.0.0.1:8000/posts/2/
{
    "id": 2,
    "title": "Alice's Django Ninja Post 1",
    "content": "Alice's Django Ninja Post 1 content",
    "author": {
        "id": 1,
        "username": "Alice",
        "email": "[email protected]"
    },
    "created_at": "2024-09-12T02:28:16.801Z",
    "updated_at": "2024-09-12T02:28:16.801Z"
}

우리는 시간 문자열을 단순화하여, "2024-09-12T02:28:16Z" 형태를 취하기로 결정했어요.

이전 버전과 비교해보면, 단순히 ".801"이라는 소수점 이하 부분만 사라졌을 뿐, 여전히 ISO 8601 표준을 완벽히 따르고 있죠.

결론적으로, 응답 내용 중 created_atupdated_at 두 필드는 형식의 변환이 필요해요. 이게 바로 앞에서 언급한 "데이터 포맷팅"이에요.


Django REST framework의 방식#

이번에도 어김없이 Django REST framework(이하 DRF)의 방식을 먼저 살펴볼게요. 이 둘의 차이점을 비교해보면, 사실상 거의 비슷하다는 걸 깨닫게 될 거예요.

DRF에서는 SerializerMethodField를 통해 시간 형식의 변환을 구현할 수 있어요. 다음은 DRF로 작성된 예제예요:

class PostSerializer(serializers.ModelSerializer):
    ...
    created_at = serializers.SerializerMethodField()
    updated_at = serializers.SerializerMethodField()

    def get_created_at(self, obj):
        return obj.created_at.strftime('%Y-%m-%dT%H:%M:%SZ')

    def get_updated_at(self, obj):
        return obj.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')

여기서 주목해야 할 핵심 세 가지는 다음과 같아요:

  1. 포맷팅 하려는 필드의 값은 반드시 SerializerMethodField여야 해요.
  2. 직렬화기(Serializer) 클래스 내부에 필드 이름과 동일하게 맞춰진 인스턴스 메서드 (첫 번째 매개변수가 self인)를 정의해야 하며, 이름 앞에 get_을 붙여야 해요. 예를 들면 get_created_at 처럼요.
  3. obj 매개변수는 현재 직렬화 중인 객체를 가리켜요. 이 예제에서는 전달되는 인자가 Post 모델의 인스턴스일 것으로 예상할 수 있죠. 이 메서드는 직렬화 과정에서 자동으로 호출되어, 원본 datetime 객체를 우리가 지정한 문자열 형식으로 바꿔줘요.

참고로, DRF 시리얼라이저의 여러 인스턴스 메서드들에서 obj라는 매개변수 이름을 사용하는 것은 일종의 네이밍 컨벤션이라고 할 수 있어요.


Django Ninja에서의 필드 데이터 포맷팅#

DRF의 방식을 살펴보았으니, 이제 Django Ninja는 어떻게 처리하는지 들여다볼 차례예요.

Django Ninja의 Resolver 메서드를 이용하면 이와 같은 요구사항을 똑같이 손쉽게 해결할 수 있답니다.

Django Ninja의 Resolver 메서드#

Django Ninja에서는 Resolver 메서드를 사용해 동일한 기능을 구현해요:

class PostResponse(Schema):
    ...
    created_at: datetime
    updated_at: datetime

    @staticmethod
    def resolve_created_at(obj: Post) -> str:
        return obj.created_at.strftime('%Y-%m-%dT%H:%M:%SZ')

    def resolve_updated_at(self, obj: Post) -> str:
        return obj.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')

메서드 이름 짓는 법을 보면, DRF가 get_ 접두사를 쓰는 반면 Django Ninja는 resolve_ 접두사를 사용해요.

또한, 잘못 보신 게 아니에요. 여기서는 두 가지 방식을 함께 보여드리고 있어요:

  • resolve_created_at은 "정적 메서드(static method)"로, @staticmethod 데코레이터를 사용하며 self 매개변수가 없어요.
  • resolve_updated_at은 전형적인 인스턴스 메서드로, self 매개변수를 가지고 있어요.

공식 문서의 예제에서도 실제로 이 두 가지 방식이 함께 소개되고 있거든요:

class TaskSchema(Schema):
    ...
    owner: Optional[str] = None
    lower_title: str

    @staticmethod
    def resolve_owner(obj):
        if not obj.owner:
            return
        return f"{obj.owner.first_name} {obj.owner.last_name}"

    def resolve_lower_title(self, obj):
        return self.title.lower()

인스턴스 메서드 버전은 아직 구현되지 않음#

하지만 잠깐! 현 단계에서는 오직 "정적 메서드" 방식만 알아두시면 돼요.

왜냐하면 두 번째 방식을 적용해보면 다음과 같은 에러 메시지를 마주하게 될 테니까요:

Error extracting attribute: NotImplementedError: Non static resolves are not supported yet [type=get_attribute_error, input_value=>, input_type=DjangoGetter]

네? 아직 구현이 안 됐다고요!

어쩔 수 없죠. 얌전히 모두 정적 메서드 방식으로 바꿔 쓰기로 해요.

반환 결과#

마지막으로 어떻게 바뀌었는지 결과를 살펴볼까요:

// http://127.0.0.1:8000/posts/2/
{
    "id": 2,
    "title": "Alice's Django Ninja Post 1",
    "content": "Alice's Django Ninja Post 1 content",
    "author": {
        "id": 1,
        "username": "Alice",
        "email": "[email protected]"
    },
    "created_at": "2024-09-12T02:28:16Z",
    "updated_at": "2024-09-12T02:28:16Z"
}

아주 좋네요. 시간 문자열 형식이 성공적으로 변환되었어요 — 16.801Z가 아니라 깔끔하게 16Z로 표시되고 있죠.


Alias를 사용한 필드 정보 평탄화 (Flattening)#

포맷팅과 관련된 또 다른 자주 만나는 요구사항은 14편에서 다루었던 복잡한 데이터 구조를 "평탄화(flatten)"하는 작업이에요.

이것은 데이터의 "재구성"이라 할 수 있는데, 구조의 재구성 역시 이번 글에서 논의하는 데이터 포맷팅의 범주에 들어간답니다.

14편에서 @property를 활용하여 "글 목록 가져오기"의 응답으로 author_name 필드의 내용을 만들어 냈던 것, 기억나시나요? — 이것이 바로 User 모델을 평탄화하여 그 안의 username 필드 정보를 직접 가져온 것이었죠.

이번에는 그것보다 훨씬 더 우아한 해결책인 alias를 소개해 드릴게요.

Alias 사용하기#

Django Ninja는 (거의 Pydantic에서 그대로 가져온 것이긴 하지만) 이 기능을 구현하기 위해 Fieldalias 매개변수를 제공하고 있어요.

Field와 관련된 더 깊은 이야기는 〈18편: Pydantic Field 설정 예시 및 기본값〉에서 자세히 다룰 예정이에요.

우선은 이를 어떻게 사용하는지 볼까요:

class PostListResponse(Schema):
    id: int
    title: str
    created_at: datetime
    author_name: str = Field(alias='author.username')

주의할 점이 있어요! Post 모델에 추가했던 기존의 @property 메서드는 지워주거나, 최소한 author_name이라는 이름과 겹치지 않게 만들어 주어야 해요. 안 그러면 에러가 발생하니까요!

저는 기존 @property 메서드를 아예 지워버리고 이 새로운 방법을 택했어요.

핵심 포인트 분석#

alias=author.username을 통해 Post관계 모델Userusername 속성값을 바로 가져왔어요. 완벽하게 중첩 데이터의 평탄화를 이루어 낸 거죠.

이러한 디자인은 의심할 여지 없이 DRF의 훌륭한 점을 잘 차용한 것으로, DRF에서 쓰이는 source=author.username 문법과 그 맥락을 같이 해요.

조금 추상적으로 다가올 수도 있지만, 굉장히 우아한 방식이죠.

alias의 쓰임새는 데이터 평탄화에만 그치지 않아요 (오히려 데이터 평탄화는 조금 더 심화된 활용법에 가깝죠). 필드 이름 치환 등 다른 세세한 활용법에 대해서는 Pydantic 공식 문서를 직접 참고해 보시길 권해 드려요.

반환 결과#

이 방법은 이전에 @property를 사용했을 때와 완벽하게 똑같은 결과를 보여줘요:

// http://127.0.0.1:8000/posts/
[
    {
        "id": 1,
        "title": "Alice's Django Ninja Post 1",
        "created_at": "2024-09-12T02:28:16.801Z",
        "author_name": "Alice"  // 평탄화된 작성자의 이름
    }
]
author_name 필드가 성공적으로 평탄화되어 작성자의 이름이 곧바로 표시되는 것을 확인할 수 있어요.


맺음말#

Django Ninja의 Resolver 메서드를 사용하면 API 응답의 필드 데이터를 동적으로 처리할 수 있어, 다양한 형식 변환과 커스텀 요구사항들을 거뜬히 만족시킬 수 있어요.

created_at이나 updated_at 같은 시간 필드를 다룰 때, Resolver 메서드는 사용하기 간편할 뿐만 아니라 코드의 구조를 깔끔하게 유지할 수 있도록 도와주죠.

Fieldalias 매개변수는 또 다른 흔한 데이터 포맷팅 작업인 "평탄화"를 더욱 우아하게 실현해 내요. API 응답을 간결하게 만들어 주면서도 그 바탕이 되는 Django 모델은 전혀 수정할 필요가 없게 만들어 주니까요.

우리는 이러한 방법들을 통해 API 출력을 훨씬 더 유연하게 통제하고, 클라이언트의 요구에 완벽히 부응할 수 있게 되었어요.

다음 장 예고#

이것으로 "Django Ninja의 HTTP 응답 처리"를 다룬 총 4편의 글이 끝나고, 제3장도 공식적으로 막을 내리게 되었어요. 다음 장에서는 API 개발에 있어 또 하나의 핵심 주제인 문서화(Documentation)에 시선을 옮겨볼까 해요!

프로젝트의 규모가 커짐에 따라 명확한 API 문서는 API를 사용해야 하는 모든 사람에게 결정적으로 중요해져요 — 심지어 백엔드 개발자 본인에게조차도 말이죠!

훌륭한 API 문서는 의사소통 비용을 크게 낮추고, 개발 효율을 끌어올리며, 오류 발생률을 줄일 수 있어요. 그것은 단순한 기술 문서를 넘어서 팀 협업을 위한 중요한 구심점 역할을 한답니다.

제4장에서는 Django Ninja 코드를 통해 어떻게 고품질의 API 문서를 효율적으로 자동 생성해 내어, 전반적인 개발 경험을 한 단계 더 도약시킬 수 있는지 그 방법을 깊이 있게 탐구해 볼게요.