Study/django

[udemy] Build a Backend REST API with Python & Django - Advanced 학습 정리

bluebamus 2024. 1. 3.

https://www.udemy.com/course/django-python-advanced/

 

 - 내 기준, 기록 할만한 내용들만 정리한다. 

 - 이번에 학습하고 있는 강좌는 초보자에게 적합하지 않는 강좌이다. 기본적인 설명보다 몇가지 생각 해볼만한 코드 스타일을 학습할만하다. 하지만 테스트 코드는 상당히 괜찮다. 다만 pytest가 아니라 django의 기본인 TestCase를 사용했다는 것이 아쉽다. 하지만 웬만한 테스트를 모두 소화해 낼 수 있을 만큼의 훌륭한 기반 구조를 보여준다.

   - 본 학습에 조각으로 정리하기는 아쉬워 따로 다른 포스팅에 정리해볼 예정이다.

 section 8. configure database

   - docker-compose로 django project와 postgresql의 연동에 대해 이야기를 하면서 depends_on에 대해 언급한다.

      - 내용이 부족하지만, 내 경험상 depends_on으로 서비스 순서를 정의하더라도 실재 서비스의 ready 시점과 container상 완료 시점이 다를 수 있다. 때문에 완벽하게 신뢰를 하기 어렵다는 말이라 생각한다.

   - 해당 프로젝트는 github action을 기반으로 test와 배포를 수행하는 목표를 가지고 있다. 때문에 배포 완료 후 사용자가 따로 관리를 하지 않더라도 databse의 ready 상태를 확인하고 완료가 된 시점에 project를 실행하는걸 기대한다.

   - 제안하는 방법으로 django의 command를 이용해 databse의 상태를 check 함수로 확인하고 만약 ready가 아니라면 일정 시간 time.sleep(1)을 이용해 대기를 한다. 

   - 상용 서비스를 할 경우 gunicorn 등의 app 서버를 사용한다. 때문에 여기서 언급되는 코드를 활용하기 위해서는 docker file을 수정하거나 shell script를 사용하는 방법 등을 이용하고 테스트로 확인할 필요가 있다.

      - gunicorn은 databse 복구시 재연결을 자동으로 수행한다. 때문에 이에대한 걱정을 따로 할 필요가 없다.

   - 장애 복구시 redis나 MQ의 Cache warm-up을 하는데 사용할 수 있을것 같다.

 

   - command 파일 : wait_for_db.py

"""
Django command to wait for the database to be available.
"""
import time

from psycopg2 import OperationalError as Psycopg2OpError

from django.db.utils import OperationalError
from django.core.management.base import BaseCommand


class Command(BaseCommand):
    """Django command to wait for database."""

    def handle(self, *args, **options):
        """Entrypoint for command."""
        self.stdout.write('Waiting for database...')
        db_up = False
        while db_up is False:
            try:
                self.check(databases=['default'])
                db_up = True
            except (Psycopg2OpError, OperationalError):
                self.stdout.write('Database unavailable, waiting 1 second...')
                time.sleep(1)

        self.stdout.write(self.style.SUCCESS('Database available!'))

 

   - test code 

"""
Test custom Django management commands.
"""
from unittest.mock import patch

from psycopg2 import OperationalError as Psycopg2OpError

from django.core.management import call_command
from django.db.utils import OperationalError
from django.test import SimpleTestCase

# check 함수 동작을 hooking 한다.
@patch('core.management.commands.wait_for_db.Command.check')
class CommandTests(SimpleTestCase):
    """Test commands."""

    def test_wait_for_db_ready(self, patched_check):
        """Test waiting for database if database ready."""
        # check 함수의 return 값을 true로 설정
        patched_check.return_value = True

        call_command('wait_for_db')

		# databases=['default'] 설정에 한번 호출 되었는지 확인한다.
        patched_check.assert_called_once_with(databases=['default'])

    @patch('time.sleep')
    def test_wait_for_db_delay(self, patched_sleep, patched_check):
        """Test waiting for database when getting OperationalError."""
        
        # Psycopg2OpError 에러 2회 OperationalError 에러 3회 후 true 성공
        patched_check.side_effect = [Psycopg2OpError] * 2 + \
            [OperationalError] * 3 + [True]

        call_command('wait_for_db')

		# 6회 시도된 이력 확인
        self.assertEqual(patched_check.call_count, 6)
        
        # patched_check 모의(mock) 객체의 assert_called_with 메서드를 사용하여 
        # databases 매개변수가 ['default']로 호출되었는지 확인
        patched_check.assert_called_with(databases=['default'])

 

 section 9. setup django admin

   - user 페이지에서 add를 누를 경우 출력되는 필드 정의

    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': (
                'email',
                'password1',
                'password2',
                'name',
                'is_active',
                'is_staff',
                'is_superuser',
            ),
        }),
    )

 

 section 10. api documentation

   - 참고 : https://drf-spectacular.readthedocs.io/en/latest/readme.html#take-it-for-a-spin

   - drf-spectacular vs drf-yasg, 이 강좌에서는 drf-spectacular를 사용한다.

   - 설정 방법

pip install drf-spectacular
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "rest_framework.authtoken",
    "drf_spectacular",
]
REST_FRAMEWORK = {
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

 

   - urls 설정

from drf_spectacular.views import (
    SpectacularAPIView,
    SpectacularSwaggerView,
)

urlpatterns = [
    path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'),
    path(
        'api/docs/',
        SpectacularSwaggerView.as_view(url_name='api-schema'),
        name='api-docs',
    ),
]

 

 section 12. build user api

   - 기본 형식의 create, update serializer 코드

class UserSerializer(serializers.ModelSerializer):
    """Serializer for the user object."""

    class Meta:
        model = get_user_model()
        fields = ['email', 'password', 'name']
        extra_kwargs = {'password': {'write_only': True, 'min_length': 5}}

    def create(self, validated_data):
        """Create and return a user with encrypted password."""
        return get_user_model().objects.create_user(**validated_data)

    def update(self, instance, validated_data):
        """Update and return user."""
        password = validated_data.pop('password', None)
        user = super().update(instance, validated_data)

        if password:
            user.set_password(password)
            user.save()

        return user

 

   - django에서 제공하는 기본 token은 basic 버전이다. 테스트로 사용하는 insomnia에 다음과 같이 설정한다.

      - 헤더의 인증 부분엥 Token을 명시해 줘야 한다.

 

   - Bearer로 설정하면 에러가 뜬다.

 

 section 13. build recipe api

   - 시리얼라이즈의 상속

      - 부모 클래스

class RecipeSerializer(serializers.ModelSerializer):
    """Serializer for recipes."""
    tags = TagSerializer(many=True, required=False)
    ingredients = IngredientSerializer(many=True, required=False)

    class Meta:
        model = Recipe
        fields = [
            'id', 'title', 'time_minutes', 'price', 'link', 'tags',
            'ingredients',
        ]
        read_only_fields = ['id']

 

      - 자식 클래스

class RecipeDetailSerializer(RecipeSerializer):
    """Serializer for recipe detail view."""

    class Meta(RecipeSerializer.Meta):
        fields = RecipeSerializer.Meta.fields + ['description']

 

 later sections

   - cbv의 사용 방법, 권한과 보안, viewset의 action과 라우터의 설명은 코드 내 사용 방법을 리뷰하는 정도로 짧게 끝낼 내용이 아니기에 다른 포스팅에서 더 상세히 정리할 예정이다.

 

   - os.path.join()은 세 개의 인수가 정의되는데 첫번째 인수(폴더 경로), 두번째 인수(하위 폴더 경로), 세번째 인수(파일명)으로 나누어 진다.

def recipe_image_file_path(instance, filename):
    """Generate file path for new recipe image."""
    ext = os.path.splitext(filename)[1]
    filename = f"{uuid.uuid4()}{ext}"

    return os.path.join("uploads", "recipe", filename)

 

   - RecipeSerializer()에서 _ (언더바)가 붙은 함수가 등장한다.

접근 제어자문법의미

Public name 외부로부터 모든 접근 허용
Protected _name 자기 클래스 내부 혹은 상속받은 자식 클래스에서만 접근 허용
Private __name 자기 클래스 내부의 메서드에서만 접근 허용

 

class RecipeSerializer(serializers.ModelSerializer):
    """Serializer for recipes."""
    tags = TagSerializer(many=True, required=False)
    ingredients = IngredientSerializer(many=True, required=False)

    class Meta:
        model = Recipe
        fields = [
            'id', 'title', 'time_minutes', 'price', 'link', 'tags',
            'ingredients',
        ]
        read_only_fields = ['id']

    def _get_or_create_tags(self, tags, recipe):
        """Handle getting or creating tags as needed."""
        auth_user = self.context['request'].user
        for tag in tags:
            tag_obj, created = Tag.objects.get_or_create(
                user=auth_user,
                **tag,
            )
            recipe.tags.add(tag_obj)

    def _get_or_create_ingredients(self, ingredients, recipe):
        """Handle getting or creating ingredients as needed."""
        auth_user = self.context['request'].user
        for ingredient in ingredients:
            ingredient_obj, created = Ingredient.objects.get_or_create(
                user=auth_user,
                **ingredient,
            )
            recipe.ingredients.add(ingredient_obj)

    def create(self, validated_data):
        """Create a recipe."""
        tags = validated_data.pop('tags', [])
        ingredients = validated_data.pop('ingredients', [])
        recipe = Recipe.objects.create(**validated_data)
        self._get_or_create_tags(tags, recipe)
        self._get_or_create_ingredients(ingredients, recipe)

        return recipe

    def update(self, instance, validated_data):
        """Update recipe."""
        tags = validated_data.pop('tags', None)
        ingredients = validated_data.pop('ingredients', None)
        if tags is not None:
            instance.tags.clear()
            self._get_or_create_tags(tags, instance)
        if ingredients is not None:
            instance.ingredients.clear()
            self._get_or_create_ingredients(ingredients, instance)

        for attr, value in validated_data.items():
            setattr(instance, attr, value)

        instance.save()
        return instance

 

   - drf-spectacular의 사용 방법

      - settings 설정

REST_FRAMEWORK = {
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

 

      - view에 커스텀 설정

@extend_schema_view(
    list=extend_schema(
        parameters=[
            OpenApiParameter(
                "tags",
                OpenApiTypes.STR,
                description="Comma separated list of tag IDs to filter",
            ),
            OpenApiParameter(
                "ingredients",
                OpenApiTypes.STR,
                description="Comma separated list of ingredient IDs to filter",
            ),
        ]
    )
)
class RecipeViewSet(viewsets.ModelViewSet):
	...

 

 

 강의 평가

   - 코드를 왜 이렇게 구현 했는지에 대한 설명이 많이 부족하다.

   - 자막이 자동 완성이라 전체 문장을 보고 대략적으로 파악해야 한다.

   - 프로젝트라 하기에 많은 부분이 부족하다.

      - uvicorn보다 uwsgi를 사용하는 이유를 모르겠다.

      - docker와 docker-compose를 github action을 이용해 배포하면서 secure을 사용하지 않는게 아쉬웠다.

      - django jwt를 사용하고 매우 기본적인 사용법만 제공했다.

      - 보안과 권한 그리고 접속 제한에 대해서가 정말 중요한데 거의 다루지 않았다.

   - 무언가 해보려 만든 강좌인데 너무 많은걸 다루면서 정작 설명이 부족해 django 프로젝트의 깊이가 얕고 다른 설명들도 부족한 상황이 되었다.

   - drf-spectacular는 사용해보지 않았던 거라 괜찮았다.

      - @extend_schema_view를 이용한 페이지 커스텀도 재미있었다.

   - viewset의 action은 그동안 사용하지 않았는데 좋은 정보였다.

   - serializers.py에서 tag와 ingredients를 저장하는 방법이 흥미로웠다. 굳이 protected 함수로 만들지 않았을거 같은데 보기에 깔끔하고 좀 더 보안해서 나중에 써먹어도 되겠다는 생각이 들었다.

   - 이번 과정을 종료하고 DRF CBV에 대해 정리를 하고 serializers와 router에 대해서도 정리를 해볼 생각이다.

   - 좀 더 깊이 있는 프로젝트와 강좌를 기대했는데... 관심을 두고 있는 남은 강좌 1개를 마무리 하고 사용률이 높은 오픈소스 몇개를 뜯어볼 생각이다. 깊이는 이 방법으로 채워야 할것 같다.

   - 전체적으로 20~30%는 얻는게 있던 강좌였다. 할인 기간에 산만큼 가격대비 만족은 했다.

   - 초급 강좌는 아니다. 

   - django 기반의 testscase 코드는 상당히 괜찮다. TDD 기반 개발을 하는데 테스트 시나리오는 좋다고 할 수 없지만 실무에서 충분히 써먹을 수준이다.

      - mock, factoryboy, faker의 사용이 있었다면 좋았을텐데, test database 설정도 가능한데 없어서 아쉬웠다. 

 

 

 - reference : 

https://velog.io/@nikevapormax/0621-Django-Rest-Framework

 

[0621] Django Rest Framework

Q를 사용해 쿼리에 and 또는 or를 적용시킬 수 있다.사용자가 admin인지 아닌지에 대해 판별하거나, 검색을 할 때 많이 사용한다. 프론트에서 데이터를 받아 or를 사용해 검색을 많이 한다. 이번에는

velog.io

https://velog.io/@duo22088/DRF-Serializer-Validation

 

(DRF) Serializer Validation

DRF에서 유효성 검사를 처리하는 대부분의 경우 단순히 기본 필드 유효성 검사에 의존하거나, 클래스에 대한 명시적 유효성 검사 방법을 이용합니다.

velog.io

https://velog.io/@jaewan/DRFserializer-%EC%9D%98-field-%EA%B5%AC%ED%98%84%ED%95%98%EC%97%AC-%EC%BB%A4%EC%8A%A4%ED%85%80%ED%95%9C-Validator-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

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

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

velog.io

https://hyeo-noo.tistory.com/324

 

[Django] Serializer와 validate (TIP!)

POST method로 새로운 글을 만들 때는 제목이 중복되면 안된다. 중복되면 안되는 필드가 있다고 생각하면 된다. 하지만 해당 글을 수정하고 저장할 때의 제목 중복여부는 수정 이전의 제목과 같을

hyeo-noo.tistory.com

https://velog.io/@catveloper/DRF-Spectacular-%EC%82%AC%EC%9A%A9%EB%B2%95

 

DRF-Spectacular 사용법

Open API Spec 3.0에 맞추어 문서화를 도와주는 라이브러리 drf-spectacular 사용법입니다.

velog.io

https://powerlichen.github.io/posts/drf-swagger/

 

DRF에서 Swagger 문서 작성

개요 OpenAPI란 API 스펙을 json, yaml로 표현한 명세이다. 직접 소스코드나 문서를 보지 않고 서비스를 이해할 수 있다. 오늘날에는 RESTful API 스펙의 사실상 표준으로 사용되고 있다.

powerlichen.github.io

 

댓글