⌨️ 비즈니스 로직 구현 회고
내가 맡은 부분은 주로 상품 쪽이었다. 상품 외에도 여러 가지 자질구레하게 진행했으나, 대부분은 상품관 관련이 있었기에 이 쪽의 비즈니스 로직 구현을 회고하려 한다.
1. 신상품 페이지 구현
첫번 째는 신상품 페이지의 구현이다. 크게는 2가지의 로직이 존재한다. 첫 번째로는 페이지네이션 정렬 된 신상품의 출력, 그리고 얹어서
왼쪽 카테고리의 로직을 추가한 신상품의 출력이다.
1.1. 신상품 Pagination 구현
페이지네이션은 쉬워보이지만, 깊게 들어가면 어려운 부분이 한 두 개가 아니다. offset의 활용이나 여러 부분에서의 최적화 등 생각보다 고려할 것이 많았다.
//신상품 조회
@Override
public Page<ProductDto> readProductWithSortedType(String sortedType, String filter, Pageable pageable) throws UnsupportedEncodingException {
if(filter != null){
HashMap<String, String> filterMap = searchFilter.hashFilterMap(filter);
String category = filterMap.get("category");
String brand = filterMap.get("brand");
List<String> categoryList = searchFilter.listFilter(category);
List<String> brandList = searchFilter.listFilter(brand);
if(sortedType.equals("lower"))
return productRepository.findAllWithSortAndFilter(categoryList,brandList,PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("price")))
.map(ProductDto::from);
else if(sortedType.equals("higher"))
return productRepository.findAllWithSortAndFilter(categoryList,brandList,PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("price").descending()))
.map(ProductDto::from);
return productRepository.findAllWithSortAndFilter(categoryList,brandList,PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("id").descending()))
.map(ProductDto::from);
} else {
if(sortedType.equals("lower"))
return productRepository.findAll(PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("price")))
.map(ProductDto::from);
else if(sortedType.equals("higher"))
return productRepository.findAll(PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("price").descending()))
.map(ProductDto::from);
}
return productRepository.findAll(PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by("id").descending()))
.map(ProductDto::from);
}
우선 내가 구현한 서비스 로직이다. 상당히 지저분하고 뭔가 중복이 많아 보이긴 한다. 리팩토링을 해야할 부분으로 TODO로 남겨두었다
모든 코드를 포스팅에서 오픈하진 않겠지만 ( Github에 있기에)
첫째로 페이지네이션 필터는 컨트롤러에서 sortedType을 받아와서 그것에 맞는 분기를 나누었다.
두 번째로 카테고리의 적용은 우선적으로 필터링 한 Order by를 기준으로 select를 진행하였다.
해당 부분은 querydsl로 구현하였다.
@Override
public Page<Product> findAllWithSortAndFilter(List<String> category, List<String> brand, Pageable pageable) throws UnsupportedEncodingException {
QProduct product = QProduct.product;
List<Product> products =
from(product)
.select(product)
.where(eqCategoryCode(category),(eqBrand(brand)))
.orderBy(productSort(pageable))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
Long count =
from(product)
.select(product.count())
.where(eqCategoryCode(category),(eqBrand(brand)))
.fetchOne();
return new PageImpl<>(products,pageable,count);
}
private BooleanExpression eqCategoryCode(List<String> category){
return category != null ? product.categoryDetail.category.categoryCode.in(category) : null;
}
private BooleanExpression eqBrand(List<String> brand){
return brand != null ? product.brand.in(brand) : null;
}
private OrderSpecifier<?> productSort(Pageable pageable) {
if (!pageable.getSort().isEmpty()) {
for (Sort.Order order : pageable.getSort()) {
Order direction = order.getDirection().isAscending() ? Order.ASC : Order.DESC;
switch (order.getProperty()) {
case "id":
return new OrderSpecifier<>(direction, product.createdAt);
case "price":
return new OrderSpecifier<>(direction, product.price);
}
}
}
return null;
}
카테고리와 브랜드( 현재 모든 상품은 null 이기에 프런트에선 구현을 안 함)를 받아 입력받은 카테고리나 브랜드가 있다면 해당 변수를 포함한 상품을 select 한다.
또한, 정렬 (sortedType)을 받은 것이 있다면, 해당 내용은 정렬을 해주는 것도 추가하였다.
우리의 신상품 페이지 url은 이런 방식이었다.
/newproduct?sorted_type=lower&filter=category%3A001%2C002%7Cbrand%3A풀무원%2CCJ
즉 RequestParam filter에 대한 분기가 필요했다.
category%3A001%2C002%7Cbrand%3A풀무원%2CCJ
이 부분을 내가 원하는 querydsl에 원하는 변수가 들어 갈 수 있게 로직을 구현했어야 하는 부분이다.
1.2. 신상품 RequestParam filter 에 대한 구현
filter를 어떻게 구현 할까 고민을 많이 했다. 최대한 고민하고 레퍼런스를 찾기보다는 로직을 세우고 그것이 효율적이지 못하다면, 공부를 통해 리팩토링을 하는 것으로 방향을 잡았다
1.2.1. query 문에 대한 이해.
아직까지는 회고가 굉장히 약한 것 같다. 모두가 보고 이해 할 수 있을 만큼이면 좋겠지만 해당 부분은 차츰 능력을 길러가는 것으로 하고 우선 내가 접근했던 방식을 그대로 작성하도록 하겠다.
처음은 query문에 대한 이해다.
결국은 내가 원하는 상품을 찾는 것을 query문으로 표현하자면 어떻게 되는가?
SELECT * FROM PRODUCTS
WHERE "CATEGORY = ? " AND "BRAND = ? "
약식으로 표현하자면 이렇게 된다.
우리가 변수로 넣어주고 싶은 부분은 "" 안의 부분이다.
1.2.2. "CATEGORY = ? " 방식이 뭐가 있지?
두 번째 접근은 내가 원하는 출력값을 표현해 주는 Java에서의 방식이 무엇이 있는지였다.
다행히 이것은 그나마 Java를 꾸준히 공부한 결과 hashmap을 바로 캐치해서 구현했다.
해시맵은 <K, V>의 형태로 저장되며 출력될 때는
K=V의 형태로 출력이 된다.
또한 중요한 점은 바로 값을 추가하여 줄 수 있다는 것이다. 이것이 무슨 말이냐?
카테고리의 중복을 구현해 줄 수 있다는 것이다. 즉 URL로
category%3A001%2C002
같이 001과 002가 들어온 다면 이를 전부 키 값 category에 넣어 value를 출력해 줄 수 있다는 의미다.
1.2.3. CATEGORY 구분은 어떻게?
이건 아주 쉽게 해결했다 그냥 %2C (* , : protocool로 인해 몇몇 보낼 수 없는 특수문자들을 나타내야 할 때 변형해서 사용)
를 기준으로 문자를 끊어서 List로 만들었다.
@Transactional
@Component
public class SearchFilter {
public HashMap<String, String> hashFilterMap(String filter) throws UnsupportedEncodingException {
String filterDecoding = URLDecoder.decode(filter, "UTF-8");
HashMap<String, String> filterMap = new HashMap<>();
String[] filterList = filterDecoding.split("\\|");
if(filter.equals("")) return null;
for (String s : filterList) {
int idx = s.indexOf(":");
String key = s.substring(0, idx);
String value = s.substring(idx + 1);
filterMap.put(key, value);
}
return filterMap;
}
public List<String> listFilter(String filterList) {
if(filterList == null) return null;
if(filterList.contains(",")) return List.of(filterList.split(","));
else return List.of(filterList);
}
}
위의 두 가지를 고려하여 만든 로직이다.
위 과정을 거쳐 만들어낸 변수 List <String> category 가? 부분에 들어가서 우리가 원하는 select를 진행해 준다.
구현하고 보니 되게 간단해 보이지만, 막상 만들었을 당시에는 querydsl의 이해도 없고, 많이 헤맸었다.
이렇게 회고를 하며 다시 보니, 로직의 구멍이 조금씩 보이는 것 같다.
2. 베스트 상품 페이지 구현
베스트 상품은 전반적으로는 신상품을 활용하여 만들었다.
우리가 추구하는 것이 뭔가? OOP 바로 객체를 활용하고 재사용을 권장하는 것이다.
이미 만들어진 객체가 있다면 재사용해서 쓰는 것이 좋다 당연히.
2.1. 베스트의 로직
베스트의 로직이 얼마나 복잡해지느냐에 따라 당연히 코드가 어려워지겠지만,
우리는 당장은 베스트의 로직을 넣을 만한 게 마땅치 않았기에 구매 횟수 n번 이상을 베스트 상품으로 출력해 주기로 했다.
@Override
public Page<Product> findBestProducts(List<String> category, List<String> brand, Integer frequency, Pageable pageable) {
QOrderDetail orderDetail = QOrderDetail.orderDetail;
QProduct product = QProduct.product;
QPayment payment = QPayment.payment;
JPQLQuery<Product> query =
from(orderDetail)
.select(product)
.innerJoin(orderDetail.product, product)
.where(orderDetail.product.id.eq(product.id),(orderDetail.orders.id.in(
JPAExpressions.select(payment.order.id).from(payment)
.where(
payment.paySuccessTf.eq(true)
))))
// .where(orderDetail.product.goe(createdAfterDateTime)
// .and(product.id.eq(orderDetail.product.id))
// .and(orderDetail.orders.id.in(
// JPAExpressions.select(payment.order.id).from(payment)
// .where(
// payment.paySuccessTf.eq(true)
// ))))
// .fetchJoin()
// .innerJoin(orderDetail.product ,product)
.where(eqCategoryCode(category),(eqBrand(brand)))
// .fetchJoin()
.groupBy(product.id)
.having(product.id.count().goe(frequency));
List<Product> products = getQuerydsl().applyPagination(pageable, query).fetch();
return new PageImpl<>(products, pageable, query.fetchCount());
}
해당 로직은 전체적으로는 신상품과는 같으나 구매 확정여부가 필요했기에
그 부분을 확인하는 로직과 그리고 n번이상의 상품들만을 그룹화한 로직을 추가해 주었다.
추가로 간단한 카테고리의 조회나 상품에 대한 통합검색은 다른 포스트에서 작성토록 하겠다
3. 피드백
- 신상품 비즈니스 로직 리팩토링 하기 -> 서비스 로직 및 DB 탐색과 페이지네이션
- 조금 더 구체적이고 단계적으로 회고하기 -> 내가 작성한 로직이니까 나 스스로는 쓰윽 지나가기만 해도 이해가 쉽다,
그러나 보는 사람을 고려하여 조금 더 친절하게 회고를 작성해 보기