[DRF]serializer의 field/object-level validation

Jay·2023년 3월 26일
0
post-thumbnail

DRF Validation

DRF를 사용하면 serializer를 사용하여 사용자로부터 입력받은 데이터의 유효성을 model의 값으로 포맷팅, 유효성검증, 저장 등과 같은 동작들을 수행하게 된다. 필자의 경우 serializer에서 수행해주어야 하는 역할을 데이터의 유효성 검증과 각 레이어에 필요한 포맷으로 변환해주는 것으로 생각하였다.
ORM을 통해 DB에 유효한 데이터를 저장하기 위해서는 DB 테이블에 데이터의 조건을 작성할수도 있다. 하지만 이는 매우 어렵고 ORM의 장점을 사용하지 못하는 것이라 생각했기 때문에 데이터의 유효성을 크게 python 코드의 Model과 serializer에서 수행하였다.

  • Model : 데이터 타입과 DB 테이블의 제약조건 정의 및 검증
  • Serializer : Model에 정의된 데이터 타입과 서비스 로직에 따른 데이터 조건을 정의 및 검증

데이터의 유효성을 검증하는 수준에는 크게 각 데이터 값의 유효성을 검증하는 field-level validation과 모델 혹은 데이터타입 클래스 수준에서 검증하는object-level validation이 있다.



field-level validation

여러 필드로 구성된 데이터 구조체 혹은 모델에서 각각의 데이터가 유효한 값인지 검증하는 수준을 field-level validatioin이라고 한다. 예를 들어 정수 필드인 a 필드로 들어온 값이 정수인지 확인하는 것이다. DRF에서는 정의된 모델에 따라 자동으로 각 필드들이 어떤 유효성 검증을 받아야되는지 매핑해주는 기능들을 제공한다. 하지만 이러한 경우, 데이터 유효성 검증에 구체적인 요구가 있을때 올바르게 검증해주지 못하는 문제가 발생한다. 이러한 경우 field-level validation을 구현해주어 데이터의 유효성을 검증해주어야 한다.

Field-level Validation 구현 방법

  • validator/Field 구현 및 사용
  • serializer 내부에 validate_필드명() 메소드 구현

DRF에서 제공하는 ModelSerializer를 사용하여 serializer를 생성하면 password의 경우 CharField로 정의되게 된다.

password = CharField(label='비밀번호', max_length=128)

하지만 이러한 경우, 사용자로부터 입력받은 데이터가 특수문자, 영어 대소문자, 비밀번호 등 서비스에서 요구하는 비밀번호의 기본 포맷인지 올바르게 검증하지 못하게 된다.

따라서 이러한 경우 validator 클래스나 field 클래스 혹은 두 클래스를 모두 구현해주어 데이터의 유효성을 올바르게 검증해 줄 수 있도록 해주어야한다.

class PasswordValidator:
    message = '비밀번호는 8자 이상의 영문 대소문자와 숫자, 특수문자를 포함하여야 합니다.'
    code = 'invalid'
    password_regex = '^(?=.*[\d])(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*()])[\w\d!@#$%^&*()]{8,}$'
    
    def __init__(self, message=None, code=None):
        if message is not None:
            self.message = message
        if code is not None:
            self.code = code
            
    def __call__(self, value):
        if not value:
            raise ValidationError(self.message, code=self.code, params={'value': value})
        
        if not re.fullmatch(self.password_regex, value):
            raise ValidationError(self.message, code=self.code, params={'value': value})

    def __eq__(self, other):
        return (
            isinstance(other, PasswordValidator)
            and (self.message == other.message)
            and (self.code == other.code)
        )

사용자로부터 입력받은 문자열 데이터가 서비스에서 요구하는 비밀번호의 포맷인지 검증하는 PasswordValidator 클래스이다. validator 객체는 __call__ 메소드 호출을 통해 데이터의 유효성을 검증한다. 이처럼 각각의 데이터의 유효성을 검증하는 것을 field-level validation 이다.



object-level validation

object-level validation은 입력받은 데이터들이 객체가 요구하는 제약조건에 만족하는 데이터인지 검증하는 것이다. 필자의 경우 스포츠 매칭 서비스를 구현하며 사용자로부터 경기 시작시간/종료시간을 입력받아야 하는 기능을 구현하였다. field-level validation으로는 각 시작시간/종료시간이 현재 시점보다 미래시점인지 검증하도록 구현하였고, object-level validation으로는 입력받은 두 시간이 올바른 시간관계인지 검증해야했다. 유효한 두 데이터의 값 관계는 아래와 같았다.

경기 시작시간 < 경기 종료시간

이처럼 데이터끼리의 관계에서 유효성을 검증하기 위해서는 object-level validation을 통해 구현해야한다. 가장 흔하게 사용하는 방법은 serializer에서 validate() 메소드를 통해 검증하는 것이다.

class BaseGameSerializer(serializers.ModelSerializer):
    start_datetime = serializers.DateTimeField(validators=[FutureDateTimeValidator(),])
    end_datetime = serializers.DateTimeField(validators=[FutureDateTimeValidator(),])
	...

    class Meta:
        model = Game
        fields = "__all__"
    
    def is_valid_gametime(self, data):
        if data.get("start_datetime") < data.get("end_datetime"):
            return True
        return False
    
    def is_valid_invitation(self, data):
        if data.get("min_invitation") <= data.get("max_invitation"):
            return True
        return False

    def validate(self, data):
        if not self.is_valid_gametime(data):
            raise InvalidGameTime()
        if not self.is_valid_invitation(data):
            raise InvalidInvitation()
        return data

serializer에서는 각 필드별로 field-level validation을 수행한 후, object-level validation을 수행하게 된다. serializervalidate() 메소드를 구현하면 해당 검증 로직이 object-level validation 과정에 수행되며 유효성을 검증할 수 있게 된다.



reference

0개의 댓글