[Django] form.is_valid() 의 동작

Humpback Whale·2024년 2월 5일
0
post-thumbnail

Django에서는 사용자가 페이지의 폼을 작성하고, 작성한 폼 데이터는 장고 폼(Form)을 사용하여 각 데이터의 유효성을 검증하게 된다.

뷰 에서 장고 폼 객체를 사용하여, 데이터의 유효성 검증하는 로직은 장고를 통해 간단한 웹어플리케이션을 만들어 보았다면 익숙할 것이다.

주로 Fucntion Based View를 통해, 포스트(POST) 방식으로 전송된 데이터를 장고 폼 객체의 데이터로 바운드 시킨 후에, 데이터의 유효성을 is_valid()로 검증을 하곤 한다.


여기서 장고 폼이 내부적으로 어떻게 필드의 유효성을 검사하는지에 대한 동작이 궁금해졌다.
따라서, 해당 포스팅을 통해 내부적으로 장고 폼이 유효성을 검사하는 로직에 대해 소개하고자 한다.


먼저 장고 BasedFormis_valid() 내부 구현을 살펴보면 다음과 같다.

class BasedForm(RenderableFormMixin):
    ...
    def is_valid(self):
    	return self.is_bound and not self.errors
    ...

우리가 폼 데이터 유효성 검사를 위해 is_valid() 를 호출할 경우,
1. 바운드 된 모델이 있는지(self.is_bound),
2. 유효성 검증 후 발생한 에러를 가지지 않는지(not self.errors)
를 확인한다.


여기서, errors의 로직은 다음과 같이 @property 로 정의되어 있다.

    @property
    def errors(self):
        if self._errors is None:
            self.full_clean()
        return self._errors

즉, BasedForm 객체의 필드로 정의되어 있는 _errors가 비어있을 경우(Form 객체 생성시 None으로 초기화 되어있음), full_clean() 메서드를 호출하게 된다.
full_clean() 메서드가 유효성 검증 로직을 담당하며, 이 과정에서 ValidationError가 발생 시, 발생한 에러를 self._errors 에 추가하여, errors는 수집된 에러들을 반환하는 것이다.


그럼 이제 full_clean() 메서드의 내부구현을 살펴보자.

def full_clean(self):
    self._errors = ErrorDict()
    if not self.is_bound:  # Stop further processing.
        return
    self.cleaned_data = {}
        if self.empty_permitted and not self.has_changed():
            return

    self._clean_fields()
    self._clean_form()
    self._post_clean()

full_cleaned() 메서드는 내부에 self.clean_data 딕셔너리를 포함하며, 여기에 유효성이 검증된 데이터를 필드이름과 값의 키-값 으로 저장할 것이다.

그렇다면, 어떻게 self.cleaned_data에 값을 저장하는가?

self._clean_fields(), self._clean_forms(), self._post_clean() 을 차례대로 호출하면서 유효성 검증 결과를 채워나가는 것이다.
즉, full_clean() 메서드는 각 필드의 유효성 검증 순서를 정의하며, 유효성이 검증된 데이터를 관리한다.

우리가 view 에서 form.is_valid() 호출 후, 유효성이 검증된 데이터 사용을 위해 cd = form.cleaned_data 사용할 수 있는 것은,

  1. is_valid() 호출 시, 내부적으로 errors 호출
  2. erorrs 호출 시 내부적으로 full_clean() 호출
  3. full_clean() 내부 self.cleaned_data 정의 및 유효성이 검증된 데이터를 추가.

하는 단계를 거친다는 것이다.


full_clean() 메서드가 포함하는 clean 함수들 중 추가적으로 알아 볼만 한 것은 _clean_fields() 이다. 해당 코드는 다음과 같다.

def _clean_fields(self):
    for name, bf in self._bound_items():
        field = bf.field
        value = bf.initial if field.disabled else bf.data
        try:
            if isinstance(field, FileField):
                value = field.clean(value, bf.initial)
            else:
                value = field.clean(value)
            self.cleaned_data[name] = value
            if hasattr(self, "clean_%s" % name):
                value = getattr(self, "clean_%s" % name)()
                self.cleaned_data[name] = value
        except ValidationError as e:
              self.add_error(name, e)

해당 로직이 각 필드를 조회하여 유효성을 검증하는 로직을 가지며, 이 과정에서 유효성 검사가 실패할 경우, self._errors에 발생한 에러를 추가하고 그렇지 않을 경우 self.cleaned_data에 유효성 검증 완료 된 값을 추가한다.

조금 더 알아볼만한 것은, 커스텀 필드 유효성 검증 또한 해당 로직에서 담당한다는 것이다.

예를 들어, Form에 email 필드가 정의되어 있으며, 해당 이메일은 중복되어서는 안된다고 해보자.
우리는 이 경우, Form 객체에 에 다음과 같은 커스텀 유효성 검증 로직을 추가할 수 있을 것이다.

class MyForm(forms.Form):
	email = forms.EmailField()
    
    def clean_email(self):
    	## 이메일 중복 검사 로직
        return <email >
        

이와 같이 cleand_<field name>() 함수를 정의하여 커스텀 유효성 검증 로직을 추가할 수 있는 것은,
cleaned_fields() 에서

if hasattr(self, "clean_%s" % name):
	value = getattr(self, "clean_%s" % name)()
    self.cleaned_data[name] = value

로직이 존재하기 때문이다.

따라서 우리는 특정 필드에 유효성 검증 로직을 커스텀 제어하고자 할 경우, clean_<field name>() 함수만 정의하면 내부적으로 알아서 해당 코드에 대한 유효성검증 로직이 수행된다는 것이다.


Form의 유효성 검증 과정 요약

  • is_valid() 호출 시, 내부에서 errors property logic을 호출
  • erros 프로퍼티는 내부에서 full_clean() 호출
  • full_clean() 은 내부에 self.cleaned_data 딕셔너리를 포함
  • full_clean() 은 내부에 _cleand_field() 등 유효성 검증 로직을 수행
  • _cleand_field() 등이 실제 유효성 검증 로직을 수행하며, 유효성을 만족할 경우 해당 데이터를 self.cleaned_data에 추가

+ clean_<field name> 과 같이 커스텀 유효성 검증 함수를 추가할 수 있는 것 또한 _clean_field() 가 이를 인식할 수 있도록 만들어졌기 때문.

0개의 댓글