🤔문제 발생
- 여러 개의 태그를 가진 게시물을 여러 개의 태그로 조회하는 API를 제작해야 했다.
- 지정한 모든 태그를 포함한 게시물만 조회해야 했다.
- IN 쿼리로 시도했지만 태그 중 하나만 포함되어도 조회되는 문제 발생
public List<Review> tagSearch(List<Integer> selectedTagIds){
return queryFactory .from(review)
.where(tag.tagId.in(selectedTagIds))
.fetch();
}
⛏해결 과정
✅ 시도 1: groupBy + having 사용 → 실패
public List<Review> tagSearch(List<Integer> selectedTagIds){
return queryFactory
.from(review)
.where(tag.tagId.in(selectedTagIds))
.groupBy(review.id)
.having(tag.tagId.count().eq(selectedTagIds.size()))
.fetch();
}
- 구현 실패
- 일부 태그만 포함되더라도 태그 개수만 맞으면 조회되는 문제가 발생.
- → 지정한 모든 태그를 포함하는 게시물만 조회하는 ****조건을 만족하지 못함
✅ 시도 2: contains() 활용 → 실패
public List<Review> tagSearch(List<Integer> selectedTagIds)
return queryFactory
.from(review)
.where(review.tags.contains("tag1"))
.fetch();
}
- 구현 실패
- contains()는 특정 1개의 태그 포함 여부만 확인 가능.
- 태그 여러개 포함 여부는 확인 불가능하여 조건을 만족하지 못함
✅ 시도 3: 태그 개수별 개별 조회 함수 작성 → 실패
- 1개 태그 조회, 2개 태그 조회... 태그 개수에 따른 조회하는 함수를 여러개 만드는 방법
- 문제점
✅ 시도 4: QueryDSL 동적 쿼리 활용 → 성공!
- 동적 쿼리를 이용하여 태그의 개수에 따라 태그 조회 조건이 변하는 단 한개의 함수 제작
public List<Review> tagSearch(List<Integer> selectedTagIds)
return queryFactory
.from(review)
.where(eqTags(selectedTagIds))
.fetch();
}
BooleanExpression
을 활용하여 태그 개수에 따라 WHERE 쿼리를 동적으로 조절하도록 함
// 모든 태그가 포함된 리뷰면 true
private BooleanExpression eqTags(List<Integer> selectedTagIds){
return selectedTagIds!= null && !selectedTagIds.isEmpty() ? Expressions.allOf(selectedTagIds.stream().map(this::isContainsTagId).toArray(BooleanExpression[]::new)) : null;
}
// 한 태그가 포함된 리뷰면 true
private BooleanExpression isContainsTagId(Integer selectedTagId) {
return review.tagIds.contains(selectedTagId);
}
- 장점
- join 쿼리를 사용하지 않음 → 쿼리 복잡도 낮음
- tag 필드가 Collection 타입인 경우도 사용 가능함
- 단점
- 조회하려는 태그가 많을 경우 WHERE절이 늘어남 → 성능 저하
- contains는 인덱스를 활용하지 않아 모든 행 검사가 일어남 → 리뷰 수가 많을 경우 성능 크게 저하
- 언제 사용하는게 좋은가?
- tag 필드가 Collection 타입인 경우 → 인덱스가 없으므로 가장 효율적
-
✅ 시도 5: join + groupBy + having 활용 → 성공!
- 여러개의 값들을 모두 포함하는 조회를 할때 join과, groupBy, having을 사용하면 문제를 손쉽게 해결할 수 있다.
public List<Review> tagSearch(List<Integer> selectedTagIds)
return queryFactory
.from(review)
.join(tag).on(tag.review.id.eq(review.id))
.where(tag.tagId.in(selectedTagIds))
.groupBy(review.id)
.having(tag.tagId.count().eq(selectedTagIds.size()))
.fetch();
}
- 쿼리 동작 방식
- join을 이용하여 review테이블과 tag테이블을 연결
- where문을 이용하여 요청한 태그가 하나라도 포함된 review만 1차 필터링
- groupBy로 tag를 그룹화 후 having으로 요청한 총 태그의 개수가 같은 review만 조회
- 장점
- join을 사용하므로 조회하려는 태그가 많아져도 성능 저하되지 않음
- 인덱스를 사용하여 조회 가능 → 리뷰 수가 많아져도 성능 저하되지 않음
- 단점
- join을 사용하므로 불필요한 연산 가능성 있음 → review와 tag에 인덱스가 존재하므로 문제없음
- tag 필드가 Collection 타입인 경우 QueryDSL에서 join이 불가능하므로 사용 불가
- → 이 경우, QueryDSL 대신 네이티브 SQL을 활용해야 함
❓동적 쿼리와 join+groupBy+having 성능 비교 (시도 4 vs 시도 5)
- 거의 모든 면에서 join+groupBy+having의 성능이 더 좋음
- tag 필드가 Collection 타입인 경우 동적 쿼리의 성능이 더 좋음
💎결론
- QueryDSL을 활용하여 여러개의 태그를 포함하는 게시물 조회하는 함수를 제작하였다.
- 제작 방법은 2가지가 있다.
- 동적 쿼리를 사용하는 방법
- join + groupBy + having을 사용하는 방법
- 성능은 거의 모든 면에서 두번째 방법이 좋았다. 다만 Collection 타입의 필드를 사용할 경우에는 첫번째 방법을 사용하는것이 좋다.