구현 기능
- 관리자가 주문/배송 현황을 한 번에 조회할 수 있는 대시보드 API를 구현했다.
GET /api/admin/dashboard
- 상태별 주문 수 (PENDING_PAYMENT, PAID, CANCELLED)
- 오늘 주문 수
- 오늘 매출 (취소 주문 제외)
- 상태별 배송 수 (ACCEPT, INSTRUCT, DEPARTURE, DELIVERING, FINAL_DELIVERY)
- 이슈에는 /orders와 /deliveries 두 개로 분리하려 했는데, 대시보드 특성상 한 번에 보여주는 게 더 자연스럽다고 판단해서 하나의 엔드포인트로 통합했다.
사용한 기술과 선택 이유
1. Spring Data JPA 쿼리 메서드
주문 수 집계에 사용한 방식
long countByStatus(OrderStatus status);
long countByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
- Spring Data JPA의 메서드 네이밍 규칙을 활용했다. 별도의 JPQL 없이 메서드 이름만으로 쿼리가 자동 생성된다.
왜 선택했나? 단순한 count 쿼리는 메서드 네이밍으로 충분히 표현 가능하고 코드가 간결하다. 복잡한 조건이 없는 경우 JPQL을 직접 작성하는 것보다 유지보수가 쉽다.
2. @Query (JPQL)
오늘 매출 집계에 사용한 방식
@Query("select coalesce(sum(o.finalPrice), 0) from Order o " +
"where o.createdAt between :start and :end " +
"and o.status = :status")
Long sumFinalPriceByCreatedAtBetween(
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("status") OrderStatus status
);
왜 JPQL을 선택했나? sum + coalesce + 상태 필터를 조합해야 하는데 메서드 네이밍만으로는 표현이 불가능하다. 이런 경우 @Query로 직접 작성하는 게 맞다.
처음에 status를 하드코딩했다가 수정한 이유
처음에는 아래처럼 작성했다.
// 처음 코드 (잘못된 방법)
@Query("...and o.status = 'PAID'")
- AI 리뷰에서 지적받았다. OrderStatus enum이 바뀌면 쿼리 문자열도 같이 수정해야 하는 문제가 생긴다. 파라미터로 받도록 수정해서 재사용성을 높였다.
coalesce를 쓴 이유 주문이 하나도 없을 때 sum은 null을 반환한다. coalesce(sum(...), 0)으로 감싸면 null 대신 0을 반환해서 NPE를 방지할 수 있다. 추가로 서비스 레이어에서도 Optional.ofNullable(...).orElse(0L)로 한 번 더 null 안전 처리를 했다.
3. @Modifying(clearAutomatically = true)
- 배치 업데이트 쿼리에 적용했다. (판매자 강제 비활성화에서 이미 사용)
@Modifying(clearAutomatically = true)
@Query("update Product p set p.status = :status where p.seller.id = :sellerId")
int updateStatusBySellerId(...);
clearAutomatically = true란? 배치 업데이트 후 영속성 컨텍스트를 자동으로 클리어해서 이후 조회 시 DB의 최신 상태를 반환하도록 보장한다. 이 옵션이 없으면 업데이트가 반영됐음에도 캐시된 이전 데이터를 읽어올 수 있다.
다른 기술과 비교
집계 방식 비교
| 방식 | 장점 | 단점 | 사용하면 좋은 곳 |
| JPA 쿼리 메서드 | 간결, 유지보수 쉬움 | 복잡한 쿼리 표현 불가 | 단순 count, exists |
| @Query JPQL | 복잡한 쿼리 표현 가능, 타입 안전 | 직접 작성 필요 | sum, 복합 조건 쿼리 |
| QueryDSL | 동적 쿼리, 컴파일 타임 오류 감지 | 설정 복잡, 학습 필요 | 검색 조건이 동적인 경우 |
| Native Query | DB 최적화 쿼리 직접 사용 | DB 종속성, 타입 불안전 | 극한의 성능 최적화 필요 시 |
- 대시보드는 고정된 집계 쿼리라 QueryDSL이나 Native Query까지 쓸 필요는 없었다.
대시보드 데이터 조회 방식 비교
방법 1. Enum 순회 + 개별 count 쿼리 (현재 방식)
Map<OrderStatus, Long> orderStatusCount = Stream.of(OrderStatus.values())
.collect(Collectors.toMap(
status -> status,
status -> orderRepository.countByStatus(status)
));
- 상태가 3개면 쿼리 3번 발생한다.
방법 2. GROUP BY로 한 번에 조회
@Query("select o.status, count(o) from Order o group by o.status")
List<Object[]> countGroupByStatus();
- 쿼리 1번으로 전체 상태별 카운트를 가져올 수 있다.
왜 현재 방식을 선택했나?
AI 리뷰에서 N+1 문제를 지적받았지만 MVP 단계에서는 무시했다. 이유는 두 가지다.
첫째, 대시보드는 관리자만 보는 화면이라 호출 빈도가 낮다. 상태 수만큼 쿼리가 나가더라도 실제 성능 영향이 거의 없다.
둘째, Object[] 형태로 받아서 Map으로 변환하는 코드가 오히려 더 복잡해진다. 상태가 3~5개 수준에서는 가독성이 더 중요하다.
트래픽이 많아지거나 대시보드가 더 복잡해지면 그때 GROUP BY 방식으로 리팩토링하면 된다.
AI 리뷰 보고 수정한 것
status 하드코딩 제거
// 수정 전 (잘못된 방법)
@Query("...and o.status = 'PAID'")
Long sumFinalPriceByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
// 수정 후
@Query("...and o.status = :status")
Long sumFinalPriceByCreatedAtBetween(
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end,
@Param("status") OrderStatus status
);
- OrderStatus.PAID를 파라미터로 넘기도록 변경했다. 이렇게 하면 enum 값이 바뀌어도 컴파일 타임에 오류가 잡히고, 다른 상태의 매출도 조회할 수 있어 재사용성이 높아진다.
'💻 PROJECT > Team Projects' 카테고리의 다른 글
| [ 🚚 ONESTOP ] Spring Boot 모니터링(Prometheus + Grafana) + Swagger JWT 인증 설정 (0) | 2026.05.21 |
|---|---|
| [ 🚚 ONESTOP ] CI/CD부터 관리자 API까지 - 기술 선택의 이유 (0) | 2026.05.18 |
| [ 🚚 ONESTOP ] 정책 설계 — 옵션 중복 방지와 상태 관리 (0) | 2026.05.15 |
| [ 🚚 ONESTOP ] Docker Compose + GitHub Actions로 팀 개발 환경 & CI 구축하기 (0) | 2026.05.14 |
| [ 🚚 ONESTOP ] Spring Boot 프로젝트 기술 선택 이유 정리 (0) | 2026.05.13 |