>

TIL

11주차 Day 1. 장고 이해도 테스트, DRF, Serializer 활용하기, JWT

ekdud 2024. 9. 2. 12:38

📑

     

    장고 기초 이해도 테스트(답안 정리)

    • Django의 MTV 패턴에서 각 요소(Model, Template, View)의 역할을 간단히 설명하세요.

    더보기

    내 답안 

    Model: 데이터베이스 테이블 정의 및 데이터 CRUD.
    Template: 웹 페이지 렌더링. 
    View: 모델에서 데이터를 받아서 템플릿에 전달. 요청에 대한 응답을 반환한다.

     

    모범답안

    Model: 데이터베이스 구조를 정의하고, 데이터를 관리하는 역할을 합니다. 데이터베이스 테이블과 매핑되며, 데이터를 저장, 조회, 수정, 삭제하는 기능을 제공합니다.

    Template: 사용자에게 보여지는 화면을 담당합니다. HTML 파일과 같은 템플릿을 통해 데이터를 시각적으로 표현하며, 동적인 웹 페이지를 생성하는 데 사용됩니다.

    View: 사용자의 요청을 처리하고, 적절한 데이터를 가져와서 템플릿과 결합하여 응답을 생성하는 역할을 합니다. View는 Model과 Template 사이에서 중개자 역할을 합니다.

     

    Django의 Custom UserModel을 사용하는 이유와 장점을 설명하세요.

    더보기

    내 답안 

    Custom UserModel을 사용하면 사용자 정의 필드를 추가하거나 기존 필드를 수정해서 프로젝트의 목적에 부합하도록 사용자 모델을 만들 수 있다. 보안, 사용자 데이터 관리 측면에서 용이하다.

     

    모범답안

    Django의 기본 User 모델을 사용하는 대신 Custom UserModel을 사용하면, 사용자 모델을 확장하거나 수정할 수 있어 프로젝트의 요구사항에 맞게 사용자 정보를 관리할 수 있습니다. 예를 들어, 추가적인 사용자 필드가 필요하거나 로그인 방식(예: 이메일로 로그인)을 변경하고 싶을 때 유용합니다. Custom UserModel을 사용하면 향후 확장성도 더 좋아지고, 프로젝트 시작 시 이러한 커스터마이징을 도입하면 나중에 구조 변경이 필요할 때 발생할 수 있는 문제를 예방할 수 있습니다.

     

    Django에서 Model 클래스는 데이터베이스 테이블과 매핑됩니다. Django ORM에서 모델을 정의할 때 필드를 정의할 수 있는 다양한 옵션 중 ManyToManyField에 대해 구체적인 사용 사례를 들어 설명하세요.

    더보기

    내 답안 

    ManyToManyField는 두 모델 간의 다대다 관계를 정의할 때 사용한다. 예를 들어 쇼핑몰 웹 사이트의 찜하기 기능을 구현할 때, 고객과 상품을 다대다 관계로 정의할 수 있다. 고객은 여러 상품을 찜할 수 있고, 한 상품은 여러 고객에게 찜하기가 될 수 있기 때문이다.

     

    모범답안

    Django의 ManyToMany는 두 모델간의 다대다 관계를 정의할 때 사용됩니다. 다대다 관계란 한 모델의 여러 인스턴스가 다른 모델의 여러 인스턴스와 연결될 수 있는 경우를 말합니다. 예를 들어, 하나의 학생이 여러 과목을 수강할 수 있고, 동시에 하나의 과목을 여러 학생이 수강할 수 있는 상황이 있을 수 있습니다.

    Django는 이러한 관계를 처리하기 위해 자동으로 중간 테이블을 생성하며 이 테이블에는 두 모델의 외래 키가 저장되어 있습니다

     

    Post 모델이 다음과 같은 필드를 가진다고 가정할 때, Create 코드를 작성하세요.

    • title: CharField(max_length=100)
    • content: TextField()
    • created_at: DateTimeField(auto_now_add=True)
    더보기

    내 답안

    - form객체를 전달하고 있지 않다..!

    def post_create(request):
        if request.method == 'POST':
            form = PostForm(request.POST)
            if form.is_valid():
                form.save()
                return redirect('post_list')
        else:
            form = PostForm()
        return render(request, 'posts/form.html')

     

    모범답안

    def post_create(request):
        if request.method == 'POST':
            form = PostForm(request.POST)
            if form.is_valid():
                form.save()
                return redirect('post_list')
        else:
            form = PostForm()
        return render(request, 'posts/form.html', {'form': form})

     

     

     


     

     

     

    DRF with Relationship

    Realationship이 있는 데이터를 직렬화하여 제공해보자

     

    Comment CRUD 작성하기

    - API 설계

      Method Endpoint
    특정 Article의 댓글 조회 GET /articles/<int:article_pk>/comments/
    새로운 댓글 작성 POST /articles/<int:article_pk>/comments/
    댓글 수정 PUT /articles/comments/<int:comment_pk>/
    댓글 삭제 DELETE /articles/comments/<int:comment_pk>/

     

     

    - 코드 작성

     

    models.py

    from django.db import models
    
    # Create your models here.
    class Article(models.Model):
        title = models.CharField(max_length=120)
        content = models.TextField()
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)
    
    class Comment(models.Model):
        article = models.ForeignKey(Article, on_delete=models.CASCADE, related_name="comments")
        content = models.TextField()
        created_at = models.DateTimeField(auto_now_add=True)
        updated_at = models.DateTimeField(auto_now=True)

     

     

    urls.py

    from django.urls import path
    from . import views
    
    app_name="articles"
    urlpatterns = [
        ...
        path("<int:article_pk>/comments/", views.CommentListAPIView.as_view(), name="comment_list"),
        path("comments/<int:comment_pk>/", views.CommentDetailAPIView.as_view(), name="comment_detail"),
    ]

     

     

    serializers.py

    from rest_framework import serializers
    from .models import Article, Comment
    
    ...
    
    class CommentSerializer(serializers.ModelSerializer):
        class Meta:
            model = Comment
            fields = "__all__"
            read_only_fields = ("article",)  # 읽기만 해도 오류가 발생하지 않도록 함.

     

     

    views.py

    from rest_framework.decorators import api_view
    from rest_framework.response import Response
    from rest_framework import status
    from rest_framework.views import APIView
    from django.shortcuts import get_object_or_404
    from .models import Article
    from .serializers import ArticleSerializer, CommentSerializer
    
    ...
    
    class CommentListAPIView(APIView):
        def get_object(self, pk):
            return get_object_or_404(Article, pk=pk)
        
        def get(self, request, article_pk):
            # 역참조
            article = self.get_object(article_pk)
            comments = article.comments.all()
            serialzer = CommentSerializer(comments, many = True)
            return Response(serialzer.data)
        
        def post(self, request, article_pk):
            article = self.get_object(article_pk)
            serializer = CommentSerializer(data=request.data)
            if serializer.is_valid(raise_exception=True):
                serializer.save(article=article)
                return Response(serializer.data, status=status.HTTP_201_CREATED)
            
    
    class CommentDetailAPIView(APIView):
        def get_object(self, pk):
            return get_object_or_404(Comment, pk=pk)
        
        def put(self, request, comment_pk):
            comment = self.get_object(comment_pk)
            serializer = CommentSerializer(comment, data=request.data, partial=True)
            if serializer.is_valid(raise_exception=True):
                serializer.save()
                return Response(serializer.data)
    
        def delete(self, request, comment_pk):
            comment = self.get_object(comment_pk)
            comment.delete()
            data = {"pk": f"{comment_pk} is deleted."}
            return Response(status=status.HTTP_204_NO_CONTENT)

     

     

    API 9개 완성!

     

     

     

    Serializer 활용하기

    Nested Relationships

    : Serializer는 필드를 오버라이드하거나 추가적인 필드를 구성할 수 있으며, 이때 모델 사이에 참조 관계가 있다면 해당 필드를 포함하거나 중첩할 수 있다.

     

    from rest_framework import serializers
    from .models import Article, Comment
    
    class CommentSerializer(serializers.ModelSerializer):
        class Meta:
            model = Comment
            fields = "__all__"
            read_only_fields = ("article",)
    
    class ArticleSerializer(serializers.ModelSerializer):
        comments = CommentSerializer(many=True, read_only=True)
        comments_count = serializers.IntegerField(source="comments.count", read_only=True)
    
        class Meta:
            model = Article
            fields = "__all__"

    ArticleSerializer클래스에 comments를 추가해 역참조가 가능하도록 함.

    comments - 기존에 존재하는 매니저 이름으로 된 필드를 다시 override한 것. Django가 자동으로 추가해주는 매니저.

    comments_count - 직접 필드를 추가해줘야 함. source 속성으로 데이터 값을 전달할 수 있고, 여기서는 Queryset API 중에 count()를 이용해 전달함. 

     

    *source: SerializerField의 속성으로 해당 필드를 채우는 데 사용하는 속성을 지정. 점 표기법으로 내부 속성에 접근.

     

     

     

    Serializer Method Fields

    🔗공식문서

    from django.contrib.auth.models import User
    from django.utils.timezone import now
    from rest_framework import serializers
    
    class UserSerializer(serializers.ModelSerializer):
        days_since_joined = serializers.SerializerMethodField()
    
        class Meta:
            model = User
            fields = '__all__'
    
        def get_days_since_joined(self, obj):
            return (now() - obj.date_joined).days

     get_ 가 붙은 필드명의 함수가 실행되어 필드로 추가된다.

     

     

    Custom Fields

    🔗공식문서

     

    보여지는 부분만 변경하고 싶다면?

     

    serializers.py

    from rest_framework import serializers
    from .models import Article, Comment
    
    class CommentSerializer(serializers.ModelSerializer):
        class Meta:
            model = Comment
            fields = "__all__"
            read_only_fields = ("article",)
    
        def to_representation(self, instance):
            ret = super().to_representation(instance)
            ret.pop("article") # 보여지는 부분에서 빼는 것.
            return ret

     

     

     


     

     

    Token Auth with JWT

    Cookie는 웹 브라우저에만 존재하는 것으로 다양한 장치들과 공통적으로 사용할 수 있는 방식이 필요하다. 그 방식으로 널리 사용되는 방법 중 하나가 Token Auth인데, 그중에서도 JWT 방식이 많이 사용된다. JWT 방식으로 Auth를 처리하면 SessionDB나 인증을 위한 여러 가지 로직 처리가 필요치 않다.

    *Token: 랜덤하게 생긴 문자열. 일정한 규칙(포맷)을 가지고 있고 간단한 서명을 더한 문자열로 토큰 자체에 유저에 대한 간단한 정보가 들어있는 형태이다. JWToken 문자열+포맷+서명.

     

     

     

     

    JWT와 세션의 차이

    - 세션 데이터베이스가 존재하지 않으며 데이터베이스가 필요하지 않음.

    - 토큰 자체가 하나의 인증 데이터.

    - 서버는 토큰이 유효한지만 검증하여 처리.

     

     

    JWT의 장단점

    ⭕ 서버에서 관리하는 데이터가 없으므로 복잡한 처리 로직이 필요하지 않음.

    일방적으로 로그인을 무효화하는 등의 처리가 불가능(세션테이블이 없어서.)

           모든 기기 로그아웃 기능, 현재 접속 유저 관리 등이 불가능..

    ⭕ 세션이나 DB없이 유저를 인증하는 것이 가능.

    ❌ Token 자체가 데이터를 담고 있는 정보이므로 탈취 당할 시 보안이 취약함.

     

     

     

     

    JWT의 처리 방식

    1. 클라이언트가 ID/PW를 서버로 보냄.

    2. 서버에서 ID/PW를 검증하고, 유효하다면 일정한 형식으로 서명 처리된 Token을 응답.

    3. 이후 클라이언트는 모든 요청 헤어에 토큰을 담아 서버로 요청을 전송.

    4. 서버는 해당 토큰의 유효성을 검증하고 유저의 신원과 권한을 확인 후 요청을 처리.

     

     

    JWT를 좀 더 가시적으로 보여주는 사이트(토큰복호화 사이트)  🔗jwt.io

    JWToken을 decode하면 ' . '을 기준으로 Header, Payload, Verify Signature(서명)의 세 부분으로 나눠진다.

    토큰을 디코딩하면 정보를 누구든 볼 수 있기 때문에 민감한 정보는 절대 담지 않도록 한다.

     

    *Signature - 페이로드의 글자 하나만 달라져도 서명이 완전히 다른 문자열로 변환되어 서버의 비밀키 값(서버만 알고 있음)을 모른다면 유효한 서명값을 만들어내는 것이 불가능함. 서버는 토큰을 받으면 Header + Payload + 비밀키로 (헤더에 명시된 암호 알고리즘으로) 생성한 서명값이 토큰의 서명값과 일치하는지를 확인하는 과정을 거쳐서 유효성 여부를 확인함. 서명의 유효여부 + 유효기간 내의 토큰인지 확인하여 AUTH 과정을 처리. 

     

     

     

    JWT 인증 방식은 장점도 많지만 단점도 확실하다.. 탈취당하면 끝이기 때문 ㅜㅜ

    Access 토큰은 요청할 때 인증을 위해 헤더에 포함해야하는 토큰인데, 매 요청시 보내는 토큰이라 보안에 취약함. 따라서 만료기한을 짧게 잡아, 탈취당하더라도 짧은 시간 내에 유효하지 않은 토큰 형태가 되도록 한다.

     

    Refresh 토큰은 Access Token이 만료되었을 때 새로 Access Token을 발급받기 위한 토큰으로, Access Token보다 긴 유효기간을 가진다. 주로 사용자의 기기에 저장해두고 사용되며 만약 Refresh Token까지 만료되었다면 다시 인증(로그인) 과정이 필요하다. Refresh Token가 탈취되는 문제를 보완하기 위해 DB 리소스를 사용하는 다양한 방식이 존재한다 ex. BlackList 등

    🔗블로그 참고

     

     

     

     

    직접 구현해보기 전에 !!

     

    일단 accounts앱 생성

     

    models.py

    from django.db import models
    from django.contrib.auth.models import AbstractUser
    
    # Create your models here.
    class User(AbstractUser):
        pass

     

     

    프로젝트의 urls.py에 accounts.urls 연결.

    from django.contrib import admin
    from django.urls import path, include
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path("api/v1/articles/", include("articles.urls")),
        path("api/v1/accounts/", include("accounts.urls")),
    ]

     

     

    settings.py에 AUTH_USER_MODEL 설정 + INSTALLED_APPS에 accounts추가

     

     

    SuperUser 생성

     

     

    seeding

     

    $ python manage.py seed articles --number=30 

    => articles의 모든 모델에 데이터 각각 30개씩 생성

    $ python manage.py seed articles --number=20 --seeder "Comment.article_id" 1 

    => 특정 모델에 20개 생성

     

     

    JWT 구현하기

    🔗공식문서

    $ pip install djangorestframework-simplejwt

    $ pip freeze > requirements.txt 도 잊지말기.

     

    settings.py에 추가해주기↴

    REST_FRAMEWORK = {
        "DEFAULT_AUTHENTICATION_CLASSES": [
            "rest_framework_simplejwt.authentication.JWTAuthentication",
        ],
    }

     

    urls.py

    from django.contrib import admin
    from django.urls import path, include
    from rest_framework_simplejwt.views import (
        TokenObtainPairView,
        TokenRefreshView,
    )
    
    urlpatterns = [
        path("signin/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
        path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
    ]

     

     

    공식문서 보고 하기 🔗

    from datetime import timedelta
    ...
    
    SIMPLE_JWT = {
        "ACCESS_TOKEN_LIFETIME": timedelta(minutes=1),
        "REFRESH_TOKEN_LIFETIME": timedelta(days=1),
        "ROTATE_REFRESH_TOKENS": True,
        "BLACKLIST_AFTER_ROTATION": True,
    }

     

    그리고 INSTALLED_APPS에 "rest_framework_simplejwt.token_blacklist" 추가

     

     

    유저가 아니면 Article에 접근하는 것을 제한하도록 해보자.

    #articles의 views.py
    ...
    from rest_framework.permissions import IsAuthenticated
    ...
    
    class ArticleListAPIView(APIView):
        permission_classes = [IsAuthenticated]
    
        ...

    동일하게 다른 클래스(ArticleDetailAPIView, CommentListAPIView, CommentDetailAPIView)에도 다 적어준다.

     

    Token을 이용해서 인증할 수 있으며, Postman에서 Access Token을 Authorization - Bearer Token타입에 줘보면 제대로 인증되는 것을 볼 수 있다.

     

     

     

    Token 유저정보는 어떻게 가져올까?

    ⇒ request.user을 이용해 User객체에 접근할 수 있다.