콘텐츠로 이동

교차 필드 검증#


2024 iThome 철인 대회

이 글은 Django Ninja 시리즈 튜토리얼의 20번째 글이에요.

이전 글에서는 단일 필드의 커스텀 검증에 대해 이야기했고, 이번 글에서는 필드 간 교차(다중 필드) 검증에 대해 논의할 거예요.

교차 필드 검증 역시 API 개발에서 매우 흔한 요구 사항이에요. 예를 들어, 계정을 등록할 때 「비밀번호」와 「비밀번호 확인」 두 필드의 내용이 동일함을 보장해야 하거나, 날짜 기간을 선택할 때 시작 날짜가 종료 날짜보다 늦어서는 안 되는 등이죠.

이러한 검증 시나리오는 단일 필드 검증을 통해서는 구현할 수 없어요. 전체 데이터의 일관성과 정확성을 보장하기 위해 여러 필드 간의 논리적 연관성을 동시에 검사해야 하기 때문이에요.

이 글에서는 Pydantic을 통해 교차 필드 검증 요구 사항을 구현하는 방법을 소개할 거예요——「비밀번호 확인」을 예로 들어 이 기능의 실제 적용을 보여드릴게요.

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

GitHub 예제 프로젝트#

👉 Django-Ninja-Tutorial


교차 필드 검증과 관심사 분리#

사실, 단일 필드이든 교차 필드이든 커스텀 검증은 반드시 Pydantic을 통해 완료해야 하는 것은 아니에요.

이론적으로 데이터 검증은 뷰 함수 내에서 직접 수행될 수 있어요. 예를 들어 입력된 필드 값을 꺼내 그 유효성을 수동으로 검증하는 식이죠. 교차 필드 검증도 마찬가지고요.

그러나 이것은 편리하긴 하지만 「거친(조잡한)」 방식이에요——검증 로직이 매우 단순한 상황에만 적합하거든요.

Pydantic을 통해 데이터 검증을 진행하면 명백한 장점을 하나 가져올 수 있어요: 바로 관심사 분리예요.

관심사 분리#

관심사 분리 (Separation of Concerns)는 일종의 설계 원칙이에요. 프로그램 내의 각기 다른 기능의 책임을 독립된 모듈이나 계층으로 나눌 것을 주장하죠.

각 모듈은 주로 하나의 구체적인 방향이나 목표에 집중하여, 여러 다른 기능들이 함께 결합되는 것을 방지해요. 이러한 분할은 프로그램을 더 테스트하기 쉽고, 유지 관리 및 확장하기 쉽게 만들어 줘요.

관심사 분리에 따르면, 데이터 검증의 로직은 뷰 함수 안에서 진행되는 것이 아니라 Schema에 집중되어야 해요.

이렇게 하면, view는 핵심 비즈니스 로직을 처리하는 데 집중할 수 있고, 데이터 검증은 전담 컴포넌트(요소)에 맡길 수 있어요.

Pydantic의 검증 메커니즘을 통해 우리는 관심사 분리를 실현하고, 데이터 검증과 비즈니스 로직을 분리할 수 있어요. 이는 코드의 구조를 향상시킬 뿐만 아니라 개발 프로세스를 더욱 명확하고 안정적으로 만들어 줘요.


새로운 요구 사항: 비밀번호 확인#

우리는 매우 간단하지만 교차 필드 검증의 가치를 충분히 설명할 수 있는 기능을 구현할 거예요: 바로 비밀번호 확인이에요.

먼저 이전 글 마지막에 있었던 「사용자 추가」 API의 요청 Schema 내용을 되짚어 볼게요:

class CreateUserRequest(Schema):
    username: str = Field(examples=['Alice'])
    email: str = Field(examples=['alice@example.com'])
    password: str = Field(min_length=8, examples=['password123'])
    bio: str | None = Field(
        default=None, examples=['Hello, I am Alice.'])
    ...

이 Schema의 설계는 확실히 부족한 점이 있어요.

왜냐하면 사용자가 등록할 때 비밀번호는 보통 두 번 입력해야 하며, 두 번째 입력의 역할은 「확인」이기 때문이죠——중요한 일은 두 번 말하는 법이니까요!

그래서, 우리는 confirm_password 필드를 추가하고 password교차 필드 검증을 진행할 거예요: 두 내용이 동일한지 확인하는 거죠.

그 안의 검증 로직이 매우 단순할지라도, 이것이야말로 교차 필드 검증을 위한 최고의 무대예요.


교차 필드 검증 구현: model_validator 사용하기#

Pydantic v2는 교차 필드 검증을 처리하기 위해 @model_validator 데코레이터를 도입했으며, 이는 Pydantic v1의 @root_validator를 개선하고 대체한 것이에요.

여기서 model은 Pydantic의 BaseModel——즉 우리의 Schema를 가리키며, Django의 Models가 아니에요.

우리는 @model_validator를 통해 「사용자 추가」 API를 강화하고, 「비밀번호 확인」 기능을 추가할 거예요.

수정된 코드를 바로 볼게요:

class CreateUserRequest(Schema):
    ...
    password: str = Field(min_length=8, examples=['password123'])
    confirm_password: str = Field(
        min_length=8, examples=['password123'])
    ...

    @model_validator(mode='after')
    def check_passwords_match(self) -> Self:
        if self.password != self.confirm_password:
            raise ValueError('비밀번호와 확인 비밀번호는 동일해야 합니다.')
        return self

핵심 포인트 분석#

  • confirm_password 필드가 추가되었어요.
  • @model_validator(mode='after') 데코레이터를 사용하여 교차 필드의 검증 메서드를 정의했어요.
    • mode는 총 세 가지가 있어요: before, after, wrap. 그 안의 세부 사항이 꽤 많아서 분량상 이 글에서 전부 펼쳐놓을 수는 없어요 (나중에 번외편에서 보충할지도 모르겠네요).
    • 여러분이 알아야 할 것은, 대부분의 경우 after 모드를 사용한다는 것이고, 이때의 검증 메서드는 「인스턴스 메서드」이며 self 매개변수는 Schema 인스턴스 그 자체(input 데이터로부터 초기화된 것)를 의미한다는 거예요.
  • 검증 메서드 check_passwords_matchpasswordconfirm_password 필드를 비교하고, 필드 내용이 다르면 ValueError를 던져요.
  • 앞서 언급했듯이 로직은 매우 간단하지만, 두 필드 사이의 검증을 확실히 실현하고 있죠.
  • 교차 필드 검증은 모든 단일 필드 검증이 완료된 후에 실행돼요.

관심사 분리의 실제 적용#

이번에 「비밀번호 확인」 기능을 추가로 구현하면서, 뷰 함수는 전혀 변동이 없다는 것을 발견할 수 있을 거예요!——이것이 바로 관심사 분리 원칙이 구현된 모습이죠.

뷰 함수 안에 직접 검증 로직을 구현하는 것(view와 Schema를 동시에 수정해야 함)과 비교하면, 이러한 구현 방식은 의심할 여지 없이 훨씬 더 깔끔하고 분리(decoupled)되어 있어요.


검증 실패 시의 HTTP 응답#

마지막으로, 데이터 검증에 실패했을 때 어떤 HTTP 응답을 받게 되는지 살펴볼게요.

처음 두 항목은 이전 글에서 이미 언급한 내용인데, 상호 대조하고 복습하기 위해 여기에 다시 나열할게요.

비밀번호 길이 제한 위반#

응답 결과는 다음과 같아요:

{
    "detail": [
        {
            "type": "string_too_short",
            "loc": [
                "body",
                "payload",
                "password"
            ],
            "msg": "String should have at least 8 characters",
            "ctx": {
                "min_length": 8
            }
        }
    ]
}

이 글은 Django Ninja가 Pydantic 검증 오류를 잡아내어 제공하는 「시스템 수준」 응답이며, 상태 코드는 422예요.

「숫자를 반드시 포함해야 함」 규칙 위반#

입력한 비밀번호에 숫자가 없을 때, 응답 결과는 다음과 같아요:

{
    "detail": [
        {
            "type": "value_error",
            "loc": [
                "body",
                "payload",
                "password"
            ],
            "msg": "Value error, 비밀번호는 하나 이상의 숫자를 포함해야 합니다.",
            "ctx": {
                "error": "비밀번호는 하나 이상의 숫자를 포함해야 합니다."
            }
        }
    ]
}

뭔가 비슷해 보이죠? 맞아요. 이것도 Django Ninja의 자동 응답 형식이기 때문이에요——오류 메시지 안에 우리가 커스텀한 내용이 포함되어 있다는 것만 빼면요.

ValueError를 던진 응답#

하지만 사실 이것은 우리가 검증 메서드 안에서 던진 것이 ValueError이기 때문에 Django Ninja가 자동으로 처리해 준 거예요.

비밀번호 확인이 일치하지 않을 때도 비슷한 응답이 발생해요:

{
    "detail": [
        {
            "type": "value_error",
            "loc": [
                "body",
                "payload"
            ],
            "msg": "Value error, 비밀번호와 확인 비밀번호는 동일해야 합니다.",
            "ctx": {
                "error": "비밀번호와 확인 비밀번호는 동일해야 합니다."
            }
        }
    ]
}

그렇다면 던져진 오류가 다른 종류의 오류, 예를 들어 Django의 ValidationError나 심지어 우리가 직접 정의한 오류라면 Django Ninja가 여전히 자동으로 처리해 줄까요?

정답은: 아니요 예요.

500 Internal Server Error」를 받게 될 거예요——이것이 바로 다다음 글의 핵심 내용이죠.


소결 및 다음 단계#

이 글에서는 @model_validator를 통해 교차 필드 검증 요구 사항을 구현하고, 동시에 관심사 분리 원칙을 실천하는 방법을 소개했어요.

이 두 글을 다 학습하고 나면, Django Ninja 데이터 검증에 대한 여러분의 이해도는 이미 대부분의 사람들을 넘어섰을 거예요.

다음에는 데이터 검증에 실패했을 때 어떻게 하면 오류를 우아하게 처리하고 응답하여 API의 사용 경험을 높일 수 있을지 깊이 있게 논의해 볼게요.