Dev/Django

WebSite(wiselyshave) Clone Project - Part.3 views.py Refactoring

sincerely10 2020. 8. 2. 22:53
반응형

안녕하세요. 이 포스트는 시리즈 포스트로 구성되어있습니다.

<1차 Clone Project 회고>
WebSite(wiselyshave) Clone Project - Part1.시작&데이터모델링
WebSite(wiselyshave) Clone Project - Part.2 Data Modeling & End Point Refactoring
WebSite(wiselyshave) Clone Project - Part.3 views.py Refactorin
WebSite(wiselyshave) Clone Project - Part.4 후기

지난번 포스트에 이어 Refactoring에 대해 다뤄보겠습니다. 이번에는 가장 핵심적인 views.py를 다루겠습니다.

1. Product App views.py

Refactoring 이후 가장 급진적인 변화가 있던 Product App의 views.py 입니다. 그럴만한 게 새로운 모델링이 적용된 게 주로 products 부분이기 때문입니다. 그리고 굉장히 비효율적으로 참조했던 Django QuerySet도 실습해가면서 하나씩 다시 작성해보았습니다.

전체 코드는 제 GitHub Link에 올려 두겠습니다.

먼저 프로젝트 때 작성한 product의 상세정보를 확인하는 views.py 입니다.

1.1.a 프로젝트 - 상품 상세정보 조회 코드

# wisely clone Project/views.py
if category_name == 'color products':
    product_color   = ProductColor.objects.get(product_id=product_id,color_id=1)
    price           = f'{int(product_color.price):,}'
    product_info    = [{
        "product_name"        : product.name,
        "product_description" : product.description,
        "product_price"       : price
    }]
    return JsonResponse({'Info':product_info}, status=200)

if category_name == 'blade products':
    blade_product   = BladeProduct.objects.get(product_id=product_id)
    price           = f'{int(blade_product.price):,}'
    product_info    = [{
        "product_name"        : product.name,
        "product_description" : product.description,
        "product_price"       : price
    }]
    return JsonResponse({'Info':product_info}, status=200)

if category_name == 'size products':
    prefetch_product_size   = Product.objects.prefetch_related('productsize_set').get(id=product_id)
    product_size_data       = [{
        'product_name'     : product.name,
        'product_price'    : f'{int(product_size.price):,}',
        'product_size'     : product_size.size.name,
        'size_description' : product_size.size.description
        }for product_size in list(
            prefetch_product_size.productsize_set.filter(skin_type_id=1)|
            prefetch_product_size.productsize_set.filter(skin_type_id=None))]
    return JsonResponse({'Info':product_size_data},status=200)

우선 category라는 3가지 상품 선택(색상 선택, 면도날 선택, 사이즈 선택)으로 구분하여 출력해주었습니다. 제품별로 나타내는 정보도 다르기 때문에 key 값도 다릅니다. 다른 걸 떠나 우선 if가 많이 있다는게 특히 아쉬운 부분입니다. 프로젝트 때 작성한 모델링이 특히 아쉬운 게 이 부분이었습니다. API 작성 시, 항상 세 가지 케이스로 나눠서 처리해줘야 하기 때문입니다.

그리고 가장 아쉬운 건 Django QuerySet의 DB hit를 줄이는 참조 관계를 제대로 모르고 살짝만 사용했기 때문입니다. 마지막에 Product에서 ProductSize라는 

1.1.b Refactoring 상품 상세정보 조회 코드

부끄러운 views.py를 뒤로 하고 Refactoring 한 views.py를 확인해보겠습니다.

product         = Product.objects.prefetch_related('productoption_set').get(id=product_id)
product_options = product.productoption_set.select_related(
    'option__color',
    'option__size',
    'option__skin_type'
)
product_info    = [{
    "name"        : product.name,
    "description" : product.description,
    "price"       : product_options.first().price if not product_options.first().option.size else None,
    "color"       : [{
        "color_name" : color_option.option.color.name,
        "color_code" : color_option.option.color.code,
        "image_url"  : color_option.option.color.image_url
    } for color_option in product_options if product_options.first().option.color],
    "size"        : [{
        "size_name"     : size_option.option.size.name,
        "description"   : size_option.option.size.description,
        "price_by_size" : size_option.price,
        "skin_type"     : [{
            "skin_type_name" : size_option.option.skin_type.name
        } for skin_option in range(0,product_options.values('option__skin_type').distinct().count())
            if product_options.first().option.skin_type]
    } for size_option in product_options.filter(option__skin_type=product_options.first().option.skin_type) 
        if product_options.first().option.size]
}]

이 코드는 이 전 포스트에서 언급한 것과 같이 API 두 개가 합쳐진 형태입니다. 조금 더 상세하게 표현하자면 product에서 역참조 관계인 ProductOption이라는 테이블을 prefetch_related로 미리 가져옵니다.(1번 라인)
2번 라인에서 ProductOption에서 color, size, skin_type에 대해 DB hit를 줄이기 위해 select_related를 선언합니다.

그 아래에서 product_info를 선언하여 각 option을 모두 선언해줍니다. 색상, 스킨타입과 같은 테이블의 경우 for로 출력해주어야 하기 때문에 마찬가지로 list comprehension으로 보여줍니다. 여기서 선언되지 않은 타입을 가져오려 하면 에러가 발생합니다. 그래서 list comprehension에서 자료형이 있는 경우만 출력해줍니다.

물론 이 코드도 개선할 포인트들이 많이 있습니다. 따라서 최대한 줄이고 if를 줄여서 업데이트되는 코드를 GitHub에 지속적으로 업로드하겠습니다.

2. Orders App views.py

전체를 다 하기에 views.py 내에 class가 많기 때문에 대표적으로 장바구니에 담긴 아이템 리스트를 조회하는 코드와 대용량  상품 구매 코드를 기록해보겠습니다.

2.1.a 프로젝트 - 장바구니 아이템 리스트 조회

제가 프로젝트 때 작성했던 코드는 너무 길어져서 별도의 함수를 작성하였습니다.
order_id를 기입하면 상품 목록을 return 해주는 형태의 함수입니다.

user_id         = request.user.id
paid_user_order = Order.objects.get(
    user_id         = user_id,
    order_status_id = 1
)
product_color_list  = OrderItem.objects.filter(order_id=user_order.id,products_colors_id__isnull=False)
product_size_list   = OrderItem.objects.filter(order_id=user_order.id,products_sizes_id__isnull=False)
product_blade_list  = OrderItem.objects.filter(order_id=user_order.id,blade_products_id__isnull=False)
color_item_list = [{
    'item_id'        : item.id,
    'product_id'     : Product.objects.get(id=ProductColor.objects.get(id=item.products_colors_id).product_id).id,
    'item_name'      : Product.objects.get(id=ProductColor.objects.get(id=item.products_colors_id).product_id).name,
    'item_price'     : ProductColor.objects.get(id=item.products_colors_id).price,
    'list_price'     : item.quantity*ProductColor.objects.get(id=item.products_colors_id).price,
    'color'          : Color.objects.get(id=ProductColor.objects.get(id=item.products_colors_id).color_id).name,
    'discount_price' : item.discount_price,
    'description'    : Product.objects.get(id=ProductColor.objects.get(id=item.products_colors_id).product_id).descrip
    'image_url'      : OrderImage.objects.get(products_colors_id=item.products_colors_id).image_url,
    'quantity'       : item.quantity,
    } for item in product_color_list
]
size_item_list = [{
    'item_id'        : item.id,
    'product_id'     : Product.objects.get(id=ProductSize.objects.get(id=item.products_sizes_id).product_id).id,
    'item_name'      : Product.objects.get(id=ProductSize.objects.get(id=item.products_sizes_id).product_id).name,
    'item_price'     : ProductSize.objects.get(id=item.products_sizes_id).price,
    'list_price'     : item.quantity*ProductSize.objects.get(id=item.products_sizes_id).price,
    'size'           : Size.objects.get(id=ProductSize.objects.get(id=item.products_sizes_id).size_id).name,
    'discount_price' : item.discount_price,
    'description'    : Product.objects.get(id=ProductSize.objects.get(id=item.products_sizes_id).product_id).descripti
    'image_url'      : OrderImage.objects.get(products_sizes_id=item.products_sizes_id).image_url,
    'quantity'       : item.quantity,
    } for item in product_size_list
]
blade_item_list = [{
    'item_id'        : item.id,
    'product_id'     : Product.objects.get(id=BladeProduct.objects.get(id=item.blade_products_id).product_id).id,
    'item_name'      : Product.objects.get(id=BladeProduct.objects.get(id=item.blade_products_id).product_id).name,
    'item_price'     : BladeProduct.objects.get(id=item.blade_products_id).price,
    'list_price'     : item.quantity*BladeProduct.objects.get(id=item.blade_products_id).price,
    'discount_price' : item.discount_price,
    'description'    : Product.objects.get(id=BladeProduct.objects.get(id=item.blade_products_id).product_id).descript
    'image_url'      : OrderImage.objects.get(blade_products_id=item.blade_products_id).image_url,
    'quantity'       : item.quantity
    } for item in product_blade_list
]
order_status =  [{
    'order_id'       : user_order.id,
    'shipping_price' : user_order.shipping_price,
    'discount_price' : user_order.discount_price,
    'total_price'    : user_order.list_price - user_order.discount_price
}]
item_list = color_item_list + size_item_list + blade_item_list + order_status
return item_list

위 코드는 각 상품 리스트를 출력해주는 함수입니다. products의 views.py에도 언급했었지만 각 상품 구매별로 나눠서 list에 담기 때문에 비효율적입니다.

2.1.b Refactoring - 장바구니 아이템 리스트 조회

아래에 개선한 형태의 item list를 출력해주는 Refactoring한 코드를 작성하였습니다.

user_id    = request.user.id
user_order = Order.objects.get(
    user_id         = user_id,
    order_status_id = 1
)
items = OrderItem.objects.select_related(
    'products_option__product',
    'products_option__option__size',
    'products_option__option__color',
    'products_option__option__skin_type'
).filter(order_id=user_order.id)
user_order_item_list = [{
    'item_id'        : item.id,
    'product_id'     : item.products_option.product.id,
    'item_name'      : item.products_option.product.name,
    'item_price'     : item.products_option.price,
    'list_price'     : item.quantity * item.products_option.price,
    'color'          : item.products_option.option.color.name if item.products_option.option.color else None,
    'size'           : item.products_option.option.size.name if item.products_option.option.size else None,
    'discount_price' : item.discount_price,
    'description'    : item.products_option.product.description,
    'image_url'      : item.products_option.image_url,
    'quantity'       : item.quantity
} for item in items]
return JsonResponse({'message':user_order_item_list}, status=200)

select_related를 이용해 products_option이라는 테이블에서 참조하는 4개의 테이블을 기록합니다. 마찬가지로 list comprehension을 통해 참조가 가능한 속성에 대해 출력해줍니다.

2.2.a 프로젝트 - 대용량 상품 구매 코드

이 코드도 마찬가지로 모델링 때문에 굉장히 비효율적이게 되었습니다. 관련 로직은 사이트 장바구니 담기에서 자세하게 보시면 더 잘 이해하실 수 있을 겁니다. 면도날과 같은 경우 아무런 옵션 없이 개수만 선택할 수 있습니다. 반면 쉐이빙젤은 이 페이지에서는 고정 사이즈이지만 제품 자체는 사이즈 선택이 가능합니다. 그리고 애프터 쉐이브는 사이즈에 피부 타입도 선택을 해야 합니다.

프로젝트를 진행할 때, 프론트엔드와 협의하여 전체 대용량 구매의 경우 애프터 쉐이브는 건성으로만 구매되도록 정하였습니다. 이렇게 기능을 덜었는데도 코드가 상당히 길어져서 면도날 선택 시라는 옵션만 기재하겠습니다.

bulk_item_list = data['Info']
user_id        = request.user.id
for item in bulk_item_list:
    if not Order.objects.filter(user_id=user_id,order_status_id=1).exists():
        Order(
            shipping_price  = 2500,
            list_price      = 0,
            discount_price  = 0,
            total_price     = 0,
            order_status_id = 1,
            user_id         = user_id
        ).save()
    user_order = Order.objects.get(
        user_id         = user_id,
        order_status_id = 1
    )
    if item['product_id'] == '3':
        if not OrderItem.objects.filter(
            blade_products_id = BladeProduct.objects.get(id = 1).id,
            order_id          = user_order.id
        ):
            OrderItem(
                order_id            = user_order.id,
                blade_products_id   = BladeProduct.objects.get(id=1).id,
                quantity            = 0,
                discount_price      = 0
            ).save()
        product_item  = OrderItem.objects.get(
            blade_products_id = BladeProduct.objects.get(product_id = item['product_id']).id,
            order_id          = user_order.id
        )
        product_item.quantity   += int(item['quantity'])
        item_price               = float(BladeProduct.objects.get(id = product_item.blade_products_id).price)
        if product_item.quantity == 2:
            product_item.discount_price = product_item.quantity * item_price * 0.07
            product_item.discount_price = round_up(product_item.discount_price,-2)
        if product_item.quantity == 3:
            product_item.discount_price = product_item.quantity * item_price * 0.15
            product_item.discount_price = round_up(product_item.discount_price,-2)
        if product_item.quantity >= 4:
            product_item.discount_price = product_item.quantity * item_price * 0.2
            product_item.discount_price = round_up(product_item.discount_price,-2)
        product_item.save()
        user_order.list_price     += product_item.quantity * BladeProduct.objects.get(id=product_item.blade_produ
        user_order.discount_price += Decimal(product_item.discount_price)

Request는 Info로 요청되어야 합니다. 그 Info의 value로 Dict 자료형이 list로 되어 있습니다. 상세 API는 이전 포스트에 올린 API Docs에서 확인하실 수 있습니다.

기존에 이 유저에 해당하는 장바구니 상태의 order(order_status_id=1)가 없다면 해당하는 order를 생성해줍니다. 그리고 해당 order에 request로 들어온 상품이 없다면(order_items라는 테이블에 request의 상품이 없다면) 요청받은 수량만큼 더 해줍니다. 있다면 생성하지 않고 바로 추가만 해줍니다.

그리고 대용량 구매의 할인 정책에 맞게 할인율을 적용해줍니다. 저는 round_up이라는 별도의 함수를 만들어서 할인가를 적용했습니다. 이 할인이 10원 단위에서 올림을 했기 때문입니다.

비교적 전체 로직은 크게 달라질 것은 없었습니다. 다만, 로직을 구현하는 과정에서 활용한 일부 하드코딩이 좀 있었습니다. 그리고 'if item['product_id'] == '3':' 과 같은 케이스 별로 작성을 해줘야 한다는 것이 가장 큰 흠이었습니다. 만약 면도기 관련 제품뿐만 아니라 여러 제품을 판다고 가정했을 때, 모든 케이스를 직접 작성해줘야 할 가능성도 커지기 때문이죠.

2.2.b Refactoring - 대용량 상품 구매 코드

Refactoring 한 코드를 확인해보겠습니다. 프로젝트 코드에서는 위 길이만 한 코드가 2개 더 있는 상태인데 반해 아래 코드는 거의 전체를 복붙 했는데도 간결하고 보기 편하게 느껴집니다.

data           = json.loads(request.body)
bulk_item_list = data['Info']
user_id        = request.user.id
for item in bulk_item_list:
    if not Order.objects.filter(
        user_id         = user_id,
        order_status_id = OrderStatus.objects.get(name="장바구니").id
    ).exists():
        Order(
            shipping_price  = 2500,
            list_price      = 0,
            discount_price  = 0,
            total_price     = 0,
            order_status_id = 1,
            user_id         = user_id
        ).save()
    user_order   = Order.objects.get(
        user_id         = user_id,
        order_status_id = OrderStatus.objects.get(name="장바구니").id
    )
    size_id = Size.objects.get(name=item['size_name']).id if Size.objects.filter(name=item['size_name']).exists() else None
    skin_type_id = SkinType.objects.get(name=item['skin_type_name']).id if SkinType.objects.filter(name=item['skin_type_name']).exists() els
    option_id       = Option.objects.get(size_id=size_id,skin_type_id=skin_type_id,color_id=None).id
    products_option = ProductOption.objects.get(product_id=item['product_id'],option_id=option_id)
    if not OrderItem.objects.filter(
        products_option_id = products_option.id,
        order_id            = user_order.id
    ).exists():
        OrderItem(
            products_option_id = products_option.id,
            order_id           = user_order.id,
            quantity           = 0,
            discount_price     = 0
        ).save()
    order_item  = OrderItem.objects.get(
        products_option_id = products_option.id,
        order_id           = user_order.id
    )
    order_item.quantity += item['quantity']
    item_price           = float(products_option.price)
    if order_item.quantity == 2:
        order_item.discount_price = order_item.quantity * item_price * 0.07
        order_item.discount_price = round_up(order_item.discount_price,-2)
    if order_item.quantity == 3:
        order_item.discount_price = order_item.quantity * item_price * 0.15
        order_item.discount_price = round_up(order_item.discount_price,-2)
    if order_item.quantity >= 4:
        order_item.discount_price = order_item.quantity * item_price * 0.2
        order_item.discount_price = round_up(order_item.discount_price,-2)
    order_item.save()
    user_order.list_price     += order_item.quantity * products_option.price
    user_order.discount_price += Decimal(order_item.discount_price)
    if user_order.list_price >= 15000:
        user_order.shipping_price = 0
    user_order.save()
return JsonResponse({'message':'SUCCESS'}, status=200)

전체 논리구조는 크게 달라지진 않았지만, id 값을 적는 대신 '장바구니'와 같은 naming을 통해 id를 찾게 되니 이 코드가 어떤 역할을 하는지 명확해졌습니다. 추가적으로 skin_type 또한 받을 수 있습니다.

여기까지가 views 코드의 Refactoring이었습니다. 이전에 말씀드린 것처럼 더 줄이고 효과적이게 된다면 갱신하도록 하겠습니다.

반응형