📑
장고 기초 이해도 테스트(답안 정리)
• 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)
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객체에 접근할 수 있다.