백엔드를 위한 django rest framework with 파이썬
reference : https://github.com/taebbong/drf_for_backend
* 책을 읽고 학습에 필요한 부분 정리
1. Chapter 4 Django REST Framework 컨셉 익히기
- DRF에서 Response라는 클래스를 사용하는데 이는 DRF의 결과 반환 방식이다.
- request와 마찬가지로 Response에는 응답에 대한 정보를 담고 있는데, response.data에는 응답에 포함되는 데이터, response.status에는 응답에 대한 상태가 나타난다.
- 상태코드
HTTP_200_OK | 데이터를 요청하는 GET 요청이 정상적으로 이뤄졌을 때 응답에 나타나는 상태 값 |
HTTP_201_CREATED | 데이터를 생성하는 POST 요청이 정상적으로 이뤄졌을 때 응답에 나타나는 상태값 |
HTTP_206_PARTIAL_CONTENT | 데이터의 일부를 수정하는 PATCH 요청이 정상적으로 이뤄졌을 때 응답에 나타나는 상태값 |
HTTP_400_BAD_REQUEST | 잘못된 요청을 보냈을 때(클라이언트가) 응답에 나타나는 상태값 |
HTTP_401_UNAUTHORIZED | 인증이 필요한데 인증 관련 내용이 요청에 없을 때 응답에 나타나는 상태값 |
HTTP_403_FORBIDDEN | 클라이언트가 접근하지 못하도록 막아놓은 곳에 요청이 왔을 때 응답에 나타나는 상태값 |
HTTP_404_NOT_FOUND | 클라이언트가 요청을 보낸 곳이 잘못된 URL일 때(리소스가 없을 때) 응답에 나타나는 상태값 |
HTTP_500_INTERNAL_SERVER_ERROR | 서버 쪽에서 코드가 잘못되었을 때 응답에 나타나는 상태값 |
2. DRF generics
- 9개의 조합
generics.ListAPIView | 전체 목록 |
generics.CreateAPIView | 생성 |
generics.RetrieveAPIView | 1개 데이터 보기 |
generics.UpdateAPIView | 1개 데이터 수정하기 |
generics.DestroyAPIView | 1개 데이터 삭제하기 |
generics.ListCreateAPIView | 전체 목록, 생성 |
generics.RetrieveUpdateAPIView | 1개 보기 + 1개 수정 |
generics.RetrieveDestroyAPIView | 1개 보기 + 1개 삭제 |
generics.RetrieveUpdateDestroyAPIView | 1개 보기 + 1개 수정 + 1개 삭제 |
3. serializer
- 기본형
class BookSerializer(serializers.Serializer):
bid = serializers.IntegerField()
title = serializers.CharField(max_length=50)
def create(self, validated_data):
return Book.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.bid = validated_data.get('bid', instance.bid)
instance.save()
return instance
- 모델형
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ['bid', 'title', 'author',]
4. FBV, CBV, Mixin, viewset 정리
- FBV : api_view 데코레이션을 사용해 사용하는 API 종류를 정의
- post는 create update와 delete는 구현을 따로 해야하며, get은 1건 그리고 list에 대한 항목을 따로 구현해야함
- 예시
@api_view(['GET', 'POST'])
def booksAPI(request):
if request.method == 'GET':
books = Book.objects.all()
serializer = BookSerializer(books, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
elif request.method == 'POST':
serializer = BookSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- CBV : 기본인 APIView를 상속받아 get과 post 구현
- post는 create update와 delete는 구현을 따로 해야하며, get은 1건 그리고 list에 대한 항목을 따로 구현해야함
- 이는 FBV와 동일함
class BooksAPI(APIView):
def get(self, request):
books = Book.objects.all()
serializer = BookSerializer(books, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def post(self, request):
serializer = BookSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class BookAPI(APIView):
def get(self, request, bid):
book = get_object_or_404(Book, bid=bid)
serializer = BookSerializer(book)
return Response(serializer.data, status=status.HTTP_200_OK)
- Mixin : list, retrieve, create, update, destroyd 에 대한 항목들이 존재한다.
- 먼저 필요한 mixin을 선언하고 최종 generics.GenericAPIView을 선언해 상속받는다.
class BooksAPIMixins(mixins.ListModelMixin, mixins.CreateModelMixin,generics.GenericAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
class BookAPIMixins(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
lookup_field = 'bid'
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
- 자주 사용될 mix들을 묶어서 9개의 상속받을 수 있는 api로도 제공한다. "2. DRF generics" 항목 참고
class BooksAPIGenerics(generics.ListCreateAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
class BookAPIGenerics(generics.RetrieveUpdateDestroyAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer
lookup_field = 'bid'
- viewset: 앞서 설명한 5가지의 mixin에 대한 모든 기능을 한번에 상속을 받는 방법이 있다.
- 장점 :
- 하나의 클래스로 하나의 모델에 대한 내용을 전부 작성할 수 있으며, 그에 따라 queryset이나 serializer_class 등 겹치는 부분을 최소화 할 수 있다.
- 라우터를 통해 url을 일일이 지정하지 않아도 일정한 규칙의 url을 만들 수 있다.
- url 또한 한번에 쉽게 정의할 수 있다.
- viewset
from rest_framework import viewsets
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
- router
from rest_framework import router
from .views import BookViewSet
router = router.SimpleRouter()
router.register('books', BookViewSet)
urlpatterns = router.urls
5. validate
- serializers의 class에는 validate 내장 함수가 있다.
- validate는 form class의 clean 내장 함수와 같다.
- 만약 각각 필드의 validate를 확인 하고자 하는 경우, validate_XXXX 와 같은 형식으로 함수명을 만들면 된다.
- 예시
def validate(self, data):
if data["password"] != data["password2"]:
raise serializers.ValidationError(
{"password": "Password fields didn't match."}
)
return data
def validate(self, data):
print("data : ", data)
user = authenticate(**data)
if user:
token = Token.objects.get(user=user)
return token
raise serializers.ValidationError(
{"error": "Unable to log in with provided credentials."}
)
6. token
- django 내장 토큰 외 외부 라이브러리를 사용하여 관리하는 토큰들이 존재한다.
- 가입을 하면 create를 하고, 이후 get을 가져온다.
- token은 갱신과 삭제가 존재한다. 이는 필요한 경우 따로 관리한다.
- 많이 사용하는 라이브러리 중 하나로 jwt가 있다.
- 설치
pip install djangorestframework djangorestframework-jwt
- 사용 예시
# 생성
token = Token.objects.create(user=user)
# 가져오기
token = Token.objects.get(user=user)
7. Insomnia로 token을 이용한 테스트 하기
- login을 해서 token을 가져온다
- header에 토큰과 content-Type을 설정한다.
- 본문의 속성을 multipart로 변경 후 nickname, position, subjects, image 필드를 만들고 patch로 전송한다.
8. 커스텀 권한
from rest_framework import permissions
class CustomReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.user == request.user
- permissions.SAFE_METHODS은 데이터에 영향을 미치지 않는 메소드 get과 같은 메소드를 의미한다.
- 이런 요청은 True로 반환하여 통과시키고, PUT/PATCH와 같은 경우 요청으로 들어온 유저와 객체 유저를 비교해 같으면 통과를 시켜준다.
9. React와 연동
- 책에는 아무런 설명이 없으 React의 구동에 대해서 해봐라는 식으로 되어 있어 고생함
- django 코드 github에 들어가면 readme에 링크가 있고 해당 링크는 아래와 같다.
https://github.com/TaeBbong/React-Board
- 현재 react를 전혀 모르는 사람으로 검색을 통해 시도를 해 봤지만, package의 호환성 에러 등의 문제로 결국 구동에 실패하고 api 테스트 툴로 실행할 것이다.
- 여기까지 정독 하면서 느끼지만, 정말 설명이 말도 안되게 부족하고 가이드가 잘 안되어 있다.
10. CORS
- django의 주소는 127.0.0.1:8000 이라면 리액트는 127.0.0.1:3000으로 실행된다. 즉 주소가 틀리다. 이렇게 주소가 다른 출처에서 django로 데이터를 가져오려 하면 이는 SOP(same origin policy)에 의해 차단된다.
- 이에 대한 예외 조항이 CORS이며 서버에서 CORS 정책을 준수하게 설정하면, SOP의 예외 조항인 CORS 정책에 의해 다른 출처끼리도 자원 공유가 가능해진다.
- django-cors-header 패키지 설치
pip install django-cors-headers
- 설정
#settings.py
INSTALLED_APP = [
...
'corsheaders',
]
# 순서 중요
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
...
]
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True
11. nested serializer
- 시리얼라이저 내에 또 다른 시리얼라이저를 넣어서 이중으로 연결되는 구조
- 예시
class PostSerializer(serializers.ModelSerializer):
profile = ProfileSerializer(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
class Meta:
model = Post
fields = ("pk", "profile", "title", "body", "image", "published_date",
"likes", "comments")
12. 필터링
- http://127.0.0.1:8000/posts?category=11&event=1 이런 형식의 url로 필터링 기능을 제공하는 방법
- 공식 패키지 설치
pip install django-filter
- settings.py 설정 (전역 설정을 하면 큐와 같은 코드에서 직접 불러오지 않아도 적용가능/전역에 적용 되므로 주의)
INSTALLED_APPS = [
'django_filters',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
],
}
- 뷰마다 filter_backends 설정
- settings.py에서 filter_backends를 DjangoFilterBackend로 설정 후 view에서 filterset_fields를 ['author', 'likes']로 설정
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.all()
permission_classes = [CustomReadOnly]
filter_backends = [DjangoFilterBackend]
filterset_fields = ["author", "likes"]
def get_serializer_class(self):
print("self.action : ", self.action)
if self.action in ["list", "retrieve"]:
print("call PostSerializer")
return PostSerializer
print("call PostCreateSerializer")
return PostCreateSerializer
def perform_create(self, serializer):
profile = Profile.objects.get(user=self.request.user)
serializer.save(author=self.request.user, profile=profile)
13. 페이징
- settings.py에 설정만 하면 전역으로 적용된다.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
],
'DEFAULT_PAGINATION_CLASS':
'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE':
3,
}
14. 현재 동작 방식 확인하는 방법
- ViewSet을 사용하면 self.action을 이용해 현재 처리하는 동작을 확인할 수 있다.
- 다른 뷰의 경우, self.request.method를 이용하면 확인할 수 있다.
15. 배포 준비
- allowed_hosts = ['.herokiapp.com', '127.0.0.1']
- docker를 사용하는 경우 docker의 게이트웨이 참조
- dj-databse-url : dj-database-url은 heroku 쪽의postgre에 연결하기 위한 패키지
- db_from_env = dj_database_url.config(conn_max_age=500)
- DATABASES['default'].update(db_from_env)
- heroku 서버에 업로드시 static이 깨지는 문제 해결
- 미들웨어 추가 : whitenoise.middleware.whitenoisemiddleware
- whitenoise 미들웨어는 django.middleware.security.SecurityMiddleware 의 하단부에 위치
16. 예외처리
- 기존 방법
{
"detail": "some error"
}
- 변경 하고자 하는 방법
{
"message": "",
"results": "",
"status": false,
"status_code": 400
}
- 코드
from rest_framework.views import exception_handler
from django.http import JsonResponse
def get_response(message="", result={}, status=False, status_code=200):
return {
"message": message,
"result": result,
"status": status,
"status_code": status_code,
}
def get_error_message(error_dict):
field = next(iter(error_dict))
response = error_dict[next(iter(error_dict))]
if isinstance(response, dict):
response = get_error_message(response)
elif isinstance(response, list):
response_message = response[0]
if isinstance(response_message, dict):
response = get_error_message(response_message)
else:
response = response[0]
return response
def handle_exception(exc, context):
error_response = exception_handler(exc, context)
if error_response is not None:
error = error_response.data
if isinstance(error, list) and error:
if isinstance(error[0], dict):
error_response.data = get_response(
message=get_error_message(error),
status_code=error_response.status_code,
)
elif isinstance(error[0], str):
error_response.data = get_response(
message=error[0],
status_code=error_response.status_code
)
if isinstance(error, dict):
error_response.data = get_response(
message=get_error_message(error),
status_code=error_response.status_code
)
return error_response
class ExceptionMiddleware(object):
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if response.status_code == 500:
response = get_response(
message="Internal server error, please try again later",
status_code=response.status_code
)
return JsonResponse(response, status=response['status_code'])
if response.status_code == 404 and "Page not found" in str(response.content):
response = get_response(
message="Page not found, invalid url",
status_code=response.status_code
)
return JsonResponse(response, status=response['status_code'])
return response
- settings.py 설정
MIDDLEWARE = [
...
"myboard.custom_exception_handler.ExceptionMiddleware",
]
REST_FRAMEWORK = [
...
"EXCEPTION_HANDLER": "myboard.custom_exception_handler.handle_exception",
]
17. drf_yasg로 api 문서화 하기
- 설치
pip install drf-yasg
- settings.py 설정
INSTALLED_APPS = [
'drf_yasg',
]
- urls.py 설정
from django.urls import path, include, re_path
from django.contrib import admin
from django.conf import settings
from django.conf.urls.static import static
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="게시판 API",
default_version="v1",
description="게시판 API 문서",
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path("admin/", admin.site.urls),
path("users/", include("users.urls")),
path("", include("posts.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += [
re_path(
r"^swagger(?P<format>\.json|\.yaml)$",
schema_view.without_ui(cache_timeout=0),
name="schema-json",
),
re_path(
r"^swagger/$",
schema_view.with_ui("swagger", cache_timeout=0),
name="schema-swagger-ui",
),
re_path(
r"^redoc/$", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"
),
]
- 접속 방법 : 두가지 다른 스타일의 문서로 접근할 수 있다.
- http://127.0.0.1:8000/swagger/
- http://127.0.0.1:8000/redoc/
'Study > django' 카테고리의 다른 글
List of Useful URL Patterns, 정규 표현에 대해 정리 (0) | 2023.08.23 |
---|---|
백엔드를 위한 django rest framework with 파이썬 책 후기 (0) | 2023.08.22 |
django-redis에서 scan_iter를 count 옵션과 함께 사용하는 방법 (1) | 2023.03.21 |
django-redis로 unlink 실행하는 방법 (0) | 2023.03.21 |
django-redis의 hset 사용법에 대해 chatGPT에 물어봤을때 (0) | 2023.03.20 |
댓글