[udemy] Build a Backend REST API with Python & Django - Advanced 학습 정리
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
https://velog.io/@duo22088/DRF-Serializer-Validation
https://hyeo-noo.tistory.com/324
https://velog.io/@catveloper/DRF-Spectacular-%EC%82%AC%EC%9A%A9%EB%B2%95
https://powerlichen.github.io/posts/drf-swagger/
'Study > django' 카테고리의 다른 글
[UDEMY]Django DRF Project: eCommerce RESTful API 강의 정리 (0) | 2024.03.01 |
---|---|
Django ORM (QuerySet)구조와 원리 그리고 최적화전략 - 정리 (0) | 2024.02.22 |
SQL을 이해하고 사용하는 Django ORM, 신동현 - PyCon Korea 2022 - 학습 정리 (0) | 2023.12.30 |
DB를 먼저 설계하고 django와 연동하기 (6) - 함수와 lock 사용하기 (0) | 2023.12.14 |
DB를 먼저 설계하고 django와 연동하기 (5) - view table 테스트 (1) | 2023.12.12 |
댓글