Django에서는 사용자가 페이지의 폼을 작성하고, 작성한 폼 데이터는 장고 폼(Form)을 사용하여 각 데이터의 유효성을 검증하게 된다.
뷰 에서 장고 폼 객체를 사용하여, 데이터의 유효성 검증하는 로직은 장고를 통해 간단한 웹어플리케이션을 만들어 보았다면 익숙할 것이다.
주로 Fucntion Based View를 통해, 포스트(POST) 방식으로 전송된 데이터를 장고 폼 객체의 데이터로 바운드 시킨 후에, 데이터의 유효성을 is_valid()
로 검증을 하곤 한다.
여기서 장고 폼이 내부적으로 어떻게 필드의 유효성을 검사하는지에 대한 동작이 궁금해졌다.
따라서, 해당 포스팅을 통해 내부적으로 장고 폼이 유효성을 검사하는 로직에 대해 소개하고자 한다.
먼저 장고 BasedForm
의 is_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
사용할 수 있는 것은,
is_valid()
호출 시, 내부적으로 errors
호출erorrs
호출 시 내부적으로 full_clean()
호출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>()
함수만 정의하면 내부적으로 알아서 해당 코드에 대한 유효성검증 로직이 수행된다는 것이다.
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()
가 이를 인식할 수 있도록 만들어졌기 때문.