Spring 7기 프로젝트/모임 플렛폼 프로젝트
Spring Data JPA에서 미리 집계된 카운트를 활용한 팔로우 목록 조회 최적화
JuNo_12
2025. 7. 20. 20:04
문제 상황
팔로우 시스템을 구현할 때 많은 개발자들이 팔로잉 수와 팔로워 수를 미리 User 엔티티에 집계해서 저장합니다. 하지만 정작 팔로우 목록을 조회할 때는 여전히 COUNT 쿼리가 발생하여 집계 데이터의 의미가 퇴색되는 경우가 많습니다.

기존 방식의 문제점
1. 불필요한 COUNT 쿼리 발생
// 기존 Repository 메서드
@Query("SELECT u FROM User u WHERE u.id IN (...)")
List<User> findFollowingsByUserId(@Param("userId") Long userId, Pageable pageable);
// 실제 실행되는 쿼리:
// 1. SELECT u FROM User u WHERE ... LIMIT 20 OFFSET 0
// 2. SELECT COUNT(u) FROM User u WHERE ... ← 불필요한 COUNT 쿼리!
2. 미리 집계한 데이터 활용 못함
User 엔티티에 followingCount와 followerCount를 미리 계산해서 저장해놨는데, 목록 조회 시에는 이 값들을 사용하지 않고 다시 COUNT 쿼리를 실행하게 됩니다.
해결 방안
1. Slice 활용으로 COUNT 쿼리 제거
Spring Data JPA에서 Page 대신 Slice를 사용하면 COUNT 쿼리를 방지할 수 있습니다.
// Page vs Slice 비교
Page<User> users = repository.findAll(pageable); // COUNT 쿼리 발생
Slice<User> users = repository.findAll(pageable); // COUNT 쿼리 없음
Slice 동작 방식:
- 요청한 개수보다 1개 더 조회하여 다음 페이지 존재 여부 판단
- 예: 20개 요청 시 21개를 조회해서 hasNext() 결정
2. Repository 메서드 수정

3. 응답 DTO 설계

4. Service 레이어 구현

DTO 구조 설계
개별 사용자 정보 DTO

목록 + 페이징 정보 래퍼 DTO

DTO 조합의 장점:
- 재사용성: 개별 사용자 DTO를 여러 곳에서 활용 가능
- 일관성: 사용자 정보는 항상 같은 구조
- 단일 책임: 각 DTO가 명확한 역할 분담
- 유지보수: 한 곳만 수정하면 모든 곳에 반영
성능 개선 결과
Before (기존 방식)
-- 1번째 쿼리: 데이터 조회
SELECT u.id, u.nickname, ... FROM users u WHERE ... LIMIT 20 OFFSET 0;
-- 2번째 쿼리: COUNT (불필요!)
SELECT COUNT(u.id) FROM users u WHERE ...;
After (개선된 방식)
-- 1번째 쿼리: 사용자 조회 (미리 집계된 totalCount 획득)
SELECT u.id, u.following_count FROM users u WHERE u.id = 1;
-- 2번째 쿼리: 데이터 조회만 (COUNT 없음)
SELECT u.id, u.nickname, ... FROM users u WHERE ... LIMIT 21 OFFSET 0;
성능 향상:
- 쿼리 수: 3번 → 2번
- COUNT 쿼리 제거로 인한 성능 향상
- 미리 집계된 데이터의 효과적 활용
클라이언트에서의 활용
const response = {
users: [...],
totalCount: 150,
currentPage: 0,
pageSize: 20
};
// 필요한 페이징 정보 계산
const hasNext = (response.currentPage + 1) * response.pageSize < response.totalCount;
const totalPages = Math.ceil(response.totalCount / response.pageSize);
const isLastPage = response.currentPage >= totalPages - 1;
결론
미리 집계된 카운트 데이터를 제대로 활용하려면 단순히 데이터를 저장하는 것만으로는 부족합니다. 조회 로직에서도 이를 적극적으로 활용하고, Spring Data JPA의 Slice를 통해 불필요한 COUNT 쿼리를 제거해야 합니다.
이러한 최적화를 통해 다음과 같은 이점을 얻을 수 있습니다:
- 성능 향상: COUNT 쿼리 제거로 인한 응답 시간 단축
- 일관성: 미리 집계된 데이터와 실제 조회 로직의 통합
- 확장성: 대용량 데이터에서도 안정적인 성능 보장
- 유지보수성: 명확한 DTO 구조와 역할 분리
팔로우 시스템뿐만 아니라 다양한 집계 데이터를 다루는 상황에서 이와 같은 패턴을 적용할 수 있습니다.