<aside> 🔑
요약
QueryDSL로 N:M 관계인 약초-태그 데이터를 페이징할 때, 페이징 범위에 따라 한 약초의 일부 태그만 조회되는 문제가 발생했다.
이는 .offset()
과 .limit()
이 조인된 태그 테이블까지 영향을 주면서 태그 데이터가 누락된 것이 원인이었다.
이를 해결하기 위해 먼저 약초 ID에만 페이징을 적용한 뒤, 해당 ID 목록을 바탕으로 태그와 기타 데이터를 별도로 조회하였다. GroupBy
와 Projections
를 활용하여 약초 하나에 여러 태그가 묶이도록 하여 데이터 일관성을 확보했다.
이 방식으로 정확한 페이징 결과와 함께 약초에 연결된 모든 태그가 누락 없이 반환되도록 개선되었다. 다대다 관계에서 페이징 시에는 ID 선조회 후 상세 조회 방식이 가장 안정적이라는 점을 배웠다.
</aside>
QueryDSL에서 N:M(중간 테이블)을 가지는 다대다 관계의 테이블을 조회하는 쿼리에 .offset()
과 .limit()
을 적용하여 페이징을 수행할 때, herb 엔티티 하나가 가져야 하는 태그 10개 모두 조회되는 것이 아니라 페이징 범위에 따라 10개 중 1개 → 2개 씩으로 조회되는 문제가 발생하였다.
<aside> 💬
즉, 페이징이 적용되지 말아야 할 자식 테이블의 로우인 태그까지도 영향을 받아서 의도와는 다른 조회가 이루어짐
</aside>
이 경우 페이징이 row 단위로 동작하기 때문에 페이징 처리시 page =0, size =1인 경우 herb 한 건에 태그 10개가 있다면, 그 중 1
개만 페이징 결과에 포함되고, 나머지 9개의 태그 데이터는 리스트에서 누락되는 문제가 발생했다.
List<Long> herbIds = jpaQueryFactory
.select(herb.id)
.from(herb)
.where(booleanBuilder)
.orderBy(orderSpecifier)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
List<HerbAdminDTO> herbList = jpaQueryFactory
.selectFrom(herb)
.leftJoin(herbTag).on(herbTag.herb.eq(herb))
.leftJoin(tag).on(tag.eq(herbTag.tag))
.leftJoin(herbViews).on(herbViews.herb.eq(herb))
.where(herb.id.in(herbIds))
.transform(
GroupBy.groupBy(herb.id).list(
Projections.constructor(HerbAdminDTO.class,
herb.id, herb.bneNm, herb.cntntsSj, herb.hbdcNm,
herb.imgUrl1, herb.imgUrl2, herb.imgUrl3, herb.imgUrl4, herb.imgUrl5, herb.imgUrl6,
herb.prvateTherpy, herb.useeRegn, herb.growthForm, herb.flowering,
herb.habitat, herb.harvest,
herbViews.viewCount,
GroupBy.list(
Projections.constructor(TagDTO.class,
tag.id, tag.name, tag.tagType
)
)
)
)
);
GroupBy
, join
, 페이징
, filter
조건이 복합적으로 얽힐 경우에는 페이징 대상 ID를 먼저 분리하고, 상세 조회 시에는 필터를 제거한 쿼리를 사용하는 방식이 가장 안정적이다.