/ PROJECT, 프레시코드, WECODE, WANTED

백엔드 프리온보딩 1주차 두번째 프로젝트 회고


원티드 1주차 프레시코드 기업과제


진행기간

21.11.04 ~ 21.11.06


구현해야 하는 기능

  • 로그인 기능
    • 사용자 인증을 통해 상품 관리를 할 수 있어야 한다
    • JWT 인증 방식을 이용한다
    • 상품 추가/수정/삭제는 admin권한을 가지 사용자만 이용할 수 있다
  • 상품 관리 기능
    • 상품 조회는 하나 또는 전체 목록을 조회할 수 있으며 전체 목록은 페이징 기능이 있다
    • 한 페이지당 아이템 수는 5개이다
    • 사용자는 상품 조회만 가능하다
    • 관리자는 상품 추가/수정/삭제를 할 수 있다

나의 역할

  • 상품 관리 기능(상품 목록 조회, 상품 페이징 처리, 메뉴 수정, 메뉴 태그 수정)
  • 모델링 설계(공통)
  • PostMan API 명세 작성

배운내용

  • unit test 코드 구현
  • 기본 환경 세팅에 있어 팀원 컨벤션 맞추기

프로젝트 회고

과제가 주어지면 팀원들과 가장 먼저 진행하는 것은 과제 기능에 대한 모델링이다. 모델링 설계가 제대로 진행되어야 기능 개발 할때 어려움이 덜 할 것이라는 것을 모두들 알고 있었다. 프레시코드의 API과제에서는 유저가 관리자와 사용자로 나뉘어져 있어야 했다. 관리자만이 상품 C, U, D가 가능했고 사용자는 상품 조회만이 가능했다.

모델링 설계를 진행할때 고려했던 점은 서비스가 확장되는 것을 고려하여 테이블을 생성했다. Badge, Size, Tag의 테이블을 따로 두었고 Item, Menu 테이블에서 FK로 참조하는 방식으로 모델을 설계했다.

이 과정에서 모델 정규화 라는 단어를 쓰게 되었는데 모델링을 설계하며 데이터의 중복을 방지하고 이후 c,r,u,d를 구현하는 과정에서 문제 없이 진행할 수 있게 스키마를 설계하는 방식이 바로 모델 정규화라는 것을 이번에 제대로 이해하게 되었다.

이 과제에서 나는 상품 관리 기능 중 상품 조회와 상품 수정 부분을 구현하게 되었다. 이 과정에서 조금 고민이 되었던 부분이 존재했다. 메뉴를 수정하는 것과 메뉴가 가지고 있는 태그부분을 수정하는 것을 어떻게 진행해야 하는지 고민이 되었다. 처음에 하나의 view에서 메뉴와 메뉴의 태그를 모두 수정하는 로직을 작성하였다. 이후 코드를 보게 되니 if~else 파티였다. ^^ 코드 가독성면에서 좋지 않은것 같았고 그래서 다른 팀원의 의견을 묻게 되었다.

그리고 들었던 피드백은 굳이 메뉴 수정과 메뉴에 해당되는 태그를 같은 view에서 진행할 필요가 있냐는 것였다. 나는 당연히 메뉴를 수정하는 로직이니 같은 view에다 넣은게 당연하다고 생각했다. 그런데 팀원의 의견을 들어보니 메뉴 수정하는 view와 메뉴의 태그를 수정하는 view가 나뉘는 것이 가독성 면에서도 훨씬 나아질 것 이라는 생각이 들었다.

피드백을 들은 후 MenuDetailView class에서 제품의 name과 description을 수정하는 로직을 구현하였고 MenuItemsView라는 새로운 class를 만들어 이곳에서 메뉴에 해당하는 item들의 정보를 업데이트 하는 로직을 구현하였다.

메뉴가 존재하는지 안하는지 조건을 걸기 위해 first라는 쿼리문을 사용하였지만 exists라는 더 명확한 쿼리문이 존재하였고 다른 팀원이 이를 언급해주었다. 어떻게 보면 굉장히 사소해 보이지만 이런 부분을 피드백 해주는 덕분에 코드의 가독성과 효율적인 로직에 대한 생각을 많이 하게 되는 것 같았다

 data = json.loads(request.body)
           
 if not Menu.objects.filter(id=data["menu_id"]).exists(): # if not Menu.objects.filter(id=data["menu_id"]).first():
    return JsonResponse({"message": "MENU_NOT_FOUND"}, status=404)


ListView 기능 구현

그 다음으로 적절한 조건처리가 되지 않아 배포후에 발생하는 에러가 ListView에서 나왔다.. 밤을 새고 있었기에 팀원들에게 굉장히 미안했다..
아래 로직에서 badge 부분의 적절한 조건 처리를 해주지 않아 실제 badge에 들어가는 데이터가 없으면 에러가 발생했다. badge는 해당 제품이 신상품인지 best 상품인지 이런 데이터가 들어가야 하는 부분이다. 실제로 아무런 데이터가 들어가지 않을 수 있으니 그 부분을 처리해주어야 한다. 하지만 나는 단순히 product.badge.name 으로 name을 가져오게만 처리하여 데이터가 들어가지 않았을때를 생각해주지 않았다. 이 덕분에 30-40분이라는 시간이 더 걸린듯 했다..

적절한 조건 처리와 에러처리에 굉장히 신경써야 겠다는 생각을 이날 새벽에 다짐하고 또 다짐했던 것 같다. 휴

menus = {
    "id"          : product.id,
    "category"    : product.category.name,
    "name"        : product.name,
    "description" : product.description,
    "isSold"      : product.is_sold,
    "badge"       : product.badge.name if product.badge is not None else None,
    "items" : [
        {
            "item"   : item.id,
            "memuID" : item.menu.id,
            "size"   : item.size.name,
            "price"  : item.price,
            "isSold" : item.is_sold,
            } for item in product.item_set.all()],
    "tags" : [
        {
            "id"     : product.tag.id,
            "menuID" : product.id,
            "type"   : product.tag.type,
            "name"   : product.tag.name
        }
    ]
}
        

unit test

그 다음으로 고민이였던 부분은 unit test였다. 사실 unit test작성에 미숙하였기에 정해놓았던 시간안에 unit test를 전부 작성하지 못했다. (이것도 새벽에 일어난 일이라 나 대신 unit test를 도와 작성해주신 팀원에게 감사하다..) unit test에 대한 로직 작성을 제대로 이해하지 못하고 view에서 해주는 일까지 unit test에 작성하게 되면서 더 꼬여버렸다. test를 위해서 필요한 테이블은 모두 create해주어야 한다.

  • 필요한 테이블은 create한다. 이때 id는 직접 지정해주어야 한다
  • 생성한 테이블은 test이후에 모두 삭제되어야 한다. 이때 참조되어 있는 순으로 먼저 삭제해주어야 에러가 나지 않는다
  • 성공 케이스의 경우 성공할 수 밖에 없는 데이터를 넣어서 요청만 하면 된다. 나머지는 view의 로직들이 해결해 줄 것이다
  • 실패 케이스의 경우 view에서 작성한 에러의 경우를 생각해서 이에 맞게 실패하는 케이스를 만들어주면 된다. 마찬가지로 view에서 상황에 맞게 실패 메시지와 status를 반환해 줄 것이기 때문에 이에 맞는 메시지와 status만 잘 작성해주면 된다.
  • FK등으로 참조하고 있는 곳에 객체를 넣을때는 그냥 간단하게 menu_id = 1 이런식으로 넣어주자. 굳이 menu = menu.objects.get(id=1) 이런식으로 넣어줄 필요가 없다 !

리펙토링 후 코드

코드 리뷰 후 리펙토링 하는 시간을 가졌다

제품을 수정하는 과정에서 아래와 같이 각 제품이 가지고 있는 여러 아이템이 존재하고 있었다

menus: [
    {
      id: 245,
      category: "SALAD",
      name: "깔라마리 달래 샐러드",
      description: "해산물 샐러드",
      isSold: false,
      badge: "NEW",
          items: [
            {
              id: 1,
              menuId: 245,
              name: "미디움",
              size: "M",
              price: 8000,
              isSold: false,
            },
            {
              id: 2,
              menuId: 245,
              name: "라지",
              size: "L",
              price: 10000,
              isSold: false,
            },
          ],
    }


  • 초반에는 MenuDetailView라는 class에서 put메소드에서 menu의 데이터와 items를 한번에 수정하는 로직을 작성했다
  • 기능구현이 끝난 후 코드를 바라보니 하나의 view에 너무 많은 if-else문이 사용되고 있었고 menu와 item에 대한 view를 따로 구현을 하는게 가독성면과 효율성면에서 더 좋을 것 같다는 생각이 들었고 팀원들과 상의 후 로직을 수정했다
# 메뉴 수정 view

class MenuDetailView(APIView):
	@authorizer
    def put(self, request, menu_id):
        try:
            if request.user.role_id != Role.Type.ADMIN.value:
                return JsonResponse({"message": "UNAUTHORIZED"}, status = 401)

            if not Menu.objects.filter(id=menu_id).exists():
                return JsonResponse({"message": f"POSTING_{menu_id}_NOT_FOUND"}, status=404)
            
            data = json.loads(request.body)

            if not (data["name"] or data["description"]):
                return JsonResponse({"message": "No input"}, status=404)

            menu = Menu.objects.get(id=menu_id)
            
            if data["name"]:
                menu.name = data["name"]
            
            if data["description"]:
                menu.description = data["description"]

            menu.save()

            return JsonResponse({"menu name": menu.name, "menu desc": menu.description}, status=200)

        except KeyError:
            return JsonResponse({"message": "KEY_ERROR"}, status=400)
# 아이템 수정 view

class MenuItemsView(APIView):
    @authorizer
    def put(self, request, item_id):
        try:
            if request.user.role_id != Role.Type.ADMIN.value:
                return JsonResponse({"message": "UNAUTHORIZED"}, status = 401)

            data = json.loads(request.body)
            
            item = Item.objects.get(id=item_id)

            if not (data["price"] or data["size"]):
                return JsonResponse({"message": "NO_INPUT"}, status=404)

            if data["price"]:
                item.price = data["price"]

            if data["size"]:
                item.size_id = Size.objects.get(size=data["size"]).id

            item.save()

            return JsonResponse({"item price": item.price, "item size": item.size.name}, status=200)

        except KeyError:
            return JsonResponse({"message": "KEY_ERROR"}, status=400)

        except ValueError:
            return JsonResponse({"message": "VALUE_ERROR"}, status=404)

        except Item.DoesNotExist:
            return JsonResponse({"message": "ITEMS_NOT_FOUND"}, status=404)

        except Size.DoesNotExist:
            return JsonResponse({"message": "SIZE_NOT_FOUND"}, status=404)

로직을 중간에 수정하려고 하다보니 생각보다 많은 시간이 걸렸지만 코드의 효율성면을 따졌을땐 코드 수정은 잘 한 선택같았다 ! ㅎㅎㅎ 물론 시간이 배로 걸렸지만 내 의견을 존중해주고 더 나은 쪽을 흔쾌히 선택해주었던 팀원들에게 고마웠다 ! 코드를 수정하며 들었던 생각은 코드를 짤때는 잘 모른다는 것이다. 다 짜고 난 뒤에야 고쳐야 할 부분들, 비효율적인 코드 로직들이 보인다


진행한 소감

익숙치 않은 unit test작성, 아직 미숙한 팀원끼리의 커뮤니케이션으로 또다시 밤을 새게 되었다. 아주 꼴딱 새버렸다.. 모든 팀원들이 지친게 눈에 보였고 어떻게든 프로젝트가 마무리 되었지만 누구 하나 웃을수 없는 상황이 되었다. 아무리 서로 배려한다고 해도 밤을 새운건 모든 사람들에게 굉장한 체력소모가 되버렸다. 무사히 프로젝트는 제출되었지만 이날 제대로 잠을 잘 수가 없었다. unit test를 제대로 작성하지 못한 것과 ListView의 조건 처리를 제대로 하지 못한 부분이 마음에 걸렸다.

unit test는 이후에 완벽하게 숙지하고 이해해서 다음 프로젝트는 똑같은 일로 문제가 발생하지 않게 신경써야겠다. 조건 처리 부분에 대해서는 나의 부주의였던 것 같다. 나는 완벽히 구현되었다고 생각했지만 그렇지 못했다. 앞으로는 조건처리, 에러처리에 대해 처음부터 생각하고 로직을 작성해야 겠다는 생각을 하게 된 것 같다.

혼자 프로젝트를 진행하는것과 다른점은 내가 하는 작은 실수가 큰 실수가 된다는것이다.

다음 프로젝트부터는 이런 부분을 신경쓰고 진행 할 것이다. 똑같은 실수를 하지 않기 위해서 !
처음의 실수는 누구나 할 수 있다고 생각한다. 하지만 똑같은 실수를 되풀이 한다는 것은 있어서는 안된다 !!