Study/django

백엔드를 위한 django rest framework with 파이썬

bluebamus 2023. 8. 22.

reference : https://github.com/taebbong/drf_for_backend

 

GitHub - TaeBbong/drf_for_backend

Contribute to TaeBbong/drf_for_backend development by creating an account on GitHub.

github.com

 

* 책을 읽고 학습에 필요한 부분 정리

 

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

 

GitHub - TaeBbong/React-Board

Contribute to TaeBbong/React-Board development by creating an account on GitHub.

github.com

 - 현재 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/

댓글