Home 트러블 슈팅 - querydsl 문제
Post
Cancel

트러블 슈팅 - querydsl 문제

회사에서 진행한 작업이기 때문에 실제 코드가 아닌 예시 코드로 적겠습니다.

상황

엔티티 상황

간단한 예시로 가게(Shop)와 해당 가게에서 파는 물건(Goods)이 각각 엔티티로 있다고 하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Entity
public class shop {
  @Id
  private Long id;
  private String name;
  private String ownerName;
}

@Entity
public class goods {
  @Id
  private Long id;
  private String name;
  private boolean activated;
  private int price;
  @ManyToOne
  private Shop shop;
}

요구 사항

구현해야 할 기능의 요구사항은 아래와 같습니다.

  • Shop의 목록을 조회해야 하며, Paging 처리가 필요하다.
  • 프론트 요청에 따라 조건이 달라져야 한다. 즉, 동적 쿼리로 구현해야 한다.
  • 해당 Shop의 Goods 중, 프론트에서 요청한 조건에 부합하고 goods.activated=true인 상품의 총액을 반환해야 한다.
  • 조건에 부합하는 Goods가 없거나 총액이 0 이하인 경우 반환할 Shop 목록에서 제외시켜야 한다.

접근

우선 쿼리문 부터 작성을 해 보았습니다. 서브 쿼리를 사용한 쿼리문과, GROUP BY를 사용한 쿼리문, 총 두 개의 쿼리문을 작성하였습니다.
두 쿼리는 동일한 값을 반환하지만, goods를 Limit 없이 조회하는 첫 번째 쿼리문 보다 두 번째 쿼리문이 효율적이었습니다.

쿼리문 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
--- from 절에 sub query 이용
SELECT sq.shop_id, sq.shop_name, sq.shop_owner_name, sq.total_goods_amount
FROM (
    SELECT shop.id         AS shop_id,
           shop.name       AS shop_name,
           shop.owner_name AS shop_owner_name,
           (
             SELECT COALESCE(SUM(goods.price), 0)
             FROM goods
             WHERE goods.activated = true
               AND goods.shop_id = shop.id
           ) AS total_goods_amount
     ) AS sq
-- JOIN 문 생략
WHERE sq.total_goods_amount > 0
--   AND
--       동적 쿼리 생략...
;

--- group by 이용
SELECT shop.id, shop.name, shop.owner_name, COALESCE(SUM(goods.price), 0) AS total_goods_amount
FROM goods
       INNER JOIN shop
                  ON goods.shop_id = shop.id
-- JOIN 문 생략
GROUP BY shop.id, shop.name, shop.owner_name
HAVING total_goods_amount > 0
-- WHERE 동적 쿼리 생략...
;

mysql 표준 GROUP BY 사용 규칙

  • GROUP BY는 GROUP BY에 정의한 내용(컬럼 또는 변형된 컬럼)만 SELECT절에 그대로 사용할 수 있다.
  • GROUP BY에 정의하지 않은 컬럼을 SELECT절에서 사용하려면 반드시 집계함수 처리를 해야 한다.

구현

JPQL, Native Query 등 방식은 여러 가지일 수 있지만, 동적 쿼리라는 요구사항에 가장 적합한 QueryDsl로 구현하고자 하였습니다.

하지만 구현하는 과정에서 두 쿼리문 모두 문제가 발생하였습니다.

문제 발생

첫 번째 쿼리문 (sub query)

‘JPQL에서 서브쿼리는 WHERE, HAVING 절에서만 사용할 수 있습니다.’

JPQL FROM 절 서브쿼리가 스펙상 불가능하다고 합니다. 따라서 개발하고자 한 Querydsl 에서도 불가능했습니다.

두 번째 쿼리문 (group by)

‘when groupBy with two fields,fetchCount throw exception’ https://github.com/querydsl/querydsl/issues/2504

paging으로 반환하기 위해 fetchCount() 메소드를 사용했는데 에러가 발생했습니다.

위의 깃허브 이슈를 보면 group by로 2개 이상의 컬럼을 걸어줄 경우 Count Query가 JPQL로의 변환이 실패된다고 합니다.

해결

간단하게 native query를 사용하면 되겠지만 동적 쿼리가 걸렸고, pagenation을 포기할 수도 없었습니다.

고민 끝에 QueryDsl을 native query로 변환해주는 클래스를 추가하여 해결하였습니다.

위 이슈에서 gudaoxuri 라는 분의 댓글에 jpaQuery를 serialize 하여 native query로 변경하고 카운트 쿼리와 실제 쿼리문을 만들어 줄 수 있다고 합니다.
해당 코드와 https://myborn.tistory.com/26의 글을 참고했습니다.

배운 점 요약

  • JPQL에서 서브쿼리는 WHERE, HAVING 절에서만 사용할 수 있음.
  • QueryDsl에서 2개 이상의 group by를 사용했을 때, count query 변환에 오류가 발생함.
  • QueryDsl도 native query로 변경할 수 있음.
This post is licensed under CC BY 4.0 by the author.

스프링 서버의 세상 간단한 CI/CD 구축 방법 (with Docker, Github Actions, AWS EC2)

SonarCloud로 코드 분석하기