ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 필터링 조회 문제 해결
    팀프로젝트 [Brewscape] 2025. 2. 9. 21:05

    🤔문제 발생

    • 여러 개의 태그를 가진 게시물을 여러 개의 태그로 조회하는 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(); }
    • 쿼리 동작 방식
      1. join을 이용하여 review테이블과 tag테이블을 연결
      2. where문을 이용하여 요청한 태그가 하나라도 포함된 review만 1차 필터링
      3. 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가지가 있다.
      1. 동적 쿼리를 사용하는 방법
      2. join + groupBy + having을 사용하는 방법
    • 성능은 거의 모든 면에서 두번째 방법이 좋았다. 다만 Collection 타입의 필드를 사용할 경우에는 첫번째 방법을 사용하는것이 좋다.
Designed by Tistory.