Project5 - About User & Order Project

Heechul Yoon·2020년 5월 9일
1

LOG

목록 보기
51/62

프로젝트 소개(Project Description)

주제(Topic)

  • 유저 관리와 상품 주문 기능 구현

구성원(Member)

  • 1인 프로젝트

기간(Developing Period)

  • 5일(20200504 ~ 20200510)

적용 기술(Skill Applied)

Python 3.8.0 : language
Pycharm venv : virtual environment
Flask 1.1.2 : web framework
Git : cooperation and version management tool
Redis : Caching database
MySQL : Database
Pymysql : Database connection
Bcrypt : password hashing
JWT : token generating

깃허브(Github)

프로젝트 초기 설계

  • model, service, view 레이어 간 의존성 설정
  • 데이터베이스 모델링(aquery tools 사용)
  • 초기 데이이터베이스 스크립트 생성 : 테이블, 외래키 관계, 기초 데이터 생성
  • pycharm venv를 사용한 가상환경 설정(python3.8)
  • .gitignore 파일 생성
  • model : 데이터베이스와 통신
  • service : 비지니스 로직
  • view : url 라우팅(블루프린트 사용) 및 유효성 검사
  • utils.py : 토큰 확인 데코레이터
  • config.py : 데이터베이스와 레디스 정보
  • connection.py :데이터베이스와 레디스 커넥션 관리
  • app.py : 플라스크 앱 생성 및 블루프린트 중앙 라우팅, jsonEncoder를 사용해서 datetime 리턴 형식 설정
  • manage.py : 프로젝트 실행
  • requirements.txt : 환경 공유
  • Dockerfile 을 통해서 이미지 생성

모델링(ERD)

  • 유저 정보의 이력을 관리하기 위해서 선분이력 사용 : 유저 로그인 정보를 관리하는 테이블(user_accounts)과 유저 상세정보를 관리하는 테이블(user_infos)을 분리시킴. 유저정보가 업데이터 될 때 기존의 유저정보 테이블의 선분을 끊어주고 새로생성되는 정보의 시작날짜를 이전 이력의 끝 날짜로 변경함

  • 하나의 유저는 auth_types 테이블에 있는 권한을 부여받는다. master권한을 가진 유저는 유저정보와 유저 주문내역을 열람할 수 있다.

  • 유저가 상품을 구매하고자 하는 의사를 담은 주문 테이블을 둔다. 즉, 하나의 상품을 유저가 장바구니에 추가하면 그 상품은 주문으로서 장바구니에 들어가는 개념. 주문테이블의 하나의 value는 상품테이블의 상품과 1대1 매칭이 가능하다.

  • 장바구니 테이블은 유저 계정 번호(user_account_id)를 외래키로 가진다. 그래서 하나의 유저가 하나의 결제되지 않은 장바구니를 가진다. 그리고 그 장바구니를 결제하면 장바구니 테이블의 is_checked_out 컬럼의 값이 1(True)로 바뀐다. 결제(check out)된 장바구니는 유자가 다음부터 볼 수 없다.

  • 장바구니가 결제(check out)되는 동시에 주문 명세서 테이블(receipts)에 주문 명세서가 만들어진다. 주문명세서는 결제(check out)된 장바구니를 외래키로 가진다.

기능(API document)

회원가입
  • json body 유효성검사
  • 마스터 권한부여는 데이터베이스에서 raw query로 업데이트
  • 이메일 및 닉네임 중복체크
  • 패스워드 bcrypt 암호화
  • 유저정보는 선분이력을 사용하기 때문에 어카운트 로그인정보 등록 후 유저정보 등록
  • 회원가입 성공 시 redis 접근 키 리턴
  • 토큰을 redis 저장공간에 key-vale 형태로 저장성
로그인
  • 이메일과 패스워드를 받아서 로그인
  • 데이터베이스에 있는 유저 정보와 input 정보 비교(bcrypt.checkpw 사용)
  • 로그인 성공 시 redis 접근 키 리턴
  • 토큰을 redis 저장공간에 key-vale 형태로 저장성
토큰 확인 데코레이터
  • header에서 가져온 토큰을 decode
  • 해당유저의 id를 가지고 데이터베이스에 권한정보, 존재여부, 삭제여부 확인
  • 토큰을 decode해서 나온 유저가 존재하면 flask g객체에 유저번호와 권한타입아이디를 저장
로그아웃
  • key를 json body로 받음
  • 받은 key를 redis에서 삭제
유저 리스트 표출
  • 마스터권한을 가진 유저가 유저 리스트와 유저의 최근 주문 내역 열람 가능(권한타입 유효성 검사)
  • 검색 키워드와 pagination을 위한 offset과 limit을 쿼리파라미터로 받음
  • 선분이력상 close_time이 2037-12-31(가장 최근정보에 해당됨)인 유저정보를 가져옴
  • 검색키워드가 들어오면 like를 사용해서 문자열이 하나라도 포함되면 값을 가져오도록 검색 구현
  • 전체중 몇명의 유저가 검색되었는지 확인하기 위해서 키워드로 필터된 유저 수와 전체유저 수를 같이 리턴
유저 상세 정보 표출
  • 마스터 권한으로 확인하고자 하는 유저의 id를 쿼리파라미터에 넣어서 요청
  • 권한 타입 유효성 검사
  • 선분이력상 close_time이 2037-12-31인 유저정보(가장 최근 이력에 해당되기 때문)를 가져옴
  • 유저의 가장 최근 주문내역 표출
상품 리스트 표출
  • 등록된 상품 리스트 표출
  • pagination : offset과 limit을 쿼리파라미터로 받아서 상품 표출, limit이 5000이상이면 애러리턴
상품 상세정보 표출
  • 하나의 상품의 상세정보 표출
장바구니에 상품 추가
  • 유저정보를 확인하고 해당유저의 장바구니가 없는 상태면 만들고 상품을 추가함(있으면 있는 장바구니에 추가)
  • 추가하고자 하는 상품 번호를 path parameter로 받음
  • check out(결제)되지 않은 장바구니에 상품을 추가함
내 장바구니 표출
  • 특정 유저의 장바구니를 표출해줌
  • 추가된 상품을 pagination 해서 보여줌
  • 토큰을 확인해서 누구의 장바구니인지 확인
  • 상품을 count하기 위해서 상품 id를 기준으로 group by해줌
  • 하나의 그룹에서 가져와야 하는 값이 하나여야 하기 때문에 서브쿼리에 limit을 1로 줘서 값을 하나만 가져옴
  • 하나의 그룹에 있는 상품의 갯수를 count하는 서브쿼리 사용
장바구니에서 상품 삭제
  • 장바구니에서 하나의 상품의 갯수에 상관없이 상품을 삭제함
  • order 테이블에서 cart_id를 가져오기 위해 서브쿼리 사용
장바구니 갯수 수정
  • 하나의 상품의 갯수를 줄일 때 사용(올릴때는 장바구니에 상품추가 기능 사용)
  • 같은 테이블의 경우 select delete 서브쿼리가 안되기 때문에 갯수를 줄이고자 하는 상품의 order 번호를 먼저 가져옴
  • 가장 최근 order 번호부터 순서대로 삭제
주문서 생성(장바구니 상품 주문 기능)
  • my-cart를 통해서 얻은 cart_id를 POST해서 해당 장바구니의 상태를 check out으로 바꿈
  • check out으로 바뀐 장바구니의 주문 명세서를 생성함(주문번호는 uuid로 생성)
  • 생성된 주문 명세서 번호, check out 한 장바구니 번호를 리턴
주문서 표출
  • 주문서를 생성하고 받은 명세서 번호와 장바구니 번호를 쿼리파라미터로 보냄
  • 유효성 검사 : 해당 주문명세서 번호, 장바구니 번호와 그것을 생성한 유저번호에 해당하는 주문명세서 번호가 없으면 404 리턴
  • is_checked_out 컬럼이 1인 장바구니의 상품목록과 해당 상품의 갯수를 가져옴

기억에 남는 기능

유저 계정테이블을 기준으로 가져오는 최근 주문내역

유저의 정보와 그 유저의 최근 하나의 주문을 같이 SELECT 하는 SQL 쿼리를 작성했다.

    SELECT 
        ua.id as user_account_id, 
        ua.email,
        ua.created_at as user_signed_up_date,
[1]     (select gd.gender from user_infos as ui left join genders as gd on gd.id = ui.gender_id where ui.user_account_id = ua.id and close_time = '2037-12-31 23:59:59') as gender,
[2]     (select name from user_infos as ui where ui.user_account_id = ua.id and close_time = '2037-12-31 23:59:59') as user_name,
        (select nick_name from user_infos as ui where ui.user_account_id = ua.id and close_time = '2037-12-31 23:59:59') as user_nick_name,
[3]     (select id from receipts as rt where rt.user_account_id = ua.id order by created_at DESC limit 1) as recent_receipt_id,
        (select order_number from receipts as rt where rt.user_account_id = ua.id order by created_at DESC limit 1) as recent_order_number,
        (select created_at from receipts as rt where rt.user_account_id = ua.id order by created_at DESC limit 1) as recent_order_date
    FROM user_accounts as ua
    LEFT JOIN user_infos as ui on ua.id = ui.user_account_id
    LEFT JOIN carts as ct on ua.id = ct.user_account_id 
    LEFT JOIN receipts as rt on ua.id = rt.user_account_id
    WHERE ui.is_deleted = 0
    AND ua.is_deleted = 0
    AND ua.auth_type_id = 2
[4] AND GROUP BY ua.id

유저 계정 테이블(user_accounts)을 기준으로 해서 선분이력관리 테이블인 user_infos, 장바구니(carts), 주문서(receipts) 테이블을 전부 join해서 한번에 가져오는 쿼리이다. 핵심은 [4]에서의 group by인데, 이것을 해주는 이유는 주문서(receipts)테이블에서 유저의 가장 최근 주문내역을 가져와야 하기 때문이다.

  • GROUP BY [4]
    만약 group by를 해주지 않는다면?
    한명의 유저는 여러개의 주문내역을 가지고 있을 것이다. 유저 계정 테이블(user_accounts)을 기준으로 join을 했지만 여러개의 주문내역 기록 때문에 유저 주문내역을 제외한 다른 row가 중복되어 표시된다.
    유저 계정 테이블(user_accounts)을 기준으로 group by를 해주게 되면 하나의 유저계정이 group이 되어 여러개의 데이터가 존재하는 주문내역테이블에 하나의 스칼라 값을 요구하게 된다.
    그래서 [3]에서와 같이 서브쿼리를 통해서 주문 생성일(created_at)을 기준으로 정렬하고 가장 최근의 값을 limit으로 1개 가져온다. 이렇게 스칼라 서브쿼리가 완성되면 group by의 조건에 부합한다.
  • 유저 계정 테이블(user_accounts)기준으로 두 테이블 건너 성별(gender)값 가져오기 [1]
    서브쿼리 안에서도 join이 가능하다는것을 알게되었다. 사실은 알고있었지만 실재로는 쿼리가 길고 복잡해져서 사용히자 않았지만 이번에 사용할 일이 생겼다. 우선 성별(gender)테이블은 유저 정보(user_infos)테이블이 외래키로 가지는 값이다.
    유저 정보(user_infos)테이블을 을 성별 테이블과 join해서 값을 가져온다. 여기서 핵심은 선분이력 관리용 테이블인 유저 정보 테이블(user_infos)이다. 가장 마지막 이력을 가져오기 위해서 WHERE조건으로 선분이력 종료일시가 '2037-12-31'인 row를 가져온다.(2037~ 이 데이터베이스에서 표현할 수 있는 가장 끝 시간이라 한다)

상품 주문

장바구니에 넣은 상품을 최종적으로 주문하는 기능을 구현했다. 장바구니에는 '주문 의사'가 담긴 상품이 추가된다. 여기서 '주문 의사'는 orders테이블의 value이다. 즉 orders테이블에는 장바구니에 담기는 상품들이 들어간다. 그리고 is_checked_out 컬럼을 두어 이 '주문 의사'가 결제가 되었는지 아니면 여전히 장바구니에 결제되자 않은 상태로 담겨있는지를 표현한다.

장바구니 자체도 결제가 된 장바구니인지, 결제가 되지않은 상태로 남아있는 장바구니로 구분하기 위해서 is_checked_out 컬럼으로 표현한다.

그렇다면 장바구니에 있는 상품의 주문은 어떻게 이루어지는가?
'주문 의사'가 담긴 상품과 장바구니 자체의 is_checked_out 컬럼을 TRUE로 만들어주는 것이다. is_checked_out이 TRUE가 되는 순간 내 장바구니 상품 표출에서 결제된 장바구니가 걸러지게 되고, 최근주문내역에서 결제된 장바구니에 있는 결제된 상품들을 확인 할 수 있다.

스스로에 대한 피드백

성장

  • docker라는 환경 공유 플랫폼을 처음 사용했다. 내 컴퓨터에서 작업 한 프로젝트의 환경을 이미지로 만들어서 docker hub에 공유하고, 내 프로젝트를 공유된 이미지를 바탕으로 docker 컨테이너 안에서 실행 하는 개념을 이해했고 성공적으로 적용할 수 있었다.
  • SQL 문법에서 GROUP BY를 처음으로 사용했다. 이전 브랜디 인턴을 할 때 각각의 유저가 등록한 상품을 갯수를 가져올 때 사용하려 했지만 GROUP BY를 사용하는 순간 다른 값들을 불필요하게 min 또는 max를 통해서 스칼라 값으로 정해주어야 해서 GROUP BY보다는 스칼라 서브쿼리를 사용했었다. 하지만 이번에는 유저 계정 정보를 기준으로 그 유저의 최근 주문내역을 가져와야 하는 상황이었고 하나의 유저의 주문내역을 그룹화 해서 그 안에서 최근 주문내역을 가져오는 것이 합리적이라고 생각했다. GROUP BY를 사용해 유저 계정을 하나의 그룹으로 하고 주문 생성일을 기준으로 정렬 후 limit을 통해서 1하의 내역만 가져오도록 했다. GROUP BY의 특성상 값을 가져오는데 제약이 많이 걸리지만 서브쿼리를 통해서 값을 가져오는 데 성공했고, 스칼라 서브쿼리를 어느정도 자유자제로 다룰 수 있게 되었다.

아쉬운점

  • 이때까지 jwt를 통해서 로그인 기능을 구현해왔었다. 하지만 redis를 session storage로 사용하면서 로그아웃을 구현했을 때 jwt의 한계를 보게 되었다. 현재 로그인에 성공하면 jwt토큰을 클라이언트에게 바로 주는것이 아닌 redis에 key-value로 저장한다. 그래서 로그아웃 api를 호출하면 redis 에서 해당 토큰 key를 삭제하고 클라이언트가 다음 로그인권한이 필요한 요청을 할 때 해당 key를 redis storage에서 찾을 수 없도록 하는 것이 핵심이다. 하지만 처음에 로그인 할 때 주어진 토큰은 백엔드 서버에서 처리 할 수가 없기 때문에 그 토큰을 가지고 백엔드 api에 접근 할 수 있다.

개선방법

  • jwt 토큰은 발급 한 순간 서버에서 손을 댈 수 없는 토큰이다. 따라서 발급된 토큰을 관리 할 수 있는 다른 로그인 토큰 발행 솔루션을 찾아야 한다. OAuth에 관해서 찾아보고 적용 해 보도록 하자
profile
Quit talking, Begin doing

0개의 댓글