콘텐츠로 이동

단일 필드 검증#

2024 iThome 철인 대회

이 글은 Django Ninja 시리즈 튜토리얼의 19번째 글이에요. 제5장: 데이터 검증과 오류 처리로 들어갈게요.

데이터 검증은 API 개발의 핵심 요구 사항 중 하나로, 클라이언트가 제출한 데이터가 예상대로인지 확인하여 잠재적인 오류와 보안 문제를 방지하는 역할을 해요.

효과적인 데이터 검증은 API가 잘못된 데이터를 수신했을 때 즉각적이고 친절한 응답을 제공하여 시스템의 안정성과 사용자 경험을 향상시킬 수 있어요.

Django Ninja에서 데이터 검증의 핵심 도구는 Pydantic이에요. 이는 강력한 검증 기능을 제공하여 데이터 타입에 대한 검사를 수행할 수 있을 뿐만 아니라 커스텀 검증도 쉽게 구현할 수 있게 해줘요.

이 글에서는 Django Ninja에서 Pydantic을 사용하여 단일 필드의 커스텀 검증을 구현하는 방법을 소개하고, 다음 글에서는 교차 필드(다중 필드)의 커스텀 검증을 설명할게요.

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

GitHub 예제 프로젝트#

👉 Django-Ninja-Tutorial


제5장 개요#

데이터 검증은 매우 중요하며, 검증에 실패할 때 프로그램은 종종 검증 오류를 던져요. 이러한 오류를 어떻게 효과적으로 처리할 파트는 「오류 처리」에서 논의할 범위예요.

이 장에서는 이 두 가지 밀접하게 관련된 주제를 다루며, 총 4편의 글로 구성돼요:

앞의 두 글에서는 입력 데이터가 예상에 부합하는지 확인하고 필요할 때 오류를 발생시키기 위해 유연한 데이터 검증을 구현하는 방법을 배울 거예요.

뒤의 두 글에서는 더 나은 사용자 경험을 제공하기 위해 API 흐름 중에 발생할 수 있는 다양한 오류(검증 오류에 국한되지 않음)를 처리하는 방법을 논의할 거예요.

Django Ninja의 데이터 검증과 오류 처리 메커니즘은 Django REST framework에 비해 더 복잡하기 때문에, 여러분이 이를 명확하게 이해할 수 있도록 돕기 위해 전체 분량을 할애하여 소개해야 해요.


API 수정#

우리는 이전 글에서 새로 만든 API——사용자 추가——를 예시로 사용할 거예요.

클라이언트에서 전송된 데이터를 더 신뢰할 수 있도록 커스텀 검증을 추가하여 계속해서 개선할 거예요.

하지만 먼저 몇 가지 오류 수정을 해야 해요. 수정된 코드는 다음과 같아요:

@router.post('/users/', summary='사용자 추가', response={201: dict})
def create_user(...) -> tuple[int, dict]:
    """
    사용자 추가
    """
    user = User(
        username=payload.username,
        email=payload.email,
        bio=payload.bio,
    )
    # set_password 메서드를 사용하여 비밀번호 암호화
    user.set_password(raw_password=payload.password)
    user.save()
    return 201, {'id': user.id, 'username': user.username}

주로 두 가지를 수정했어요:

  1. router 데코레이터에 response={201: dict} 매개변수를 추가했어요. 원래는 정의되어 있지 않아서, 실제로 이 API를 사용할 때 오류가 발생할 거예요. 기본적으로 200 응답만 있기 때문에, 200 이외의 응답을 원한다면 response 매개변수를 통해 선언해야만 해요.
  2. set_password 메서드를 사용하여 사용자가 입력한 비밀번호를 암호화했어요. 이것은 비밀번호가 DB에 직접 저장되는 것을 방지하는 Django 내장 기능이에요. 비밀번호를 평문으로 저장해서는 안 된다는 것은 의심할 여지 없이 현대 개발의 기본 상식이죠.

수정이 끝났으니, 본격적으로 주제로 들어갈게요.

다양한 「수준」의 검증#

검증이라고 하니, 당연히 클라이언트에서 온 요청과 관련이 있어요——요청 내용을 검증하는 거죠.

Django Ninja에서는 Schema를 정의하여 각 API가 수신하는 데이터 구조를 설명할 수 있어요. Pydantic을 기반으로 하는 이 Schema는 요청에 있는 데이터를 자동으로 검증해 줘요.

Schema 내의 type hints는 데이터 타입을 검증할 수 있으며, 이는 가장 기본적인 검증이에요.

이전 글에서 언급한 Pydantic Field는 데이터의 길이, 범위 등의 특성에 대한 검증을 수행할 수 있어요. 이 부분은 나중에 시연해 드릴게요.

이것들은 다소 「형식적인」 검증에 속하며, 이 글에서는 특정 규칙을 기반으로 하는 더 복잡한 「커스텀 검증」에 초점을 맞출 거예요.

예제 API의 Schema 현황#

「사용자 추가」를 예로 들면, request body는 username, email, password, 그리고 bio 등의 필드를 받아요. 우리가 정의한 Schema를 통해 가장 기본적인 데이터 타입 검증을 완료할 수 있어요.

class CreateUserRequest(Schema):
    username: str
    email: str
    password: str
    bio: str | None = None

이전 글에서 설명한 것처럼, bio 필드만 선택적이며 나머지는 필수 사항이에요——필드가 없으면 422 응답을 받게 되죠. 따라서 Schema는 데이터의 「존재성」도 동시에 검증해요.

현재까지는 괜찮아 보여요! 하지만 우리는 여기서 만족하지 않을 거예요.


새로운 요구 사항: 비밀번호 규칙#

사용자가 비밀번호를 설정할 때, 다음 두 가지 규칙을 준수하도록 요구하려고 해요:

  1. 비밀번호 길이는 최소 8자 이상이어야 해요.
  2. 최소 하나의 숫자를 포함해야 해요.

이러한 규칙은 계정의 보안을 향상시키고 사용자가 너무 간단한 비밀번호를 설정하는 것을 방지하는 데 도움이 돼요.

교육 목적을 고려하여 규칙을 너무 복잡하게 만들지는 않았어요. 이 두 가지 규칙은 각각 특정한 교육적 의미가 있어요:

  1. 최소 길이 제한은 직접 구현할 필요 없이 Pydantic Field를 통해 직접 구현할 수 있어요.
  2. 두 번째 규칙은 하이라이트인데, Pydantic의 @field_validator 데코레이터를 사용하여 필드의 검증 규칙을 직접 정의할 거예요.

비밀번호 규칙 검증 구현: field_validator 사용하기#

요구 사항에 따라 먼저 Pydantic의 Field를 활용하여 최소 길이 제한을 설정할 수 있어요:

password: str = Field(min_length=8, examples=['password123'])

위와 같이 min_length=8 매개변수만 추가하면 돼요.

「숫자를 포함해야 한다」는 검증은 @field_validator 데코레이터를 사용하여 구현해야 해요.

field_validator 데코레이터#

Pydantic v1에서는 이 데코레이터의 이름이 validator였고, v2에서야 field_validator로 변경되었어요.

Pydantic은 v1에서 v2로 넘어오면서 많은 breaking change가 있었어요. 이전에 언급했던 example 매개변수가 examples로 바뀐 것도 그 예 중 하나예요. 이 부분은 주의할 가치가 있어요.

다음은 수정된 Schema인데, 우리는 field_validator 부분에만 집중할게요:

class CreateUserRequest(Schema):
    ...
    password: str
    ...

    @field_validator('password')
    @classmethod
    def validate_password_contains_number(cls, v: str) -> str:
        """
        비밀번호가 하나 이상의 숫자를 포함하는지 검증
        """
        if not re.search(r'\d', v):
            raise ValueError('비밀번호는 하나 이상의 숫자를 포함해야 합니다.')
        return v

핵심 포인트 분석#

  1. field_validator 데코레이터는 반드시 매개변수를 사용해야 하며, 유효한 값은 password와 같은 필드 이름이에요.
  2. 예시에서는 보여주지 않았지만, 여러 필드에 적용할 수도 있어요.
    1. 작성법은 @field_validator('필드1', '필드2', ...) 이며, 심지어 @field_validator('*')로 작성하여 모든 필드에 적용할 수도 있어요.
    2. 하지만 주의할 점은, 이러한 필드들이 동일한 검증 로직을 실행하므로 이론적으로 논리가 유사한 필드들이어야 한다는 거예요.
  3. 검증 메서드의 이름은 사용자 정의할 수 있으며, 본인이 이해하기 쉽다면 어떻게 이름 지어도 상관없어요.
    1. 왜냐하면 Pydantic은 주로 데코레이터에 있는 필드 이름을 보기 때문이에요.
    2. 이것은 Django REST framework의 검증 메서드가 validate_<필드명> 형태의 네이밍 패턴을 채택하는 것과는 아주 큰 차이가 있어요.
  4. Pydantic 검증 메서드의 매개변수 네이밍 관례는 v인 반면, Django REST framework는 value예요.
  5. 관례 2: 검증 메서드는 성공 시 입력값을 원형 그대로 return하고, 실패 시에는 오류를 발생시켜요.
  6. Pydantic의 검증 메서드는 「클래스 메서드」이므로 첫 번째 매개변수는 cls예요. 특이한 점은 @classmethod 데코레이터를 생략할 수 있다는 거예요. Pydantic이 내부적으로 이미 처리했기 때문이죠.
    1. 하지만 공식 문서에서는 여전히 @classmethod를 사용하는 것을 권장하고 있으니, 우리는 그 권장 사항을 따르도록 할게요.
    2. 만약 @classmethod 데코레이터를 선언한다면, 그 위치는 검증 메서드에 가장 가까워야 해요.

예상치 못했죠? 단 몇 줄의 코드 안에 이렇게나 많은 관전 포인트가 있다니요!


실제 테스트#

비밀번호 길이가 부족한 상황을 테스트해 본 결과는 다음과 같아요:

{
    "detail": [
        {
            "type": "string_too_short",
            "loc": [
                "body",
                "payload",
                "password"
            ],
            "msg": "String should have at least 8 characters",
            "ctx": {
                "min_length": 8
            }
        },
        {
            "type": "string_too_short",
            "loc": [
                "body",
                "payload",
                "confirm_password"
            ],
            "msg": "String should have at least 8 characters",
            "ctx": {
                "min_length": 8
            }
        }
    ]
}
이것은 Field가 검사할 때 스스로 던진 오류이며, 응답 상태 코드는 422예요.

다음으로, 비밀번호에 숫자가 포함되지 않은 상황을 테스트해 볼게요:

{
    "detail": [
        {
            "type": "value_error",
            "loc": [
                "body",
                "payload",
                "password"
            ],
            "msg": "Value error, 비밀번호는 하나 이상의 숫자를 포함해야 합니다.",
            "ctx": {
                "error": "비밀번호는 하나 이상의 숫자를 포함해야 합니다."
            }
        }
    ]
}
이것은 우리가 만든 「부분 사용자 정의」 오류 응답이라고 볼 수 있어요. 구조는 여전히 Django Ninja가 결정하지만, 오류 메시지 부분은 우리가 직접 정의했기 때문이죠.

오류 응답에 대한 커스터마이징은 훨씬 더 유연해질 수 있지만, 이는 다다음 글인 「오류 처리 (상) HttpError와 커스텀 HTTP 응답」의 주제이므로 그때 가서 더 자세히 논의할게요.


소결#

이 글에서는 Pydantic을 통해 단일 필드에 대한 데이터 검증을 진행하고, 비밀번호 강도 검사 규칙을 구현하는 방법을 배웠어요.

다음 글에서는 이 주제를 이어서 더 복잡한 교차 필드 검증을 구현해 볼 거예요.