결과적으로 팀이 꾸려지고 난 뒤 제일 처음 한 것이,
지금 와서 느낀 것이지만,
--> 그래서 처음부터 모든 기능을 고려하여 확장가능한 모델링을 하려고 노력했다.
우리팀은 제일 처음 약속한 방식이 있었다.
프론트, 백 따로 기능 열심히 만들다가 마지막 순간에 맞추는 일만 없었으면 좋겠다고 ..
그래서 우리는 scrum 방식으로 agile 하게 프로젝트를 진행하려는 노력을 정말 많이했다.
다음은 그 디테일을 소개하겠다.
앞서 언급한 것 처럼,
sprint 를 할 때 한 기능을 죽어라 다 만들고
이후에 그것을 프론트랑 맞춰보지 않았다.
어떤 기능이 동작 할 만한 최소한의 코드가 완성되면, 공유된 키값을 바탕으로 프론트엔드랑 바로 맞춰봤다.
즉, (development & test) 흐름을 기능이 최소한으로 완성될 때 마다 계속 반복했다.
한번에 만들고 테스트를 해버리면 어딘가 잘못되면 찾기가 너무 힘들기 때문이다 ..
실제로 한번은 어떤 식으로 데이터를 주고 받을지 완벽하게 이야기가 되지 않은 상태에서 데이터 구조를 먼저 정했다가 프론트엔드(래영님) 에게 혼란을 드린적이 있었는데,
이것 역시 초기에 test 를 하지 않았으면 발견하지 못했을 부분이다. 아마 로직을 수정하느라 시간을 허비했을 것이 분명하다. 이래서 agile 한 방식이 중요하구나 하는 것을 느꼈다.
개인별 topic : 팀원 별로 돌아가면서 자기가 이 때까지 구현한 것은 무엇인지, 오늘 할 task 는 무엇인지, blocker (bottle neck) 은 무엇이였는지 에 대해 이야기했다.
이 과정에서 서로가 어느 부분까지 진행하고 있는지 확인하며 흐름을 맞추려고 했으며, 팀원이 blocker 가 있으면 서로가 아는 부분만큼 도와주려고 애썼다.
프론트엔드/백엔드 별 topic : 그 날 프론트엔드와 백엔드가 맞춰야 할 사항들을 이야기했다.
예를들어 백엔드 쪽에서 나는 "오늘 래영님하고는 장바구니 api 통신을 2시에 맞춰보고, 송희님이랑 3시에 카테고리 별 상품분류 페이지 api 부분 맞춰보는게 어떨까요?" 와 같이 프론트엔드 팀원분들과 api 통신을 테스트하기 위한 세부 일정을 맞추려고 노력했다.
팀 전체 topic : 팀 전체가 나아가야 할 방향에 대해 이야기했다. 2주라는 시간 안에 배민문방구의 기능들을 모두 다 구현하는 것이 사실 불가능했기 때문에, 작업상황을 공유하여 우리 팀이 어떤 기능에 더 신경써야할지 덜 신경써야할지에 대해 이야기하는 시간을 가졌다.
이렇게 stand-up meeting 을 하고나면 내가 무작정 하루동안 코딩만 하는 것이 아니라,
팀이 어디까지 왔는지 진행상황을 알 수 있고,
오늘 어느 부분에 좀 더 신경을 써야하는지에 대해 알 수 있어서 좋았다.
우리팀은 프로젝트의 flow 관리를 위해 Trello tool 을 활용하였다.
크게 네 카테고리로 나눠서
추가적으로 우리는 프론트엔드와 백엔드 간 key 값을 공유하기 위해 다음과 같이 RESTFUL API 주소명으로 티켓을 만들고 그 안에 key 값을 공유했다.
개인적으로 key 값을 trello 에 공유한 건 매우 잘한 부분이라고 생각한다.
슬랙을 통해 바로바로 필요한 정보가 있으면 공유를 하였고,
github bot 을 연동시켜서 팀 전체가 어떤 PR 을 올렸는지, 어떤 기능이 master 에 merge 가 됐는지 바로바로 확인할 수 있게끔했다.
슬랙에 깃헙 봇을 연동시키는 일은 생각보다 간단해서 금방했다.
다음으로는 프로젝트 동안 프로젝트의 버전 관리를 위해 사용한 github 에 대해서 이야기를 해 보려고 한다.
우리는 먼저 프론트엔드, 백엔드를 나눠서 repository 를 판 뒤 기능별로 branch 를 만들어서 작업했다.
백엔드는 나와 다민님 두명이여서, 각자 맡은 기능별로 branch 를 파서 작업했다.
github 을 쓰면서 이전에 외주를 하거나, 혼자 사이드 프로젝트를 진행했을 때 많이 해보지 못한 것들을 많이 경험해본 것 같다.
왜냐면
하지만 이번에는 나 혼자가 아니라 백엔드 팀원 분과 함께 기능을 분담하여 진행했다.
따라서 같은 app 의 같은 view 에서 작업 할 일이 잦았기 때문에 master branch 에 merge 할 때 conflict 가 발생하는 경우가 잦았다.
이번에 어떻게 merge conflict 를 resolve 하는 방법에 대해서도 자세히 알게되는 계기가 되었다.
간단하게 정리하면, 다음과 같은 순서이다.
풀 리퀘스트(PR) 을 날릴 때, 어떤 식으로 리뷰요청을 남겨야 할 지에 대해 많이 고민해보는 시간이 되었다.
그 전에는 그냥 master 에 merge 해달라는 심정으로 올렸다면,
지금은 reviewer 의 입장에서 한번 더 생각해보고 PR 요청을 하도록 내 자신이 바뀐 것 같다.
프로젝트 동안 PR 요청을 하면서 왜 reviewer 의 입장에서 요청을 해야되는지 느낀점을 나열해 보려고 한다.
이 외에도 더 많은 이유가 있지만 대표적으로 두가지만 적어보았다.
그 전에 개발할 때는 PR 라벨에 대해서 별로 신경을 안 썼는데
지금와서 써보니, PR 라벨을 관리하는 것의 장점이 너무 많다는 사실을 깨달았다.
라벨에 status 별로 색과 이름이 다르며,
reviewer 가 달 수 있는 라벨과 (수정요청, Accepted, 리뷰진행중, conflict 해결요청)
reviewee 가 달 수 있는 라벨이 나뉘어져 있다.(도움요청, 리뷰요청, 추가기능구현중)
단적인 예로, 내가 PR 을 올렸는데 수정해야 할 기능이 급하게 생기거나 급하게 commit 을 해야 할 경우, reviewer 가 리뷰를 하기 전 추가기능구현중 으로 라벨을 바꾸면 된다.
1차 프로젝트를 진행하는 동안 라벨관리가 꽤 인상깊었다.
실제 있었던 예시를 들면,
장바구니에서 결제페이지로 상품을 이동시키는 로직이 있었다.
장바구니에 담긴 상품들을 선택적으로 결제페이지로 이동시켜야 했는데,
이 때 프론트엔드에서는 아직 전역상태관리라는 기술을 배우지 않았기 때문에
장바구니 창에서 선택된 상품을 결제 페이지 창에도 띄우는 기술을 구현하지 못한다는 것을 알고 있었다.
그래서 나는 프론트와 상의해서 백엔드 쪽에서 결제페이지로 넘어간 상품의 order status 를 pending payment 로 바꿔서 관리하겠다고 이야기했다. 물론 까다로웠지만 어쩔 수 없는 부분이였기에 그렇게 했다.
기능별로 브랜치를 만들어서 Github 에서 Version Control 했다.
miniconda 를 통해 python version 을 3.8 로 맞춰서 새로운 가상환경을 만들었다.
또한 mysqlclient 와 같이 필수적인 패키지들을 다운받은 뒤, 해당 가상환경에 설치된 모든 패키지 파일들을 requirements.txt 파일에 명령어 하나로 넣어주었다.
무거운 Django framework 를 사용하는 만큼, 최대한 경량화 하기 위해 쓸데없는 app 은 주석처리하고 추가해야 할 app 들은 추가해주었다.
또한 SECRET_KEY 나 DATABASE, HASHING_ALGORITHM 과 같이 노출되어서는 안되는 값들은 my_settings.py 에 넣은 후 .gitignore 에 추가해서 tracking 하지 못하도록 했다.
AqueryTool 을 이용해 모델링을 ERD 를 만들었다.
User, Product, Order 세 부분으로 나누어 모델링을 하였고, app 별로 색깔을 다르게 했다.
many-to-many, one-to-many, one-to-one 관계를 명확하게 설정하였다.
unique=True 및 null=True 인 경우를 고려했다.
모델링을 하며 정말 많이 고민했던 부분들이 있었다.
products 에 접근 -> 해당 product 의 sub_category 로 접근 -> 해당 sub_category 에 연결된 모든 products 를 전부 가져옴 -> product 마다 products_options 를 통해 options 테이블로 접근
sub_categories 테이블에서 products_options 테이블 역참조 (장고에서는 <sub_category instance>.productoption_set.all() )
이렇듯 특정 컬럼이 없어도 값을 가져올 수는 있지만, 나중에 실제로 데이터를 어떻게 가져올 지 먼저 생각해 본 다음 쿼리가 너무 길어질 것 같다 싶으면 쿼리최적화를 위해 테이블에 fk 를 더 추가하여 모델링을 하는게 훨씬 좋다는 걸 깨달았다.
결론은 모델링을 하면서도 데이터를 어떻게 효율적으로 가져올지에 대해서 고민을 하는 것이 필수라는 것이다.
이 때 order_status 는 유저의 input 과 관계없는 서비스 전부터 만들어져있어야 하는 데이터이다.
실제로 코드를 짤 때 이 로직때문에 많이 애를 먹었지만, 데이터를 관리하기엔 효율적인 구조라는 것을 깨달았다.
추가적으로 생각해봤던건, 두번의 각기 다른 주문으로 같은상품 A 를 두번 샀다면 어떻게 해야할까 ?
유저가 이 상품에 대해 리뷰를 쓸 때 자신이 어떤 상품에 대해 review 를 남기고 있는 것인지 알게해야하지 않을까? 라는 생각을 했다.
여기에 대해서 우리팀은 나중에 A 상품 리뷰 페이지에 자신이 주문했던 내역을 보여주고, 어떤 주문에 대해서 리뷰를 달 것인지 클릭하게끔 UX/UI 를 기획했다.
이렇듯 UX/UI 를 유저 입장에서 미리 생각하고 기획, 설계 하는 것이 매우 중요한 일임을 깨달았다.
실제로 코드를 작성한 것을 여기에 다 적기엔 너무 많기에, 기록하고 싶었던 코드들을 소개하려고 한다.
# 장바구니에서 상품을 몇개만 선택할 수 있음
@auth_check
def patch(self, request):
try:
selected_products = json.loads(request.body)['selected_products']
if not selected_products:
return JsonResponse({'message': 'PRODUCT_NOT_SELECTED'}, status=400)
with transaction.atomic():
before_purchase, _ = OrderStatus.objects.get_or_create(id=1, status='구매전')
pending_purchase, _ = OrderStatus.objects.get_or_create(id=2, status='결제중')
order_before_purchase, _ = Order.objects.get_or_create(user=request.user, order_status=before_purchase)
order_pending_purchase, _ = Order.objects.get_or_create(user=request.user, order_status=pending_purchase)
for selected_product in selected_products:
product_id = selected_product['product_id']
product_option_id = selected_product['product_option_id']
# 결제중인 상품도 장바구니에 노출되는데, 결제중인 상품을 선택해서 담을 가능성이 있기때문에
# get 으로 하면 에러가 나기 때문에 filter로 했음.
if product_option_id:
cart = Cart.objects.filter(
product_id = product_id,
product_option_id = product_option_id,
order = order_before_purchase
).first()
pending_cart = Cart.objects.filter(
product_id = product_id,
product_option_id = product_option_id,
order = order_pending_purchase
).first()
else:
cart = Cart.objects.filter(
product_id = product_id,
order = order_before_purchase
).first()
pending_cart = Cart.objects.filter(
product_id = product_id,
order = order_pending_purchase
).first()
if not cart:
continue
if pending_cart:
pending_cart.quantity += cart.quantity
pending_cart.save()
cart.delete()
else:
cart.order = order_pending_purchase
cart.save()
return JsonResponse({'message': 'SUCCESS'}, status=201)
except JSONDecodeError:
return JsonResponse({'message': 'JSON_DECODE_ERROR'}, status=400)
except KeyError:
return JsonResponse({'message': 'KEY_ERROR'}, status=400)
except Cart.DoesNotExist:
return JsonResponse({'message': 'CART_DOES_NOT_EXIST'}, status=404)
hot_products = cart_querysets.values('product_id').\
annotate(total_quantity_sold=Sum('quantity'))\
.order_by('-quantity')[:10]
'is_new'
키 값을 확인해보자. product_dict = {
'product_id' : product.id,
'product_name' : product.name,
'product_price' : float(product.price),
'product_thumbnail': product.thumbnail_image_url,
'discount_rate' : discount_rate * 100,
'discounted_price' : discounted_price,
'stock' : product.stock,
'is_in_wishlist' : is_in_wishlist,
'is_new' : 1 if product in Product.objects.filter\
(updated_at__gte=datetime.datetime.today()-datetime.timedelta(days=7))\
else 0,
'is_best' : 1 if product in get_hot_products_querysets() else 0,
'is_sale' : 1 if discount_rate > 0 else 0
}
Product.objects.filter(updated_at__gte=datetime.datetime.today() -datetime.timedelta(days=7))
이 외에도 기록하고 싶은 코드 및 문제를 해결하는 과정이 담긴 코드들은
프로젝트 기간동안 블로그에 TIL 로 정리해놓았다.
다만 한가지 아쉬운 것은, 지금까지는 trello 에 키값을 공유했지만 postman 으로 API 명세를 만들 수도 있다는 사실을 늦게 알아버리는 바람에 이 기능을 활용하지 못했다.
다음 프로젝트 부터는 postman 을 활용해 API 문서를 만들 계획이다.
다들 모난 데 없이 유쾌하고 좋은 팀원들이였음에 감사하다.
프로젝트 중 기술적으로 필요한게 있으면 돌려서 이야기하지 않고
직접적으로 이야기 하셨지만 전혀 기분나쁘게 들리지 않았고, 나 또한 그렇게 하려고 노력했다.
그러다보니 자연스럽게 팀 분위기도 좋고, 2주라는 짧은 시간 안에 나온 결과물도 아쉬운 부분이 분명 존재하긴 하지만 우리가 하려고 했던 데 까지는 모두 구현했다는 점에서 만족스러웠다.
팀 끼리 프론트/백 나뉘어서 제대로 각 잡고 진행했던 프로젝트는 이번 프로젝트가 처음이였지만 정말 많은 것을 배울 수 있었다.
기술적인 부분은 당연하고,
프로젝트의 매끄러운 진행을 위해 커뮤니케이션을 어떻게 이끌어가야하는지를 배웠다.
프로젝트가 남의 프로젝트가 아니라 우리의 프로젝트, 내 프로젝트 였기 때문에 자연스레 오너십이 생겼고,
그 오너십이 있으면 정말 뭐든 할 수 있겠구나 라는 것을 느꼈던 시간이였다.
마지막으로 끝까지 모르는 것을 열정적으로 질문해주신 백엔드 팀원 다민님 덕분에
어떻게 좀 더 잘, 그리고 쉽게 대답하는 방법에 대해 스스로 고민해 볼 수 있게 되어 좋은 시간이였다.
내가 완전 개발을 막 시작했을 때 주변 개발자 친구들에게 궁금한 것을 물어봤을 때가 떠올랐다.
내 친구들은 항상 나에게 이해하기 쉽게 알려주되 절대 정답을 바로 알려주진 않았는데,
나도 "질문하면 이해하기 쉽게 대답해주는 사람", "또 물어보고 싶은 사람", "정답을 알려주지 않고 힌트를 주는 사람" 이 되기 위해 노력할 것이다.
그리고,
IT 현업에 계시다 와서 db 를 설계하고 query 를 어떻게 날려야 최적화 할 수 있는지에 대한 인사이트를 주신 송희님,
pm 으로써 어떤식으로 팀을 이끌어야 하는지에 대한 인사이트를 주신 미현님,
기술적으로 어려운 부분임에도 백엔드에서 요청드린대로 끝까지 포기않고 팀을 위해 프론트 기능 구현해주신 래영님께 감사드린다.
2차 플젝때는 더 많은 배움 얻으시길!!
너무너무 수고많으셨습니다 택향님!! 👏🏻👏🏻