처음하는 프로젝트였기 때문에 처음시작 했을 때는 백엔드 3명의 역할이 모호했다. 프로젝트 조 발표가 있고나서 바로 모델링을 시작했다. 팀원과 모델링을 어느정도 완성하고 나서 각자의 역할이 정해지게 되었다.
필자는 상품과 관련된 데이터를 담당하게 되었고 task는 다음과 같다.
홈메뉴(메인상품, 시즌상품, 카테고리별 상품, 번들상품)
전체상품 페이지
카테고리별 상품 페이지
상품 상세 페이지
레시피 페이지
레시피 상세 페이지
Lookbook 페이지(추천 레시피)
데이터베이스 관리(MySQL)
크롤링
AWS EC2에 프로젝트 배포
AWS RDS 구축
처음에 대부분의 모델링을 했었기 때문에 테이블과의 관계에 있어서 제일 많이 엮여있는 상품을 맡게 되었다. 프로젝트안에 있는 대부분의 데이터에 대한 GET기능을 구현했어야 해서 자연스럽게 데이터 크롤링과 데이터베이스 관리를 맡게 되었다.
상품 하나에 대한 정보가 상품전체페이지와 상품상세페이지에 나뉘어 담겨져 있어서 하나의 상품에 대한 정보를 가져오려면 상품전체 페이지에서 데이터를 가져오고, 그 상품의 링크를 타고 들어가서 하나의 상품에 해당하는 데이터를 가져와야 했다.
모든 상품에 대한 상세페이지로 들어가는 url을 리스트로 만들어서 하나의 상품에 대한 크롤링이 끝나면 다음 url로 넘어가게 하는 크롤러를 만들어 데이터를 수집했다.
크게 상품, 유저, 주문으로 나뉘어 테이블간의 관계를 잡아주었다. 모델링에서 중요한 관계에 있는 테이블들을 알아보자.
우선 상품테이블안에서 유사상품 필드의 자기참조이다. 하나의 상품은 여러개의 유사상품을 가지게 되기 때문에 처음에는 유사상품이라는 상품테이블과 똑같은 테이블을 만들고 foreignkey로 상품테이블을 가지게 해서 두 테이블을 ManyToMany relation으로 만들어 주었다. 하지만 self라는 자기테이블 참조 기능을 알게 되면서 해당 필드에 자기참조를 넣어 중간테이블을 만들어주어 해결했다.
두번째로는 번들상품이다. 하나의 번들 프로모션 테이블은 여러개의 상품을 가질 수 있다. 하지만 하나의 상품이 여러개의 번들에 포함되는 것이 이해하는것이 어려웠지만 프로모션은 일주일단위로 바뀌는 기간제의 성격을 가지기 때문에 하나의 상품이 중복해서 추천될 수 있다는 점에서 ManyToMany관계가 성립하는 것이 이해가 되었다.
세번째로는 장바구니, 주문, 유저의 세가지 테이블의 관계였다. 장바구니와 주문의 생성과정을 알아보자.
- 상품페이지 또는 상품 상세페이지에서 'add to cart'를 누른다.
- 장바구니 테이블과 주문테이블이 동시에 생성된다. 장바구니 테이블에는 상품이 즉시 담기고 주문테이블은 생성만되고 어떤유저가 생성을 했는지만 표시된다.(주문테이블은 foreignkey로 유저테이블을 가진다.)
- 유저가 장바구니를 checkout(결제창으로 이동)하면 주문테이블에 방금 체크아웃한 장바구니 정보가 update된다.
- 유저가 결제창에서 정보(배송지, 결제주소, 카드정보 등)를 입력하고 결제를 완료하면 주문테이블에 유저가 입력한 정보가 업데이트 된다.(2에서 주문테이블이 생성되었을 때는 해당필드에 null이 들어가있음.)
상품과 주문의 관계를 보면, 상품은 여러개의 주문에 들어갈 수 있고, 주문 또한 여러개의 상품을 가질 수 있다. 즉 장바구니는 상품과 주문의 ManyToMany관계에서 중간테이블 역할을 수행한다.
<결제정보 = 배송지정보>
1. 유저가 결제정보 = 배송지정보를 체크하면 billing_address테이블의 shipping_address_id필드에 id값이 입력된다.(shipping_address테이블을 foreignkey로 가지기 때문)
2. 주문 테이블은 billing_address테이블의 id를 foreignkey로 가지기 때문에 즉시 주문테이블의 billing address 필드에 값이 생선된다.
<결제정보 != 배송지정보>
1. 유저가 입력한 결제지 주소 데이터가 billing_address 테이블에 입력된다. 그리고 is_shipping_address 테이블에 False값이 들어간다. shipping_address_id 필드에는 null이 들어간다.
2. 주문테이블에 billing_address_id필드에 값이 입력된다.
이렇게 주문테이블이 생성되면 결제 영수증의 개념으로 주문테이블의 value와 주문테이블이 참조하는 테이블이 데이터로 뿌려질 것이다.
홈페이지 대부분의 GET기능을 구현했다.
def get(self, request):
[1] sort_by = request.GET.get('sort_by', 'id')
[2] offset = int(request.GET.get('offset', 0))
limit = int(request.GET.get('limit', 12))
product_info = Product.objects.select_related('harvest_year', 'measure').order_by(sort_by).values(
'name',
'id',
'price',
'small_image',
'harvest_year__year',
'measure_id__measure',
'is_on_sale',
'is_in_stock',
)[offset:offset+limit]
페이지네이션 : 페이지 기능이 들어가는 전체 상품 페이지, 전체 레시피 페이지, 카테고리별 상품 페이지에 사용된다.
쿼리파라미터로 offset값(시작하는 값) limit값(보여주는 개수)을 받고, 시작하는 값부터 시작하는값 + 보여주는 갯수를 더한값 까지 보여주도록 한다. 보여주는값인 limit의 default를 12로 두고 값이 들어오지않으면 시작값부터 12개를 보여주도록 한다. 처음부터 12로 지정하지 않은 이유는 나중에 보여주는 갯수가 변화할 수 있는 경우를 반영하기 위해서 이다. [2]에서와 같이 쿼리파라미터가 들어오지 않은경우 default로 시작을 0, 끝을 0+12로 주어 처음부터 12개의 유닛을 뿌리도록 만든다.
정렬기능 : 전체상품페이지, 카테고리별 상품 페이지에 구현되며 이름순, 가격순으로 정렬이 가능하다. 쿼리파라미터로 정렬기준을 key로 받으면 그것을 쿼리매서드인 order_by()안에 바로 넣어서 정렬된 값을 리턴한다. [1]에서와 같이 값이 들어오지 않으면 default로 'id'를 기준으로 변수에 담아 id순의 정렬된 값을 리턴한다.
쿼리파라미터로 정렬기준('sort_by'), 시작값('offset')을 동시에 받아서 2페이지에서의 가격순 정렬도 가능하다.
data_caching = Product.objects.select_related('measure', 'harvest_year').prefetch_related('similar_product').get(id=product_id)
product_info = {
'id' : data_caching.id,
'name' : data_caching.name,
'harvest_year_id__year' : data_caching.harvest_year.year,
'measure_id__measure' : data_caching.measure.measure,
'is_in_stock' : data_caching.is_in_stock,
'description' : data_caching.description,
'price' : data_caching.price,
'small_image' : data_caching.small_image,
'big_image1' : data_caching.big_image1,
'big_image2' : data_caching.big_image2,
'big_image3' : data_caching.big_image3,
'energy' : data_caching.energy,
'carbonydrate' : data_caching.carbonydrate,
'protein' : data_caching.protein,
'fat' : data_caching.fat,
'mineral' : data_caching.mineral,
'vitamin' : data_caching.vitamin,
'similar_product' : list(data_caching.similar_product.values('name', 'harvest_year_id__year', 'is_in_stock', 'measure_id__measure'))
}
manytomany 관계에 있는 유사상품과 상품은 하나의 상품이 여러개의 유사상품을 가지기 때문에 하나의 객체를 가져와 values()로 여러개의 유사상품을 리스트에 담고 리스트화 해준다. 여기서 values()를 통해서 모든 값을 다 불러와 similar_product만 그 딕셔너리안에 추가해주면 되는것 아니냐는 의문이 들 수 있지만, values()로 key와 value를 뿌려주고 되면 key값을 정할 정할 수 없다. 그래서 lookup을 했는 경우 'harvest_year_id__year'과 같이 key값이 지저분하게 남아 프론트엔드에게 가독성 좋은 키값을 넘겨줄 수 없다.(필자의 경우 이미 values()로 지저분한 키값을 넘겨주고 프론트엔드에서 키값설정이 끝난 후 이 사실을 깨닳아 get으로 객체를 뽑아 리턴함에 키값이 지저분하게 되었다.)
1차 프로젝트 때 배운점은 커밋의 무게이다. 어렵고 복잡한 작업을 하고 그 작업이 팀원의 작업에 영향을 미치는 만큼 내 코드와 커밋이 팀과 프로젝트에 끼치는 영향을 항상 고려하여 책임감을 가져야 겠다고 생각했다. 두번째로는 커뮤니케이션 능력이 생각이상으로 중요하다는 것이다. 백앤드 안에서도, 프론트앤드와 백인드끼리도 결국 겹치는부분이 있고 그부분에 대한 원활한 소통이 결국 좋은 결과물을 가져온다. 그래서 내가하는 작업을 상대방이 알아듣기 쉽게 말해야하고, 상대방이 원하는 것이 무엇인지 핵심을 파악하는것이 중요하다고 느꼈다. 마지막으로는 누구도 내문제를 해결해주지 않는다는 자세로 임해야 한다는 것이다. 물론 필요한 것을 질문 할 수는 있겠지만 그 질문은 내 깊은 고민에서 나와야 하고, 내 문제해결능력에 발전이 있는 질문이어야 한다. 어떤 문제도 당장 정답을 물어보기 보다는 혼자 해답을 찾는 시간을 가지고 그 시간에 익숙해지는 것이 여러 상황에 대한 대처능력을 키우는 것이라고 느꼈다.