> Wecode 2차 프로젝트 회고록 - StockX 클론 ShockX

Sua·2021년 3월 13일
3

Project

목록 보기
2/2
post-thumbnail

Project Overview

위코드에서의 두 번째 프로젝트가 끝났다. StockX 사이트는 프로젝트 발표할 때부터 재미있는 사이트라고 생각했는데, 클론할 수 있어서 감사했다. 내가 만약 신발 마니아라면 애용하고 싶을 정도로 잘 만든 사이트다. 또, 두 번 모두 좋은 팀원들과 함께해서 미래에 있을 행운을 끌어다 쓴 게 아닐까 싶을 정도다. 우리 팀의 이름은 놀라게 하자는 의미로 'ShockX'라고 지었다.

🙅‍♀️ StockX 클론 프로젝트

StockX는 미국 온라인 리셀 중개 사이트이다. 주로 유명 브랜드의 패션아이템을 경매 방식으로 사고 팔 수 있다. 주식처럼 어떤 상품의 시세가 얼마나 오르고 내렸는지 등 사용자에게 다양한 통계정보를 시각화하여 제공하고 있다.

아래는 우리 ShockX팀이 클론한 사이트이다.

기간

2021.03.02-2021.03.12 (11일)

팀원

프론트엔드 3명 / 백엔드 4명

백엔드 기술 스택

  • Django
  • Python
  • MySQL
  • Bcrypt, JWT
  • 카카오 소셜 로그인 API
  • Git & GitHub
  • AWS EC2, RDS

커뮤니케이션

  • Trello
  • Notion
  • Slack

백엔드 주요 구현 사항

✅ 표시는 내가 구현한 기능

모델링

  • 관계형 데이터베이스 모델링 ✅
  • 데이터 csv 파일 작성, 데이터 업로더 작성 ✅

회원가입 & 로그인(User)

  • 카카오 소셜 로그인
  • 로그인 데코레이터를 통한 토큰 인증

상품(Product)

  • 상품 리스트페이지 조회(조건별 필터링)
  • 상품 상세페이지 조회(사이즈별 통계 정보 제공)

주문(Order)

  • 경매 방식의 상품 구매, 판매 시스템 구현 ✅
  • 마이페이지 구매, 판매 내역 조회 ✅
  • 포트폴리오 상품 등록, 조회 ✅

개인 목표

1차 프로젝트 때는 상품 API를 했는데, 이번에는 주문 API를 맡게 되었다. 사용자가 주문하고, 주문 내역을 확인하는 플로우를 경험해볼 수 있어서 좋았다.

그런데 StockX는 미국에서 유명한 리셀 사이트로 일반적인 커머스 사이트와는 시스템이 조금 다르다. 사용자는 구매자 또는 판매자가 되어 자신이 원하는 가격을 제시할 수 있고, 구매가격과 판매가격이 매칭될 때 거래가 이루어진다. 사이트의 비즈니스 로직을 제대로 이해하고, 프론트엔드와 많이 소통하면서 로직을 짜야겠다고 생각했다.

Project Review

👍 잘한 점!

소통

1차 때는 프론트엔드와 많이 소통을 하지 못한 점이 아쉬웠다. 그래서 이번에는 좀 더 적극적으로 소통을 해보려고 했다. 다행히도 프론트엔드분들께서 먼저 적극적으로 이야기를 나누려고 했고, API 구현이 되면 계속 맞춰보고 수정할 수 있었다!

특히, Notion에 API문서를 만들어서 프론트엔드와 키를 맞추고, 어떤 url로 접근해야 하는지 소통했다.

또한, Trello의 체크리스트를 활용했다. 하나의 엔트포인트에 세부적인 목표를 둬서, 체계적으로 관리를 할 수 있었다.

이외에도 내가 맡지 않은 백엔드 파트도 계속 참견(?)해가면서 전체적인 흐름을 보려고 노력했다.

모델링과 DB 생성

1차 때 모델링을 빠르게 끝내야 한다는 교훈을 얻어서 이번에는 백엔드 팀원들이 머리를 맞대로 빨리 모델링을 끝내려고 했다. 한 번 해본 경험이 있어서 모델링 초안은 빨리 끝났다.

하지만 구매 기록과 판매 기록 테이블을 어떻게 만들지, 배송 정보는 어떻게 관리할 것인지 등 몇 가지 이슈가 생겼다. 열띤 토론을 통해 의견을 주고 받으면서 모델링을 완성했다. 혼자 모델링을 했다면 놓칠 수 있는 부분을 보완할 수 있었던 것 같다.

그리고 모델링을 기반으로 사이트에 필요한 데이터를 생성했다. 크롤링을 할 수 없었기에 직접 데이터를 만들어야 했다. 특히, stockx는 주식처럼 이전의 거래기록을 확인할 수 있는 사이트였기에 유의미한 거래 기록을 만드는 데 초점을 두었고 약 3000건의 거래 기록을 만들어냈다.

👎 아쉬웠던 점!

Git 활용

이번에 git rebase를 처음 사용하게 되었는데, 익숙해지는데 시간이 오래 걸려서 여러 사람들의 도움을 받았다. rebase를 하다 중간에 코드가 없어져서 당황했는데, git reset hard를 쓰면 된다고 해서 코드를 살려내기도 했다. 언제 git과 낯을 안 가리게 될까.. 한 번 제대로 파봐야겠다.

💻 공유하고 싶은 코드

database transaction 적용

stockx 사이트는 구매 또는 판매를 할 때 배송 정보를 함께 입력해야 한다. 따라서 주문 로직에서 배송과 관련된 shipping information 테이블과 주문과 관련된 bid, ask, order 테이블이 함께 수정된다.

이렇게 두 개의 이상의 테이블을 수정할 때는 database transaction를 적용해야 한다.

트랜잭션은 일련의 작업들이 마치 하나의 작업처럼 취급되어서 모두 다 성공하거나 아니면 모두 다 실패하는 걸 의미한다.

예를 들어, 배송 정보는 생성되었는데 중간에 에러가 나서 주문이 완료되지 않았다면 배송정보와 주문정보 간의 미스매칭이 발생할 것이다. 따라서 배송정보와 주문정보 생성을 하나의 유닛으로 취급해서 모두 다 성공하거나 모두 다 실패하도록 만들어준다.

django에서 트랜잭션을 적용하는 것 자체는 어렵지 않았다. transaction.atomic()를 사용하면 된다. 대신 언제 트랜잭션이 필요하고 왜 필요한지를 아는 것이 더 중요하다고 생각한다.

사실 이 부분에서 트랜잭션을 적용해야 한다는 사실조차 알지 못했다. 멘토님의 리뷰를 받고 적용하게 되었는데, 찾아보니 위코드 2주차 Database 세션에서 언급했던 내용이었다. 그 때는 데이터베이스에 대해 잘 몰랐던 때여서 눈에 안 들어왔는데, 프로젝트를 진행하면서 왜 필요한지 확 깨달을 수 있었다.

            with transaction.atomic():
                shipping_information, created = ShippingInformation.objects.get_or_create(
                    ...
                )

                order_status_current = OrderStatus.objects.get(name=ORDER_STATUS_CURRENT)
                order_status_pending = OrderStatus.objects.get(name=ORDER_STATUS_PENDING)
                
                # bid일 때
                if bool(int(is_bid)):
                    if not expiration_date:
                        raise KeyError
                    
                    Bid.objects.create(
                        ...
                        shipping_information = shipping_information
                    )

                    return JsonResponse({'message':'SUCCESS'}, status=201)
                
                # buy일 때
                if not total_price:
                    raise KeyError

                bid = Bid.objects.create(
                    ...
                    shipping_information = shipping_information
                )

                if not product_size.ask_set.filter(order_status__name=ORDER_STATUS_CURRENT).exists():
                    raise ProductSize.DoesNotExist

                lowest_ask  = product_size.ask_set.filter(order_status__name=ORDER_STATUS_CURRENT).order_by('price').first()

                lowest_ask.order_status = order_status_pending
                ...
                lowest_ask.save()

                Order.objects.create(bid=bid, ask=lowest_ask)
                
                return JsonResponse({'message':'SUCCESS'}, status=201)
        
        except KeyError:
            return JsonResponse({'message':'KEY_ERROR'}, status=400)
        except ProductSize.DoesNotExist:
            return JsonResponse({'message':'ASK_DOES_NOT_EXIST'}, status=404)

깔끔하게 코드 작성하기.. 언더바언더바와 닷의 미학

Djanog ORM에서 __ 언더바언더바와 .를 잘 쓰면 깔끔하게 코드를 작성할 수 있다. 또한 우리는 관계형 데이터베이스를 사용하고 있으므로 연결되어 있는 테이블은 참조 관계를 통해 가져오는 게 더 적절하다.

이걸 잘 활용 못했을 때는 이런 피드백을 받을 수 있고,

잘 활용하면 이렇게 칭찬받을 수 있다.

class PortfolioView(View):
    @login_decorator
    def get(self, request):
        user = request.user

        portfolios = Portfolio.objects.select_related('product_size', 'product_size__product', 'product_size__size')\
                .filter(user=user).prefetch_related('product_size__ask_set')

        portfolio_products = [{
            'name'           : portfolio.product_size.product.name,
            'size'           : portfolio.product_size.size.name,
            'purchase_date'  : portfolio.purchase_date.strftime('%Y/%m/%d'),
            'purchase_price' : int(portfolio.purchase_price),
            'market_value'   : int(portfolio.product_size.product.productsize_set\
                    .filter(ask__order_status__name=ORDER_STATUS_HISTORY)\
                    .annotate(size_avg=Avg('ask__price')).aggregate(total_avg=Avg('size_avg'))['total_avg'])
            } for portfolio in portfolios
        ]

        return JsonResponse({'portfolio':portfolio_products}, status=200)

🛠 개선하고 싶은 코드

메모리 캐싱

바로 위의 포트폴리오 API에서 개선해야 하는 부분이 있다면 바로 메모리 캐싱이다.
일반적으로 select_related와 prefetch_related를 사용해서 쿼리문 갯수가 많이 줄기는 했지만, 여전히 많은 수의 쿼리가 날아간다. 아무래도 filter와 annotate, aggregate를 사용해서 그런 것 같은데 이 부분은 좀 더 공부해서 리팩토링해야겠다. 그리고 메모리 캐싱 세션에서 Redis라는 것을 살짝 언급해주셨는데 공부해서 적용해봐야겠다.

리팩토링 후기 : Django | Prefetch 객체, annotate 활용하여 쿼리 최적화하기

유닛테스트

TDD 테스트 주도 개발이 중요하다는 걸 들어만 봤는데 드디어 그 첫걸음인 유닛테스트를 작성해보았다. 처음 하는 것이라 어찌어찌 유닛테스트 코드를 짜긴 했는데 먼저 로직을 짜고 거기에 끼워맞추는 느낌이다. TDD에 대해서 공부를 하고 탄탄한 로직을 가진 코드를 짤 수 있도록 노력해야겠다는 생각이 든다.

✨우리 많이 성장했다!

프로젝트 마지막날 금요일, 클론한 사이트를 발표하는 시간을 가졌는데, 한 팀도 빠짐없이 완성도 높은 결과물을 만들어내서 너무 멋있었고 감동했다! 다들 잠도 많이 못자고, 여러 상황들로 힘들어하는 사람들도 많았는데, 정말 함께 해서 가능한 일이 아니었나 싶다. 이제 우리 17기는 한 달 동안 기업협업으로 뿔뿔이 흩어지게 되는데 다들 얼마나 성장해올지 궁금하다. 화이팅~

profile
Leave your comfort zone

12개의 댓글

comment-user-thumbnail
2021년 3월 13일

캬 역시 성실한 수아님 프로젝트 회고록도 모두가 노는 토요일에 올리시다니...! 깔끔한 성격의 수아님답게 코드도 후기도 깔끔하네요!!! 기업협업에서도 더더 성장하는 수아님이 되시기를 바랍니다...! ><

1개의 답글
comment-user-thumbnail
2021년 3월 13일

역시 수아님...👍👍👍 언더바의 장인 칭호는 수아님꺼에요__!!! 많이 본받고있어요! 2차프로젝트 shockX 너무 멋있었구 수고많으셨습니다!! 기협같이 못나가게되어 너무 아쉽네요ㅠㅠㅠ 꼭 따로 만나용!!🥰🥰🥰

1개의 답글
comment-user-thumbnail
2021년 3월 13일

수아님 역시.. 모범생.... 수아님 회고록 클론 프로젝트 해야겠네요 ㅋㅋㅋ 고생하셨어요!!

1개의 답글
comment-user-thumbnail
2021년 3월 14일

필터링, 언바 장인 수아님..🍒 항상 느끼지만 수아님은 설명을 정말 잘하시는 것 같아요!! 글도 코드도 깔끔한 수아님..🤲
한번도 같은 팀이 못돼 슬프지만.. 저는 항상 멀리서 수아님을 응원하고 있습니다 >< 그래도 계속 같은 방을 써서 얼굴 볼 수 있어 좋았어요!! 그녀 떡볶이 코트... 넘 잘어울리잖아~~
하트 누르고 갑니다!! 💕 저희 금요일에 만나요!!! 고생 많으셨습니다❤️❤️

1개의 답글
comment-user-thumbnail
2021년 3월 14일

수아님~ 이번 프로젝트때 같이 맞춰봐서 너무 좋았어요!!!! 🥰 밤늦게 지하철역으로 걸어가는길에 마이페이지 해보자는 얘기 나오고 그날 새벽 5시에 푸쉬하셨던거 저는 잊지 못합니다 😭 기업협업때도 화이팅하세용~!!!!

1개의 답글
comment-user-thumbnail
2021년 3월 17일

수아 님~ 고생 많으셨습니다__. ㅎㅎ 스윗홈 잊은 건 아니시죠? 😂 어디에서나 긍정적으로 열심히 하는 모습에 힘을 얻고 갑니다 수아 님 빠셍💪💪

1개의 답글