본문 바로가기
관리자

Programming-[Backend]/Django

Django REST framework 공식문서: 2. Serializer - Validation과 Data 접근

728x90
반응형

 

 

 

DRF 공식문서 스터디 기록. 일반적이고 기본적인 내용은 생략하고, 새롭게 배운 내용과 실무에서 쓰일만한 내용 위주로 공부한다.

DRF 공식문서의 API Guide 부분을 참고했다.

https://www.django-rest-framework.org/

 

 

 


 

 

1. Validation

 

Validation에 대한 자세한 내용은 다음에 Validator 내용을 작성하는 곳에서 다룰 예정이다. 여기서는 Serializer를 사용할 때 필수적으로 알아야할 Validation의 기본적인 사항에 대해서만 공부한다.

 

validated_data에 접근하거나, 어떤 instance를 save하기 전에 반드시 is_valid()를 호출해야 한다.

 

즉 아래와 같은 방식으로 사용해야한다.

 

serializer = CommentSerializer(data={'email': 'foobar', 'content': 'baz'})
serializer.is_valid() # <- 반드시 호출
serializer.save()

 

그리고 is_valid()가 실행된 후에는 Serializer내에서 view에서 전달하는 data에 직접 접근이 불가능하고 반드시 validated_data에만 접근이 가능하다. 이것은 view -> Serializer로 넘어간 이후 검증된 정보만 갖고 DB와 통신하기 위함이다. 만약 기존 data에도 접근이 가능하게한다면, 여기서 가져온 검증되지 않은 데이터를 DB로 전송하여 검증이 의미가 없어질 수 있기 때문이다.

 

 

 

1-1. Field-level 검증

다른 언어들과 마찬가지로 Field-level, Object-level의 검증이 따로 존재한다. DRF에서는 .validate_<field_name> 이라는 메서드로 Serializer에서 정의한 각 field를 검증할 수 있도록 정형화 해놓았다.

 

from rest_framework import serializers

class BlogPostSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=100)
    content = serializers.CharField(required=False)

    def validate_title(self, value):
        """
        Check that the blog post is about Django.
        """
        if 'django' not in value.lower():
            raise serializers.ValidationError("Blog post is not about Django")
        return value

 

만약 위 content 필드와 같이 required=False로 지정해놓고 요청에서 content 필드값을 보내지 않으면 (어찌보면 당연히) validation이 일어나지 않는다.

 

 

Validator

각 필드에 Validator를 정의할 수 있다. 이렇게 validator를 정의하여 사용함으로써 코드 재사용성을 높일 수 있다. 그냥 함수로 정의해도 되고, 특별한 Validator class를 만들어서 Meta 정보에 넣어서 사용할 수도 있다.

 

 

일반 함수

def multiple_of_ten(value):
    if value % 10 != 0:
        raise serializers.ValidationError('Not a multiple of ten')

class GameRecord(serializers.Serializer):
    score = IntegerField(validators=[multiple_of_ten])
    ...

 

Validator 생성

사실 UniqueTogetherValidator는 장고에서 제공하는 특별한 클래스이다. 이런 Validator에 대해서는 나중에 Validator를 다룰 때 따로 더 알아본다.

class EventSerializer(serializers.Serializer):
    name = serializers.CharField()
    room_number = serializers.IntegerField(choices=[101, 102, 103, 201])
    date = serializers.DateField()

    class Meta:
        # Each room only has one event per day.
        validators = [
            UniqueTogetherValidator(
                queryset=Event.objects.all(),
                fields=['room_number', 'date']
            )
        ]

 

 

 

1-2. Object-level 검증

Serializer에서 정의한 필드들을 여러 개 한 번에 검증한다던가, 다른 내용을 검증하고 싶다면 validate() 메서드 내에 정의하여 사용한다. 아래는 start, finish로 정의된 두 필드의 날짜값을 비교하여 ValidationError를 raise하는 예문이다.

from rest_framework import serializers

class EventSerializer(serializers.Serializer):
    description = serializers.CharField(max_length=100)
    start = serializers.DateTimeField()
    finish = serializers.DateTimeField()

    def validate(self, data):
        """
        Check that start is before finish.
        """
        if data['start'] > data['finish']:
            raise serializers.ValidationError("finish must occur after start")
        return data

 

 

이 정도만 알아도 검증 로직을 넣는데는 큰 문제가 없다. 일반적으로 자주 사용하는 Validator 클래스라던가, 좀 더 자세한 내용은 다음에 Validator를 따로 공부하면서 알아보면 된다!

 


 

2. Data 접근

 

Serializer의 구성을 모르고 사용하면서 제일 난감하고 에러가 많이 났던 부분이 data 부분이다. DRF에서는 뭔가 정해진 대로 data를 다루지 않으면 에러를 뱉는 경우가 많은 것 같다 ㅠ

 

2-1. Instance와 data

  • Serializer를 초기화할때 인자값으로 넘기는 어떤 객체는 serializer에서 instance로 취급된다. 즉, create나 update의 대상이 된다.
  • data는 Serializer에서 정의하여 클라이언트에서 받아오는 데이터를 의미한다. 이 data는 initial_data라는 객체에 담긴다.
  • Serializer 초기화 시, partial=True로 주면 Serializer에서 정의한 필드의 일부만 data로 전달할 수도 있다.

 

view

# Update `comment` with partial data
serializer = CommentSerializer(comment, data={'content': 'foo bar'}, partial=True)

serializer

class UserSerializer(serializers.Serializer):
    email = serializers.EmailField()
    username = serializers.CharField(max_length=100)

class CommentSerializer(serializers.Serializer):
    user = UserSerializer()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

 

위 예시에서 Comment 모델을 업데이트하기 위한 CommentSerializer를 정의했다. Comment의 필드가 많을 수 있지만, update를 위해 필요한 필드는 user, content, created만 정의했다. 여기서 user는 nested object 형태로 정의해서 값을 받아오도록 했다.

 

view에서 serializer를 초기화하면서 instance로는 comment를, data는 'content' 필드만 넘겼다. partial=True로 줬기 때문에 이게 가능하다.

 

 

2-2. Nested Object의 특징

 

위 예제에서 user 필드는 UserSerializer로 한 번 더 감쌌다. 이런 경우를 nested object라고 부른다.

 

Serializer 초기화 시

한 번 더 감싸사 값을 주면 된다. validation 실패 시 serializer.errors에 담길 때도, validated_data에 담길 때도 주석문과 같이 한 번더 감싸진 형태로 입력된다.

serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
serializer.is_valid()
# False
serializer.errors
# {'user': {'email': ['Enter a valid e-mail address.']}, 'created': ['This field is required.']}

 

 

create나 update시

한 번더 감싸진 형태이므로 validated_data에서 .pop으로 꺼내서 **(unpacking) 해서 사용해야한다.

 

 

create

user를 만들면 반드시 profile이 생성되도록 하는 경우가 있다고 가정한다. 그럼 아래와 같이 nested object 형태로 serializer를 구성하고 create를 하면 깔끔하게 작성할 수 있을 것이다.

class UserSerializer(serializers.ModelSerializer):
    profile = ProfileSerializer()

    class Meta:
        model = User
        fields = ['username', 'email', 'profile']

    def create(self, validated_data):
        profile_data = validated_data.pop('profile')
        user = User.objects.create(**validated_data)
        Profile.objects.create(user=user, **profile_data)
        return user

 

update

create와 마찬가지로 validated_data에서 nested된 'profile' 정보를 가져오고, instance(user).profile의 is_premium_member, has_support_contract 속성값을 set 해준다.

 

create나 update시 partial_update등 Serializer에서 정의한 write 목적(DB에 create 또는 update하기 위한, read_only=True가 아닌 필드)의 필드를 정확히 입력받지 못하는 상황이 오면, DRF는 에러를 뱉도록 되어있다고 한다. 여러가지 경우의 수를 고려해야하기 때문이라고 하는데, 만약 자동으로 writable하게 필드를 정의하고 싶다면 DRF Writable Nested 라는 라이브러리를  추천한다. 그러나 개발자가 필수 write 필드를 라이브러리에 맡겨버리는게 맞을지는 좀 의문이 든다. 나중에 Serializer를 직접 작성하면서 많이 불편할 것 같으면 참고해보면 좋겠다.

def update(self, instance, validated_data):
        profile_data = validated_data.pop('profile')
        # Unless the application properly enforces that this field is
        # always set, the following could raise a `DoesNotExist`, which
        # would need to be handled.
        profile = instance.profile

        instance.username = validated_data.get('username', instance.username)
        instance.email = validated_data.get('email', instance.email)
        instance.save()

        profile.is_premium_member = profile_data.get(
            'is_premium_member',
            profile.is_premium_member
        )
        profile.has_support_contract = profile_data.get(
            'has_support_contract',
            profile.has_support_contract
         )
        profile.save()

        return instance

 

 

 

2-3. required, many, context

 

꼭 필요하지 않은 필드라면 required=False를 주면된다. 아래 예시처럼 nested object에도 적용 가능하다. 그리고 list 형태로 받고 싶다면 many=True 옵션을 주면 된다.

 

class CommentSerializer(serializers.Serializer):
    user = UserSerializer(required=False)
    edits = EditItemSerializer(many=True)  # A nested list of 'edit' items.
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

 

BookSerializer에 Book 모델의 모든 object를 list로 불러와서 instance 형태로 many=True로 전달한다. 약간 헷갈리는 naming 방식인데... serializer.data는 최종적으로 serializer를 거친 정보를 view로 돌려줘서 Response로 보내기 위한 최종 결과물이다. 나중에 BaseSerializer를 공부하면서 다룰 것이다.

(왜 문서가 기본부터 설명안하고 응용 -> 기본으로 설명하는 건지 좀 의문... ㅠ)

queryset = Book.objects.all()
serializer = BookSerializer(queryset, many=True)
serializer.data
# [
#     {'id': 0, 'title': 'The electric kool-aid acid test', 'author': 'Tom Wolfe'},
#     {'id': 1, 'title': 'If this is a man', 'author': 'Primo Levi'},
#     {'id': 2, 'title': 'The wind-up bird chronicle', 'author': 'Haruki Murakami'}
# ]

 

 

 

추가로 연관 관계가 있는, 위 예제에서 User-Profile 같은 모델을 저장할 때는 manager class를 사용하면 좀 더 깔끔하게 할 수 있다고 한다. Manage class는 자바-Spring Data Jpa의 Repository 처럼 쓸 수 있는 인터페이스인데, 이건 나중에 공부해봐도 좋을 것 같다. 궁금하면 아래 블로그 참고.

 

화해 블로그 - DRY를 위해 Django Manager를 적용해봅시다.

http://blog.hwahae.co.kr/all/tech/tech-tech/4108/

728x90
반응형