교차 필드 검증#

이 글은 Django Ninja 시리즈 튜토리얼의 20번째 글이에요.
이전 글에서는 단일 필드의 커스텀 검증에 대해 이야기했고, 이번 글에서는 필드 간 교차(다중 필드) 검증에 대해 논의할 거예요.
교차 필드 검증 역시 API 개발에서 매우 흔한 요구 사항이에요. 예를 들어, 계정을 등록할 때 「비밀번호」와 「비밀번호 확인」 두 필드의 내용이 동일함을 보장해야 하거나, 날짜 기간을 선택할 때 시작 날짜가 종료 날짜보다 늦어서는 안 되는 등이죠.
이러한 검증 시나리오는 단일 필드 검증을 통해서는 구현할 수 없어요. 전체 데이터의 일관성과 정확성을 보장하기 위해 여러 필드 간의 논리적 연관성을 동시에 검사해야 하기 때문이에요.
이 글에서는 Pydantic을 통해 교차 필드 검증 요구 사항을 구현하는 방법을 소개할 거예요——「비밀번호 확인」을 예로 들어 이 기능의 실제 적용을 보여드릴게요.
이 글의 모든 코드 변경 사항은 이 PR을 참고해 주세요.
GitHub 예제 프로젝트#
교차 필드 검증과 관심사 분리#
사실, 단일 필드이든 교차 필드이든 커스텀 검증은 반드시 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_match는password와confirm_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의 사용 경험을 높일 수 있을지 깊이 있게 논의해 볼게요.