이번 2차 프로젝트에서는
크게 User, Product, Order로 앱을 나누어 구성하였으며
그 중 내가 작업했던
1) User앱 전체
2) Product앱 중 상품 ListView, FilterView
코드들에 대한 리뷰를 해보려 한다.
import json
import jwt
import bcrypt
import re
import datetime
import requests
from django.views import View
from django.http import (
HttpResponse,
JsonResponse
)
from .models import User
from .utils import login_decorator
from order.models import Order, OrderStatus
from product.models import Product
from my_settings import (
SECRET_KEY,
ALGORITHM
)
from .models import User
class SignUpView(View):
def post(self, request):
#post로 받아야 하는 이유: User 정보를 DB에 저장할 것이므로
try:
data = json.loads(request.body)
# json 형식으로 frontend로부터 온 request 중 body에 담겨져 온 부분을 data라는 변수에 저장
if User.objects.filter(email=data['email']).exists():
return JsonResponse({'message': 'EMAIL_ALREADY_EXISTS'}, status=400)
# 유저가 등록하고자 하는 이메일이 email이 이미 존재할 경우 400 error
if '@' not in data['email']:
return JsonResponse({'message': 'INVALID_EMAIL'}, status=401)
# 유저가 등록하고자 하는 이메일에 @가 없을 경우 401 error
if len(data['password']) < 8:
return JsonResponse({'message': 'INVALID_PASSWORD'}, status=401)
# 유저가 등록하고자 하는 password가 8자리 미만일 경우 401 error
# 위의 조건에 걸리지 않은 경우에는 (else 굳이 쓰지 않아도 됨)
User.objects.create(
first_name = data['first_name'],
last_name = data['last_name'],
birthday = datetime.datetime.strptime(data['birthday'], '%Y.%m.%d').date(),
email = data['email'],
password = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
)
# json 형식의 data는 모두 dictionary형태로 오기 때문에 key값을 특정하는 할 때는 []를 사용.
# first_name, last_name, birthday, email, password 모두 저장
# birthday : data['birthday'] 값을 datetime 객체 중 strptime() 클래스 메서드를 사용하여
# '1999.9.19' 형태로 받아(date_string) 1999/09/19(format) 형태로 저장함
# 아래 사진을 참고하였음.
# password : data['password'] 값을 utf-8 형태로 encode하여 byte화 시킴.
# 이후 bcrypt.hashpw를 이용하여 암호화 (gensalt를 뿌려서)
# 맨 마지막 decode('utf-8')은 byte의 string화를 위해 필요.
return JsonResponse ({'message':'SIGNUP_SUCCESS'}, status=200)
except KeyError:
return JsonResponse ({'message':'INVALID_KEY'}, status=401)
먼저 위 코드의 가장 먼저 보이는 단점은
정규표현식을 작성하지 않아 좀 더 세밀하고 정밀한 validation 구현이 어려웠다는 것이다.
정규표현식을 사용한 리팩토링이 필요해보인다.
class SignInView(View):
def post(self, request):
# 이번에는 올바른 사용자인지 확인하여 토큰을 직접 발급해주어야하므로 post 메소드가 필요하다
try:
data = json.loads(request.body)
# 역시 이번에도 front로부터 들어온 request의 body 부분을 json으로 받아 data에 저장하고
if User.objects.filter(email=data['email']).exists():
user = User.objects.get(email=data['email'])
# 만일 email이 존재하는 email이라면 DB에서 해당 email에 해당하는 User 객체를 찾아 user 변수에 저장한다
if bcrypt.checkpw(data['password'].encode('utf-8'), user.password.encode('utf-8')):
access_token = jwt.encode({'user_id':user.id}, SECRET_KEY['secret'], ALGORITHM['algorithm']).decode('utf-8')
# SignUp에서 암호화 시에는 bcrypt의 hashpw 메서드를 사용하지만, SignIn에서는 암호화된 비밀번호에 대한 확인을 위해 checkpw 메서드를 사용한다.
# 유저가 입력한 password를 byte화 한 값과 유저가 입력한 email에 해당한 이미 등록된 유저의 password를 바이트한 값이 같을 경우에
# jwt를 이용하여 access_token을 발급해준다.
# 토큰발급: jwt.encode( 누구나 알아도 괜찮은 정보(대표적으로 고유 id) , 시크릿키 , 알고리즘 ).decode('utf-8')
return JsonResponse({'access_token':access_token}, status=200)
# 유효한 유저임이 확인되어 토큰이 발급된 것 == 로그인에 성공한 것
else:
return JsonResponse({'message':'UNAUTHORIZED'}, status=401)
else:
return JsonResponse({'message':'INVALID_USER'}, status=401)
except KeyError:
return JsonResponse({'message':'KEY_ERROR'}, status=400)
class MyPage(View):
@login_decorator
# 확실히 인증된(로그인이 끝난) 사용자만 볼 수 있는 페이지에 대한 뷰를 작성할 때마다
# 매번 로그인 절차를 쓰는 것이 상당히 비효율적이기 때문에 로그인 데코레이터를 user앱 내 utils.py에 작성하고 사용해준다.
def get(self, request):
# 사용자의 first_name만 띄워주는 목적이므로 get 메소드를 사용하였다.
return JsonResponse ({'first_name': request.user.first_name})
class MyOrder(View):
@login_decorator
def post(self, request):
# 먼저 cart에 담긴 아이템(order_status_id=1, order_status='pending') 중 front에서 주문하겠다고 요청한 상품들에 대해서만
# order_status를 'ordered'로 바꾸어 저장할 필요가 있다. (order_status_id=3)
# 데이터에 대한 저장이 이루어져야 하므로 post 메소드를 사용했다.
ordered_items = Order.objects.filter(order_status_id=1)
# order_status_id=1 즉 pending 상태인 모든 상품의 객체를 조회하여
for ordered_item in ordered_items:
ordered_item.order_status_id=3
ordered_item.save()
# for 문을 돌려 order_status_id를 3으로, 즉 상태를 ordered로 변화시켜 다시 저장한다.
return JsonResponse ({'message':'ORDER_COMPLETE'}, status=200)
# 이후 상품 주문 완료 메시지를 띄운다
@login_decorator
def get(self, request):
# order_status_id를 3으로 바꾸어 'ordered' 상태로 저장시킨 객체들을 이제 페이지에 실제로 보여줘야하는 메소드이므로
# get 메소드를 사용하였다.
ordered_items = []
# 주문 완료된 상품들을 채워 보여주기 위한 빈 배열을 먼저 선언한다.
order_items = Order.objects.filter(user_id=request.user.id, order_status_id=3).select_related("order_status").prefetch_related("product")
# 주문된 상품 중, 현재 로그인한 사용자의 주문된 상품만을 보여줘야하므로 filter 조건에는
# 1) 현재 로그인한 사용자의 (user_id=request.user.id) 2) 주문된 상품 (order_status_id=3)만을 골라
# Order 테이블이 정참조하고 있는 order_status 테이블은 select_related로,
# Order 테이블이 역참조하고 있는 product 테이블은 prefetch_related로 미리 queryset을 캐싱해 둔다.
# select_related와 prefetch_related는 모두 불필요한 쿼리를 줄일 수 있는 방법이므로 사용할 수 있는 조건이라면 무조건 사용하는 것이 유리하다.
for item in order_items:
data = {
"name" : item.product.name,
"price" : item.product.price,
"quantity" : item.quantity,
"color" : item.product.color.name,
"order_status" : item.order_status.name,
"image" : item.product.image_set.get(image_category=1).image_url
}
ordered_items.append(data)
# select_related와 prefetch_related로 미리 캐싱해둔 쿼리셋을 for문으로 돌며
# front와 미리 협의하여 주고받기로한 정보들을 dictionary 형식으로 만들어
# 미리 선언해둔 빈 배열에 append 시켜 JsonResponse로 렌더할 data list를 만든다.
# 이 때 select_related로 캐싱해둔 쿼리셋은 dot notation + [table명] + 속성명 으로,
# prefetch_related로 캐싱해둔 쿼리셋은 dot notation + [table명_set] + 객체 특정 + 속성명 으로 접근
return JsonResponse ({'MyOrder': ordered_items})
API는 MyPage, MyOrder 두 개를 따로 작성하였지만
실제 웹페이지에서는 한 페이지 내에서 렌더된다.
(아래 사진에서 좌측은 MyPage API, 우측은 MyOrders API)
이번 프로젝트를하며 select_related와 prefetch_related의 필요성을 확실히 느꼈다.
이제는 여태까지 select_related와 prefetch_related를 이해하기 위해 읽었던 수많은 포스팅들을 다시 보며
내가 다시 정리해볼 필요가 있을 것 같다.
class ProductListView(View):
def get(self, request):
# 상품을 보여주기만 하므로 역시 get메소드 사용
all_products = Product.objects.prefetch_related("image_set").all()
# image_url을 프론트쪽에 보내줘야 하므로 역시 prefetch_related를 이용해 미리 쿼리셋을 캐싱해둔다
products = [ {
"product_id" : product.id,
"product_name" : product.name,
"product_group" : product.group,
"product_img" : product.image_set.get(image_category_id=1).image_url,
"model_img" : product.image_set.get(image_category_id=2).image_url,
"product_price" : product.price
} for product in all_products ]
# 미리 캐싱해둔 쿼리셋을 돌며 속성값에 접근하여 front에 보내줄 리스트를 만든다
# 이 리스트 컴프리헨션을 구상하며 애를 좀 먹었다. 내가 참고했던 리스트 컴프리헨션 자료는 아래 사진이다.
return JsonResponse({'products' : products}, status=200)
category는 color, care, gift_card로 나눌 수 있고 (색조, 기초, 기프트카드 상품)
apply on은 lips, face, eyes 로 나눌 수 있다. (입술, 얼굴, 눈에 바르는 상품)
즉 category 별로 나누어진 상품을 모두 합쳐도 전체 상품이 나오고
apply on 별로 나누어진 상품을 모두 합쳐도 전체 상품이 나온다.
게다가 category와 apply on은 중복 선택이 가능하다. 즉 category=color&care, apply on=lips&eyes
이런 식으로의 필터링이 가능한 것.
이 API를 제대로 구현하기 위해서는 QueryString을 사용해야 했다.
QueryString을 이용한다는 것은
이런 식으로
내가 보내는 FilterView의 엔드포인트 뒤에 ? 가 생기고 그 이후 입력한 파라미터 값에 따라
필요한 데이터만 제공하게 되는 것이다.
파라미터는 위에서처럼 parameter = value
형태로 적어 보내진다.
중복 필터를 사용할 때처럼 여러개의 파라미터가 필요할 경우에는 &를 붙여 여러개의 파라미터를 보내게 된다.
그래서 최종적으로 [엔드포인트주소]?파라미터=값&파라미터=값
형식의 주소를 갖게 되는 것이다.
class FilterView(View):
def get(self, request):
category_names = request.GET.getlist('category_names', None)
applying_names = request.GET.getlist('applying_names', None)
# 백엔드에서 프론트로부터의 요청을 받을 때에는 request.GET.get을 이용한다. 다만 이번에 내 경우
# 중복 필터링을 가능하게 해야했기 때문에 request.GET.getlist를 사용하여 여러 개의 파라미터를 받을 수 있도록 처리했다.
if applying_names == []:
# 사용자가 apply on 필터를 설정하지 않은 경우, 즉 category만 필터하여 요청을 보내 오는 경우
found_products = Product.objects.select_related('category').prefetch_related('image_set').filter(category__name__in=category_names)
# 역시 select_related와 prefetch_related를 이용하여 쿼리셋을 미리 캐싱한다.
# 여기서 처음 사용해본 것은 filter(category__name__in = category_names) 부분인데
# category 테이블의 name이 parameter로 받은 category_names인 것만 캐싱하겠다는 쿼리이다.
products = [{
"product_id" : category_product.id,
"product_name" : category_product.name,
"product_group" : category_product.group,
"product_img" : category_product.image_set.get(image_category_id=1).image_url,
"model_img" : category_product.image_set.get(image_category_id=2).image_url,
"product_price" : category_product.price
} for category_product in found_products ]
return JsonResponse({'products': products}, status=200)
if category_names == []:
# 이번에는 사용자가 category 필터를 설정하지 않은 경우, 즉 apply on만 필터하여 요청을 보내오는 경우인데
for name in applying_names:
# 여기서는 위에서와 달리 포문을 한번 더 사용했다. (리팩토링하는 과정에서 위에 if applying_names==[] 부분만 고치고 이 부분은 고치지 않은 것 같음)
# 하지만 위에서처럼 포문을 빼고, filter(applying__name__in=applying_names)로 해주어도 똑같이 작동한다.
found_products = Product.objects.select_related('applying').prefetch_related('image_set').filter(applying__name=name)
products = [{
"product_id" : applying_product.id,
"product_name" : applying_product.name,
"product_group" : applying_product.group,
"product_img" : Image.objects.select_related('product').get(product_id=applying_product.id, image_category_id=1).image_url,
"model_img" : Image.objects.select_related('product').get(product_id=applying_product.id, image_category_id=2).image_url,
"product_price" : applying_product.price
} for applying_product in found_products ]
return JsonResponse({'products' : products}, status=200)
found_products = Product.objects.select_related('category').select_related('applying').prefetch_related('image_set').filter(category__name__in=category_names, applying__name__in=applying_names)
# category나 applying filter 중 하나만 필터하는 것이 아닌 둘 다 모두 필터링한 경우는 아래의 식을 타게 된다.
products = [{
"product_id" : category_applying_product.id,
"product_name" : category_applying_product.name,
"product_group" : category_applying_product.group,
"product_img" : Image.objects.select_related('product').get(product_id=category_applying_product.id, image_category_id=1).image_url,
"model_img" : Image.objects.select_related('product').get(product_id=category_applying_product.id, image_category_id=2).image_url,
"product_price" : category_applying_product.price
} for category_applying_product in found_products ]
return JsonResponse ({'products' : products}, status=200)
- 정규표현식
- List comprehension
- helper function
- unit_test
에 대한 필요성이 느껴진다.
이제 리팩토링 지옥으로 들어가보자. ㅎㅎㅎㅎ
references:
https://docs.python.org/ko/3/library/datetime.html (datetime, strptime)
https://jay-ji.tistory.com/35 (select_related, prefetch_related)
https://wikidocs.net/22805 (list comprehension)