[DRF]serializer 의 field 구현하여 커스텀한 Validator 사용하기

Jay·2022년 9월 23일
1

개요

DRF를 사용하면 serializer를 정말 많이 구현하게 된다. 직렬화 뿐만 아니라 유효성 검사, 데이터의 저장 등 객체에 관련하여 정말 많은 일들을 serializer를 통해 다루게 된다.

class UserSignupSerializer(serializers.Serializer):
    email = serializers.EmailField(required=True)
    username = serializers.CharField(required=True)

serializer의 수많은 필드들이 이미 DRF에 구현되어있다. serializers의 필드들은 URL, Email, 수의 값 등 각 필드에 사용자가 원하는 포맷의 데이터인지 validation을 통해 검증하고, 알맞는 값이 아닌 경우에는 ValidationError를 발생시켜준다.

나는 User와 관련된 API를 개발하며 입력된 비밀번호가 알맞은 포멧인지 확인하는 검증 과정이 필요하였다. 하지만 회원가입, 회원정보 수정 등의 기능을 구현하며 반복적으로 password의 유효성을 검증하는 코드를 작성해야만 했다.

class UserUpdateSerializer(serializers.ModelSerializer):
    email = serializers.EmailField(required=False)
    username = serializers.CharField(required=False)
    new_password = serializers.CharField(required=False)
    
    class Meta:
        model = get_user_model()
        fields = ['email', 'username', 'password', 'new_password']
        
    def validate_password(self, value):
        if not check_password(value, self.instance.password):
            raise ValueError("잘못된 비밀번호입니다.")
        return value
        
    def validate_new_password(self, value):
        UserPasswordValidator.check_password(value)
        return value

구글에 customizing validation을 검색하면 위와 같이 validate_속성() 메소드를 구현하라고 한다. 하지만 반복적으로 검증되어야 하는 필드가 사용되는 모든 serializer에 위와 같은 코드를 작성하려고 하니 매우 번거로운 일이다. 또한 에러메세지를 수정하려고 해당 validate 메소드를 모두 찾아 수정해야한다. 이에 직접 Field를 구현하고, 이에 알맞는 validator를 구현하여 위와 같은 문제를 해결하고자 한다.



Validator 구현하기

먼저 Field에 사용할 validator class를 구현해야 한다. validator 클래스에 에러메세지와 검증하는 방식을 직접 구현할 수 있다.

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)
        )

message와 code는 serializer의 is_valid() 메소드를 통한 유효성 검사에서 False가 나왔을때 errors에 저장되는 message와 code이다.

__init__(self, message=None, code=None)

생성자에 message와 code를 입력하여 default message와 default code를 변경할 수 있다.

__call__(self, value)

serializer로 전달받은 값을 검증하는데 사용되는 메소드이다. value는 전달 받은 값으로, 해당 값을 검증하는 코드를 내부에 작성하면 된다. 위의 경우에는 입력받은 값이 없거나 미리 정해논 정규식과 매칭되지 않으면 ValidationError를 발생시키도록 구현하였다.



RegexValidator 사용하기

DRF 깃헙 코드를 보는 중 정규표현식으로 유효성 검증을 할 수 있는 RegexValidator 클래스를 찾게 되었다. 직접 구현한 Validator 클래스와 유사한 기능을 RegexValidator로도 구현할 수 있기에 해당 클래스를 사용하는 방법도 찾아보게 되었다.

# Signature: RegexValidator(regex, inverse_match=None, flags=None, message=None)

class UserSignupSerializer(serializers.Serializer):
    email = serializers.EmailField()
    username = serializers.CharField()
    password = serializers.CharField(validators=[RegexValidator(r'^(?=.*[\d])(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*()])[\w\d!@#$%^&*()]{8,}$', message='비밀번호는 8자 이상의 영문 대소문자와 숫자, 특수문자를 포함하여야 합니다.')])
    password_check = serializers.CharField(validators=[RegexValidator(r'^(?=.*[\d])(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*()])[\w\d!@#$%^&*()]{8,}$', message='비밀번호는 8자 이상의 영문 대소문자와 숫자, 특수문자를 포함하여야 합니다.')])
    
    def create(self, validated_data):
        user = get_user_model().objects.create_user(**validated_data)
        return user

serializers.CharField에 유효성 검증 객체로 RegexValidator 객체를 전달해주면 해당 객체의 정규표현식으로 유효성을 검증하게 된다. 유효하지 않은 경우 에러메세지는 message 인



serializer의 Field 구현하기

이제 serializer의 field를 구현해야 한다. 사실 필드를 직접 구현하지 않고, 기존의 serializers.CharField 인스턴트 생성시, 인자로 validator를 입력해주어도 된다. 하지만 Field 코드를 읽어본 김에 직접 한번 구현해보고자 한다.
기본적으로 CharField를 상속받아 default_error_message를 입력하였다. 하지만 나는 정확한 필드의 error_message를 response에 담기 위해 PasswordValidator로 default_error_message를 전달해주지는 않았다.

from rest_framework.fields import CharField

from utils.validators import PasswordValidator


class PasswordField(CharField):
    default_error_messages = {
        'invalid' : '유효한 비밀번호가 아닙니다.'
    }
    
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        validator = PasswordValidator()
        self.validators.append(validator)

__init__ 생성자의 validator를 보면 PasswordValidator 객체를 넣어주었다. 이제 해당 필드의 유효성검사는 custom한 PasswordValidator로 검증하게 된다.



적용 및 개선

from utils.fields import PasswordField

class UserSignupSerializer(serializers.Serializer):
    email = serializers.EmailField(required=True)
    username = serializers.CharField(required=True)
    password = PasswordField(required=True)
    password_check = PasswordField(required=True)
    
    def create(self, validated_data):
        user = get_user_model().objects.create_user(**validated_data)
        return user

password 속성을 charField에서 PasswordField로 바꿔주고 validate_password() 와 validate_password_check() 메소드를 삭제하였다.

class UserSignupSerializer(serializers.Serializer):
    email = serializers.EmailField(required=True)
    username = serializers.CharField(required=True)
    password = serializers.CharField(required=True, validators=[PasswordValidator()])
    password_check = serializers.CharField(required=True, validators=[PasswordValidator()])
    
    def create(self, validated_data):
        user = get_user_model().objects.create_user(**validated_data)
        return user

아니면 기존의 CharField에 validator인자로 PasswordValidator를 넣어줘도 된다.

그리고 postman으로 유효하지 않은 회원가입 정보를 입력하니 위와 같이 잘 검증되었다. 이제 비밀번호와 비밀번호 유효성 검사를 PasswordValidator 클래스에서 관리할 수 있다.



reference

https://github.com/encode/django-rest-framework/blob/2de50818296b1b4bae68787626c0236752e35101/rest_framework/fields.py#L15

0개의 댓글