[브랜디인턴쉽] 회고록 volume 2

김기용·2021년 1월 18일
0

프로젝트 작업사항으로는 배송지 조회, 생성, 수정, 삭제 그리고 상품 검색 및 상세조회 부분을 맡아 작업하였다.


⚡️ 배송지 모델


⚡️배송지 정보 조회 페이지

PROBLEM

주문 시작과 동시에 해당 유저에 대한 기본 배송지 정보를 반환하고
회원, 비회원 모두 주문 시작이 가능하니 유연하게 배송지 정보 값을 받아 처리해 주어야했다.

SOLUTION

#PRESENTATION LAYER

data = dict()
if 'account_id' in g:
    data['account_id'] = g.account_id
if 'permission_type_id' in g:
    data['permission_type_id'] = g.permission_type_id
connection = get_connection(self.database)
destination_detail = self.service.destination_service

비회원 유저도 주문을 시작할수 있기때문에 토큰값이 항상 들어온다고 가정한다면 비회원일 경우 keyError 가 발생하기때문에 g 객체를 유연하게 받아오는 방법을 선택하였다.

# BUSINESS LAYER
if permission_type_id not in data and 'account_id' not in data:
    return '로그인한 회원만 배송정보를 조회할수 있습니다'
    
if not data['permission_type_id'] == 3:
    raise InvalidUser('허가되지 않은 유저입니다')

한 계층에서 일관된 처리를 해주기 위해 service에서 예외를 raise 해주기로 결정하였다. 이렇게 처리된 데이터는 business layer 에서 회원이 아닐경우 데이터를 조회할수 없음으로 예외처리를 해준다. 만약 토큰값이 들어온 경우 권한체크를하여 예외처리도 같이 해주었다.


⚡️ 배송지 생성

PROBLEM

배송지를 생성할때 고려해볼 사항이 몇가지 있다. 첫번째로, 오직 회원만이 배송지를 생성할 수 있고, 두번째로 회원당 생성할수 있는 배송지 정보는 5개로 제한해야하며, 5개중 무조건 한가지는 기본배송지 값으로 표시되어 있어야 했다.

SOLUTION

# Business Layer
if not data['permission_type_id'] == 3:
    raise InvalidUser('오직 회원만이 배송지를 생성할 수 있습니다.')

모든 비지니스 로직은 service layer에서 구성하였다. 오직 회원만이 배송지 생성을 할수 있음으로 회원인지 판단해주는 로직을 구현하였다.

# Business Layer
data_length = self.destination_dao.destination_check_length(connection, data['user_id'])
if data_length >= 5:
    raise DataLimitExceeded('최대 입력할 수 있는 배송지 개수를 초과했습니다.')

배송지의 개수는 유저당 오직 1개만 가질 수 있기때문에 데이터베이스의 존재하는 배송지의 개수를 확인하는 로직을 구현하였다.

# Business Layer
default_location_flag = self.destination_dao.check_default_location_by_user(connection, data['user_id'])
if not default_location_flag:
    data['default_location'] = 1
else:
    data['default_location'] = 0

만약 회원이고, 존재하는 배송지도 5개 미만이라면 데이터에 존재하는 기본배송지 여부에 따라 유연하게 기본배송지 값을 선언해준다.

check_default_location_by_user 메서드는 기본 배송지 유무를 확인해 truthy falsy 값으로 반환해 주는 역할을 한다.

# Business Layer
return self.destination_dao.create_destination_dao(connection, data)

모든 조건이 만족되었다면 배송지 생성을 한다.


⚡️ 배송지 수정

PROBLEM

배송지 수정 API 또한 생성과 마찬가지고 데이터의 개수, 기본배송지 여부에 따라서 로직을 짜주어야한다. 주요사항으로는 기본배송지 값이 수정될 경우를 고려해야했는데 이는 크게 두가지 가능성으로 볼 수 있다.

  1. 기존의 배송지 정보를 기본배송지로 설정하는 값인지
  2. 기존의 기본배송지 정보를 해제하려는 값인지

SOLUTION

생성때와 마찬가지로 회원만이 배송지 정보를 수정할 수 있기때문에 로직을 만들어준다. 모든 로직은 Layered Pattern에 의해서 Business Layer 에 구현해준다.

# Business Layer
if not data['permission_type_id'] == 3:
    raise InvalidUser('오직 회원만이 배송지를 생성할 수 있습니다.')
    

배송지 수정의 주요사항을 분기처리해주는 로직을 구현했다. 기본배송지(default_location) 값을 설정해 주려는건지 아니면 해제하는 값인지를 판별해야한다.


⚡️ default_location = 0 이 입력된경우

먼저 default_location = 0 값이 들어온 경우를 처리해준다. check_default_location 메서드를 사용해 truthy falsy 값을 flag 변수에 할당 받는다.

{
    "destination_id": "55",
    "recipient": "뮤즈씨",
	.
  	.
  	.
    "default_location":"0"
}
if data['default_location'] == "0":
    flag = self.destination_dao.check_default_location_flag(connection, data)

🕹 flag 값이 Truthy 일 경우

default_location = 1 을 default_location = 0 으로 수정하는 상황

기본 배송지가 해제되는 상황임으로 데이터베이스에 존재하는 값중 최초로 입력된 값이 기본배송지로 자동으로 변경해주고 수정을 진행하여야합니다.

# 최초로 입력된 배송지 정보를 찾아 기본배송지로 바꾸어준다.
self.destination_dao.update_default_location_true(connection, data)
# 배송지 정보 수정
return self.destination_dao.update_destination_info_dao(connection, data)

🕹 flag 값이 Falsy 일 경우

default_location = 0 을 default_location = 0 으로 수정하는 상황

이 경우는 기본배송지값이 영향을 받지 않기때문에 바로 수정을 진행하면 된다.

# 배송지 정보 수정
return self.destination_dao.update_destination_info_dao(connection, data)

⚡️ default_location = 1 이 입력된경우

{
    "destination_id": "55",
    "recipient": "뮤즈씨",
	.
  	.
  	.
    "default_location":"1"
}

기본배송지를 설정하는 값이 들어오는 경우

# 존재하는 모든 데이터의 기본배송지 정보를 0으로 바꿔준다.
self.destination_dao.update_default_location_false(connection, data)

# 배송지 수정 진행
return self.destination_dao.update_destination_info_dao(connection, data)

배송지 데이터중 기본배송지값을 가진 데이터는 오직 1개만 존재할 수 있기때문에 기본배송지를 설정하는 값이 들어온다면 기존에 존재하는 모든 데이터의 기본 배송지를 0으로 만들어주야한다.


⚡️ 배송지 삭제

PROBLEM

기본배송지는 1개가 존재해야하기 때문에 기본배송지가 지워지더라도 남아있는 배송지 데이터중 최초로 입력된 배송지 정보가 기본배송지로 설정되어야한다.

SOLUTION

  1. 삭제 권한을 가진 유저인지 판단하는 로직 구현
if not data['permission_type_id'] == 3:
    raise InvalidUser('오직 회원만이 배송지를 삭제할 수 있습니다.')
    
  1. 해당 배송지 정보를 먼저 삭제한다. 먼저 삭제하는 이유는 해당 배송지를 제외한 나머지 배송지정보값중에 기본 배송지가 존재하는지 찾기 위함이다.
# 삭제 진행
self.destination_dao.delete_destination_dao(connection, data)
  1. 기본배송지 데이터가 삭제됬을 수도 있으므로 기본배송지가 존재하는지 Truthy, Falsy 값으로 리턴받는다.
default_location_flag = self.destination_dao.check_default_location(connection, data)
  1. default_location 값에 따라 기본배송지를 설정해준다.
if not default_location_flag:
    self.destination_dao.update_default_location_true(connecton, data)

⚡️ 상품 상세 페이지

PROBLEM

  1. 회원 비회원 모두 상품 상세 페이지를 조회할수 있다. 하지만 데코레이터로 회원, 비회원에 따라 달라지는 북마크(하트) 상태를 표시해주어야 한다.

  2. 상품과 관련된 다수의 이미지와, 색상, 그리고 사이즈 데이터를 반환해 주어야했지만 쿼리문에서 Left join을 사용할경우 상품명의 중복이 발생였고, Inner join을 사용할 경우 오직 한개의 이미지, 색상 그리고 사이즈가 반환되는 결과가 있었다.

SOLUTION

1. 회원, 비회원에 따라 달라지는 북마크(하트) 상태표시

회원, 비회원 모두 상세 페이지를 조회할수 있게 만들어주기 위해 쿼리를 3개로 분단한뒤 조합하는 방법을 선택했다.

비회원일경우 query + query_end

회원일 경우 query + query_with_account + query_end

query = """
    SELECT
        product.id AS product_id
        .
        .
        .
        bookmark.bookmark_count
"""

query_end= """
    FROM
        product.id AS product_id
        .
        .
        .
    INNER JOIN bookmark_volumes AS bookmark
        ON bookmark.product_id = product.id
    WHERE
        product.id =  %(product_id)s
        AND product.is_deleted =0
"""

query_with_account= """
    , EXISTS(
        SELECT
            id
        FROM
            bookmarks
        WHERE
            account_id = %(account_id)s
            AND product_id = %(product_id)s
            AND is_deleted = 0
        ) AS is_bookmarked
"""

2. INNER JOIN / LEFT JOIN 을 사용할없는 문제

INNER JOIN / LEFT JOIN 을 사용할 수 없기때문에 쿼리를 나누어서 DAO로 내보내고 서비스에서 조합해서 반환하는 방법으로 문제해결을 했다.


images   = self.product_dao.get_product_images_dao(connection, data)
sizes    = self.product_dao.get_product_sizes_dao(connection, data)
colors   = self.product_dao.get_product_colours_dao(connection, data)
products = self.product_dao.get_product_detail_dat(connnection, data)

product['images']  = images
product['sizes']   = sizes
product['colours'] = colours
return product

⚡️ 상품 검색 페이지

PROBLEM

쿼리스트링으로 입력받은 키워드를 '상품이름', '셀러이름' 과 겹치는 데이터를 모두 반환해주어야 했고 최소 20개의 데이터를 보여주는것으로 하며, 정렬은 추천순, 판매량순, 최신순을 기준으로 삼아야한다.

SOLUTION

  1. 쿼리스트링으로 검색 키워드는 q, 개수는 limit, 그리고 정렬은 sort_type으로 받는방법을 사용하였다.
@validate_params(
    Params('q', GET, str, required=True),
    Params('limit', GET, str, required=True, rule=[NumberRule()],
    Params('sort_type', GET, str, required=True, rule=[SortTypeRule()])
    )

검색 api가 실행되면 무조건 모두 받아야되는 값임으로 flask-request-validator 를 사용하였다.


class SortTypeRule(AbstractRule):
    def validate(self, value):
        sort_type = [1, 2, 3]
        errors = []
        if value not in sort_type:
            errors.append('1~3 값만 받습니다')
        return value, errors
  1. 데이터의 개수 제한인 limit 값과 sort_type은 고정 값만 받아 올수 있음으로 flask-request-validator 라이브리에서 AbstractRule 상속받아 커스텀 Rule을 제작해 주었다. 정렬값은 아래와 같이 사전에 프론트와 정의해두었다.
sort_type = {
    '1': '북마크추천순',
    '2': '판매순',
    '3': '최신순'
    }
  1. if 문과 case 문중에 어느것이 더 효율적일까 고민을 하던중, 필터라는 특성상 추가될 가능성이 있기때문에 확장성을 고려해야했다. 그래서 필터가 추가될 경우를 대비해 if 문보다 유지보수가 쉬운 case 문을 사용해 query 문을 구현하였다.
.
.
ORDER BY
    (CASE WHEN %(sort_type)s=1 THEN bookmark_count END) DESC
    , (CASE WHEN %(sort_type)s=2 THEN sales_count END) DESC
    , (CASE WHEN %(sort_type)s=3 THEN product.id END) DESC
LIMIT%(limit)s;
profile
매일 새로운 배움을 통해 꾸준히 성장하는 것을 목표를 두고 있습니다. 논리적인 사고로 문제해결 하는것에 희열을 느끼고 언젠가 제가 만든 결과물들이 사람들에게 편이를 제공하며 사용되는 날을 간절히 소망하고 있습니다. 🙏

0개의 댓글