[DRF] JWT와 ORM 심화

집중맞은 도둑력·2024년 4월 24일

웹 개발

목록 보기
11/14
post-thumbnail

0. 🔖 목차


  1. JWT
    1-1. 쿠키와 세션
    1-2. JWT 소개
  2. ORM 심화
    2-1. 더 복잡한 쿼리
    2-2. ORM 최적화

1. JWT


1-1. 쿠키와 세션

쿠키

쿠키는 사용자의 컴퓨터에 저장되는 작은 텍스트 파일이다.

웹 사이트를 방문할 때 서버가 사용자의 브라우저에 데이터를 저장하도록 요청하고, 이후 사용자가 같은 웹 사이트를 다시 방문하면 브라우저가 이 쿠키를 서버에 전송하여 사용자를 인식하게 한다.

보통 사용자가 사이트에 로그인 상태를 유지하거나, 장바구니에 담긴 상품을 기억하는 데 사용된다.

세션

HTTP는 무상태 특성 때문에 서버와 한 번 요청과 응답을 주고 받으면 이를 기억하지 않는다.

쿠키와 세션을 사용하면 이러한 문제를 해결할 수 있다.

유저가 서버에 로그인 요청을 하여 통과되면 서버는 사용자의 쿠키에 세션 ID를 저장하도록 한다.

이후 유저는 서버에 다른 요청을 할 때 쿠키의 세션 ID를 같이 보내고 서버는 세션 DB에 해당 세션 ID가 있는지 확인을 하여 유저의 로그인 상태를 유지한다.

쿠키와 세션의 단점

쿠키는 사용자의 웹 브라우저에 저장되기 때문에 웹 브라우저를 사용하는 환경이 아니라면 사용할 수 없다는 단점이 있다.

1-2. JWT 소개

JWT란?

Token Auth 방식을 사용하면 장치에 구애받지 않는 인증을 할 수있으며 JWT방식이 많이 사용된다.

JWTJSON Web Token의 약자이다.

장점

토큰 자체가 필요한 정보(유저의 pk, 만료 시간 등)를 포함하고 있어서, 서버는 토큰만 확인하면 되므로 사용자의 로그인 상태 등을 저장할 필요가 없어 간단해진다. 즉, 세션이 필요가 없다.

단점

세션 DB가 없기 때문에 모든 기기 로그아웃, 현재 접속 유저 관리 등의 처리가 불가능하다.

Token 자체가 데이터를 담고 있는 정보이기 때문에 탈취당하면 보안에 취약해진다.

이러한 단점을 해결하기 위해 Access Token과 Refresh Token을 사용한다.

Access Token

요청할 때 인증을 위해 헤더에 포함되어야 하는 토큰이다.

매번 요청을 할 때 마다 보내는 토큰이기 때문에 보안이 취약하다.

따라서 만료기한을 매우 짧게 잡아서 탈취를 당하더라도 빠르게 만료되도록 하여 문제를 해결한다.

Refresh Token

Access Token이 만료되었을때 새로 Access Token을 발급받기 위한 Token이다.

Access Token 보다 유효기간이 길며 사용자의 기기에 저장해두고 사용한다.

만약 Refresh Token이 만료되었다면 다시 로그인을 해야한다.

이러한 Refresh Token이 탈취당했을 때를 보완하기 위해 Black List 등의 DB를 이용한 방식을 사용하기도 한다.

1-3. JWT 구현

장고는 다 해줍니다

DRF에는 Access Token과 Refresh Token을 쉽게 생성하고 검증하는 라이브러리가 존재한다.

  1. pip install djangorestframework-simplejwt 터미널에서 simplejwt 설치
  2. settings.py에 REST_FRAMEWORK 추가
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
}
  1. url에 회원가입, refresh API 추가
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"),
]

3가지 절차를 마치면 아래처럼 회원가입 및 refresh를 할 때 토큰이 응답으로 주어진다.

  1. JWT 시간 제한
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,
}

접근 제한

인증이 필요한 view에 아래 코드처럼 APIView의 permission_classes 변수를 오버라이딩 하면 해당 view는 이제부터 헤더의 Authorization 키에 대한 벨류로 유효한 access token이 있는 요청만 사용이 가능하다.

from rest_framework.permissions import IsAuthenticated

class AricleListAPIView(APIView):

    # permission_classes 변수 오버라이딩
    # 이걸 명시하면 이제부터 이 클래스 뷰는 요청 헤더의 Authorization에 유효한 access 토큰이 있어야만 들어올 수 있음
    permission_classes = [IsAuthenticated,]

2. ORM 심화


2-1. 더 복잡한 쿼리

Q()

각 조건을 &, |, ~등의 연산자로 연결하여 더 복잡한 조건을 줄 수 있다.

from django.db.models import Q
Product.objects.filter(
	Q(price__gt=15000) | Q(quantity__lt=3000)
)

F()

쿼리를 작성할 때 기존 필드 값에 의존하는 값을 참조할 수 있다.

from django.db.models import F
Product.objects.update(price = F('price') + 1000)

annotate()

쿼리셋으로 반환하기 전 각 레코드마다 특정 연산을 통해 추가적인 필드를 생성하여서 반환할 수 있다.

from django.db.models import F, annotate
products = Product.objects.annotate(
    total_price=F('price') * F('quantity')
)

aggregate()

반환된 쿼리셋 내의 데이터 전체에 대한 연산 결과를 반환할 수 있다.

from django.db.models import AVG, aggregate
Product.objects.aggregate(Avg('price'))
# {'price__avg': 14627.76}

Product.objects.aggregate(my_avg = Avg('price'))
# {'my_avg': 14627.76}

raw

raw를 통해 SQL 쿼리를 직접 작성하여 실행할 수 있다.

다만 "id" 필드가 필수적으로 들어가야 한다.

categories_count = Product.objects.raw(
'''
SELECT "id", "category", COUNT("category") AS "category_count" 
FROM "products_product" 
GROUP BY "category"
'''
)

Group By는 어떻게?

values로 그룹핑 하고자 하는 필드를 먼저 가져오고 annotate로 해당 필드를 집계 함수로 표현해주면 된다.

from django.db.models import values, Count
products = Product.objects.values('category')
products.annotate(category_count = Count('category'))

2-2. ORM 최적화

지연로딩(Lazy Loading)

대부분의 ORM은 지연로딩이라는 특징이 있다.

지연로딩이란 ORM 명령어를 실행한 뒤 바로 쿼리문이 실행되는게 아니라 실제로 데이터가 사용될 때 쿼리가 실행된다는 특징이다.

이렇게 하면 불필요한 쿼리를 방지하고 메모리 사용량과 데이터베이스의 부담을 줄일 수 있다.

N+1 문제

관계형 데이터베이스에서 지연로딩을 사용할 경우 관련된 객체를 조회하기 위해 N개의 추가 쿼리가 발생하고 실행되는 문제다.

comments = Comment.objects.all()
for comment in comments: => 실제로 comments 데이터를 사용함
	print(f"{comment.id}의 글제목") => 이미 불러온 데이터라 쿼리가 실행되지 않음
	print(f"{comment.article.title}") => article id 조회 쿼리 발생

ORM을 통해 Comment 테이블을 받아오고 실제로 데이터를 사용했을 때 SQL 쿼리문이 실행되면서 Comment 테이블을 가져오지만 Comment 테이블과 관련된 Article 테이블 데이터는 사용되지 않아서 불러오지 않는다.

이후에 코드에서 Comment 테이블의 각 레코드에서 Article 데이터를 정참조한다면 그때마다 Article 데이터를 참조하는 쿼리문이 실행되어 N번의 쿼리가 더 실행된다.

이를 N+1 문제라고 한다.

즉시로딩(Eager Loading)

ModelClass.objects.filter(조건절)
	.select_related('정방향 참조') # JOIN
	.prefetch_related('역방향 참조') # Additional Query

ORM을 작성할 때 관련된 테이블도 같이 불러오게 만들 수 있다.

select_related는 정참조인 경우에 실제 쿼리에서 Join을 통해 연관 데이터를 모두 가져온다.

prefetch_related는 역참조인 경우에 쿼리를 두 번 보내서 관련된 테이블 두 개를 메모리에 불러와서 연관 데이터를 모두 가져온다.

2-3. Silk

Django를 위한 프로파일링 및 검사도구다.

  1. pip install django-silk를 통해 다운받는다.
  2. settings.py의 미들웨어와 설치된 앱 목록에 추가해준다
MIDDLEWARE = [
    ...
    "silk.middleware.SilkyMiddleware",
    ...
]

INSTALLED_APPS = (
    ...
    "silk"
)
  1. urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))]를 통해 프로파일링 화면 url을 만들어준다.
profile
틀린_내용이_있다면_말해주세요.

0개의 댓글