deep-dive2024-07-25·8 min·285/348

Django Form Field 검증 커스터마이징

Django Form의 필드 검증 로직을 커스터마이징하는 다양한 방법을 알아봅니다.

Django Form Field 검증 커스터마이징

Django Form은 데이터 검증을 자동으로 처리하지만, 특정 비즈니스 로직에 맞는 검증이 필요할 때가 많습니다. 이 글에서는 다양한 검증 커스터마이징 방법을 알아봅니다.

Environment

$ python --version
Python 3.11.5

$ pip show django
Name: Django
Version: 4.2.5

Problem: 기본 검증만으로 부족한 상황

from django import forms

class RegistrationForm(forms.Form):
    username = forms.CharField(max_length=50)
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    confirm_password = forms.CharField(widget=forms.PasswordInput)

# Basic validation only checks field types
form = RegistrationForm(data={
    'username': 'joel',
    'email': 'joel@example.com',
    'password': 'mypassword',
    'confirm_password': 'differentpassword'
})

print(form.is_valid())  # True - but passwords don't match!

Analysis: Django 검증 프로세스

# Django form validation flow:
# 1. Field-level validation (field.clean)
# 2. Form-level validation (form.clean)
# 3. Model validation (if ModelForm)

# Each field has its own clean method
class CharField(forms.Field):
    def clean(self, value):
        value = self.to_python(value)
        self.validate(value)
        self.run_validators(value)
        return value

Solution: 다양한 검증 커스터마이징 방법

방법 1: clean_ 메서드

from django import forms
import re

class RegistrationForm(forms.Form):
    username = forms.CharField(max_length=50)
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    confirm_password = forms.CharField(widget=forms.PasswordInput)
    
    def clean_username(self):
        username = self.cleaned_data['username']
        
        # Check if username contains only alphanumeric characters
        if not re.match(r'^[a-zA-Z0-9_]+$', username):
            raise forms.ValidationError(
                "Username can only contain letters, numbers, and underscores."
            )
        
        # Check length
        if len(username) < 3:
            raise forms.ValidationError(
                "Username must be at least 3 characters long."
            )
        
        return username.lower()
    
    def clean_email(self):
        email = self.cleaned_data['email']
        
        # Check if email is from a specific domain
        blocked_domains = ['tempmail.com', 'throwaway.com']
        domain = email.split('@')[1]
        
        if domain in blocked_domains:
            raise forms.ValidationError(
                f"Emails from {domain} are not allowed."
            )
        
        return email
    
    def clean_password(self):
        password = self.cleaned_data['password']
        
        # Custom password validation
        if len(password) < 8:
            raise forms.ValidationError(
                "Password must be at least 8 characters long."
            )
        
        if not re.search(r'[A-Z]', password):
            raise forms.ValidationError(
                "Password must contain at least one uppercase letter."
            )
        
        if not re.search(r'[0-9]', password):
            raise forms.ValidationError(
                "Password must contain at least one digit."
            )
        
        return password

방법 2: clean() 메서드로 폼 레벨 검증

class RegistrationForm(forms.Form):
    username = forms.CharField(max_length=50)
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)
    confirm_password = forms.CharField(widget=forms.PasswordInput)
    
    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get('password')
        confirm_password = cleaned_data.get('confirm_password')
        
        if password and confirm_password:
            if password != confirm_password:
                raise forms.ValidationError(
                    "Passwords do not match."
                )
        
        # Cross-field validation
        username = cleaned_data.get('username')
        if username and password:
            if username in password:
                raise forms.ValidationError(
                    "Password cannot contain your username."
                )
        
        return cleaned_data

방법 3: 커스텀 필드 클래스

from django import forms
from django.core.exceptions import ValidationError

class PhoneNumberField(forms.CharField):
    def __init__(self, *args, **kwargs):
        self.country_code = kwargs.pop('country_code', '+82')
        super().__init__(*args, **kwargs)
    
    def clean(self, value):
        value = super().clean(value)
        
        # Remove spaces and dashes
        value = re.sub(r'[\s\-]', '', value)
        
        # Add country code if not present
        if not value.startswith('+'):
            value = self.country_code + value
        
        # Validate format
        if not re.match(r'^\+\d{10,15}$', value):
            raise ValidationError(
                "Invalid phone number format."
            )
        
        return value

class ContactForm(forms.Form):
    name = forms.CharField(max_length=100)
    phone = PhoneNumberField(country_code='+351')

방법 4: 커스텀 validator 함수

from django.core.exceptions import ValidationError

def validate_even(value):
    if value % 2 != 0:
        raise ValidationError(f'{value} is not an even number.')

def validate_file_size(max_size_mb=5):
    def validator(file):
        max_size = max_size_mb * 1024 * 1024
        if file.size > max_size:
            raise ValidationError(
                f'File size must be less than {max_size_mb}MB.'
            )
    return validator

class UploadForm(forms.Form):
    number = forms.IntegerField(validators=[validate_even])
    document = forms.FileField(
        validators=[validate_file_size(max_size_mb=10)]
    )

방법 5: ModelForm 검증

from django.db import models
from django import forms

class Product(models.Model):
    name = forms.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.IntegerField(default=0)
    
    class Meta:
        model = Product
        fields = ['name', 'price', 'stock']

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ['name', 'price', 'stock']
    
    def clean_stock(self):
        stock = self.cleaned_data['stock']
        if stock < 0:
            raise forms.ValidationError("Stock cannot be negative.")
        return stock
    
    def clean(self):
        cleaned_data = super().clean()
        price = cleaned_data.get('price')
        stock = cleaned_data.get('stock')
        
        if price and stock and price < 1 and stock > 100:
            raise forms.ValidationError(
                "Low-price items cannot have high stock."
            )
        
        return cleaned_data

Lessons Learned

  1. clean_: 개별 필드의 검증이 필요하면 clean_<fieldname> 메서드를 오버라이드하세요.

  2. clean() 메서드: 여러 필드 간의 교차 검증이 필요하면 clean() 메서드를 사용하세요.

  3. 커스텀 필드: 재사용 가능한 검증 로직은 커스텀 필드 클래스로 만드는 것이 좋습니다.

  4. validator 함수: 간단한 검증 로직은 validator 함수로 분리하면 테스트가 쉬워집니다.

  5. 에러 메시지: 사용자 친화적인 에러 메시지를 작성하여 어떤 문제가 있는지 명확하게 알려주세요.


This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.