프로젝트 3주차 회고

송형근·2024년 10월 13일

Side Project - Astraphe

목록 보기
3/4
post-thumbnail

중간 발표가 있는 주차였기 때문에, MVP를 최대한 완료하려고 노력해서 80%이상은 구현 완료

진행 사항

  • Product CRUD
  • Product Search - querydsl
  • category Entity
  • data.sql 작성
  • Spring Boot Application Exclude
  • Product 재고 수정 로직 구현
  • 모니터링 설정
  • querydsl을 사용해서 상품 검색을 구현했음
  • 검색 조건이나 정렬 조건은 querydsl로 간편하게 구현했지만, 카테고리도 조건에 함께 추가하려다보니 어떤 방식으로 구현할 지 고민이 되었음
  • 카테고리는 더미데이터로 1000, 2000, 3000번만 존재하고 있으며, 전체 카테고리에 대한 검색도 필요하다고 생각 했기 때문에 전체 카테고리 검색도 필요했음

Product Service 내 Search 구현

    public Page<ProductResponseDto> searchProduct(String companyName, String productName, Long categoryCode, Pageable pageable){
        if(!categoryCode.equals(0L)){ // category 0은 전체
            try{
                categoryRepository.findById(categoryCode).orElseThrow();
            }catch (Exception e){
                throw new ApiException(HttpStatus.BAD_REQUEST, "잘못된 Category 입니다", e.getMessage());
            }
        }

        try{
            if(!(companyName ==null)){
                Page<ProductResponseDto> productResponseDtos = productRepository.searchByCompanyName(companyName, categoryCode, pageable).map(ProductResponseDto::from);
                return productResponseDtos;
            }else{
                Page<ProductResponseDto> productResponseDtos = productRepository.searchByProductName(productName, categoryCode, pageable).map(ProductResponseDto::from);
                return productResponseDtos;
            }
        }catch (Exception e){
            throw new ApiException(HttpStatus.INTERNAL_SERVER_ERROR, "Product Search에 실패했습니다.", e.getMessage());
        }
    }
  • 전체 카테고리는 카테고리 테이블에는 존재하지 않으므로 전체 카테고리(0L)에 대해서는 카테고리 유효성 검증을 제외

ProductCustomRepositoryImpl

  • Product QueryDSL 구현체
@RequiredArgsConstructor
public class ProductCustomRepositoryImpl implements ProductCustomRepository{

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public Page<Product> searchByCompanyName(String companyName, Long categoryCode, Pageable pageable) {
        List<OrderSpecifier> orders = getAllOrderSpecifiers(pageable);

        List<Product> query;
        JPAQuery<Long> countQuery;

        if(categoryCode.equals(0L)){
            query = jpaQueryFactory
                    .selectFrom(product)
                    .distinct()
                    .where(
                            product.isDeleted.isFalse(),
                            product.company.companyName.contains(companyName)
                    )
                    .offset(pageable.getOffset())
                    .limit(pageable.getPageSize())
                    .orderBy(orders.stream().toArray(OrderSpecifier[]::new))
                    .fetch();
            countQuery = jpaQueryFactory
                    .select(product.count())
                    .from(product)
                    .where(
                            product.isDeleted.isFalse(),
                            product.company.companyName.contains(companyName)
                    );

            return PageableExecutionUtils.getPage(query, pageable, () -> countQuery.fetchOne());
        }

        query = jpaQueryFactory
                .selectFrom(product)
                .distinct()
                .where(
                        product.isDeleted.isFalse(),
                        product.company.companyName.contains(companyName),
                        product.category.categoryCode.eq(categoryCode)
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(orders.stream().toArray(OrderSpecifier[]::new))
                .fetch();

        countQuery = jpaQueryFactory
                .select(product.count())
                .from(product)
                .where(
                        product.isDeleted.isFalse(),
                        product.company.companyName.contains(companyName),
                        product.category.categoryCode.eq(categoryCode)
                );

        return PageableExecutionUtils.getPage(query, pageable, () -> countQuery.fetchOne());
    }

    @Override
    public Page<Product> searchByProductName(String productName, Long categoryCode, Pageable pageable) {
        List<OrderSpecifier> orders = getAllOrderSpecifiers(pageable);

        List<Product> query;
        JPAQuery<Long> countQuery;

        if(categoryCode.equals(0L)){
            query = jpaQueryFactory
                    .selectFrom(product)
                    .distinct()
                    .where(
                            product.isDeleted.isFalse(),
                            product.productName.contains(productName)
                    )
                    .offset(pageable.getOffset())
                    .limit(pageable.getPageSize())
                    .orderBy(orders.stream().toArray(OrderSpecifier[]::new))
                    .fetch();
            countQuery = jpaQueryFactory
                    .select(product.count())
                    .from(product)
                    .where(
                            product.isDeleted.isFalse(),
                            product.productName.contains(productName)
                    );

            return PageableExecutionUtils.getPage(query, pageable, () -> countQuery.fetchOne());
        }

        query = jpaQueryFactory
                .selectFrom(product)
                .distinct()
                .where(
                        product.isDeleted.isFalse(),
                        product.productName.contains(productName),
                        product.category.categoryCode.eq(categoryCode)
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(orders.stream().toArray(OrderSpecifier[]::new))
                .fetch();

        countQuery = jpaQueryFactory
                .select(product.count())
                .from(product)
                .where(
                        product.isDeleted.isFalse(),
                        product.productName.contains(productName),
                        product.category.categoryCode.eq(categoryCode)
                );

        return PageableExecutionUtils.getPage(query, pageable, () -> countQuery.fetchOne());
    }
}
  • 전체 카테고리(0L)에 대해서는 전체 검색을 실시하고, 아닌 경우에 대해서는 categoryCode도 검색 조건에 추가하는 방식으로 구현

data.sql

  • 카테고리는 API로 추가하지않고 임의로 추가해두기로 의사결정을 내림
  • DB에 직접 추가해두는 방식과 미리 데이터를 생성해두는 방식 중에 고민을 하다가 DB가 바뀌면 데이터를 또 새로 추가해줘야한다는 부분이 번거로울 것 같아서 data.sql을 작성하게 되었음

Create Table 필요성

  • 초기에는 Entity를 먼저 생성해둔 상태였기 때문에, Create Table은 필요 없겠지 하고 데이터만 작성을 해뒀음
  • 외부 일정이 있어 랩탑을 챙겨 나가서 실행을 했는데, 테이블이 없어서 Error가 발생하는 것을 보고, data.sql의 실행 시점이 JPA가 Entity를 생성하는것보다 빠르구나 하는 것을 알게되었음
  • 그래서 data.sql에 IF NOT EXISTS 조건을 추가해서 Create Table sql도 추가함

Reduce Product Kafka 적용

  • 주문 시 재고 감소를 비동기적으로 처리하기 위해 kafka를 적용했음

ServletRequestAttributes NullPointer Exception

모니터링 설정

  • 프로젝트의 요건에 모니터링 환경 구축 및 알람 설정이 있었기 때문에 해당 부분 작업 진행

prometheus expected a valid start token, got "<" ("invalid") while parsing: "<" Issue

  • Order 서버가 정상적으로 실행되었음에도 불구하고 해당 로그를 남기며 Prometheus에서 metric을 가져오는 데 실패하고 있었음
  • Order 서버의 Build.gradle부터 찬찬히 훑어보던 중 spring security 의존성이 존재하는데, config 설정이 없는 부분을 확인
  • 담당자분께 해당 부분 설정 요청드리고, Local에서 Permit 설정 후 재기동하니 해결 완료
  • 예전에 어렴풋이 겪었던 이슈였어서 비교적 빠르게 조치를 할 수 있었음

서버 다운 알람 템플릿 작성

  • 기본 템플릿을 사용해 메시지를 전송하게 하니까 영 이쁘지 못해서 템플릿 작성을 시도함
{{ if gt (len .Alerts.Firing) 0 }}
[DOWN] {{ (index .Alerts.Firing 0).Labels.alertname }} ({{ (index .Alerts.Firing 0).Labels.job }}
*DOWN*
{{ else if gt (len .Alerts.Resolved) 0 }}
[OK] {{ (index .Alerts.Resolved 0).Labels.alertname }} ({{ (index .Alerts.Resolved 0).Labels.job }}
*OK*
{{ end }}
  • 위와 같은 느낌으로 alertName과 jobName을 활용해서 아래와 같이 전송되도록 템플릿 작성

P.S.

  • 원하던 목표를 100%까진 아니더라도 80%이상 달성했고, 시행착오도 겪으면서 많이 성장했던 한 주 였다.
  • 이력서 작성에 팀원 분의 CI/CD 까지 도와주다보니 주말까지 정신없이 시간이 지나갔던 것 같다...
profile
티스토리로 이주 (https://lucas-song94.tistory.com)

0개의 댓글