블로그

Spring Data JPA에서 @Query를 통한 페이징 처리 시 주의점 - [2부]

등록일
2024-08-07 16:21:22
조회수
291

이어서 살펴보겠습니다.


11. createCountQueryFor를 실행하기 전, 어떤 queryEnhancer를 사용하는지 확인해봐야 합니다.


12. StringQuery 클래스의 생성자를 살펴보도록 하죠. QueryEnhancerFactory의 static 메서드가 있는 것 같습니다.


13. 저는 네이티브쿼리를 사용했기에, if문을 탑니다. 아쉽게도 jSqlParser를 사용하지 않고 있어, DefaultQueryEnhancer를 사용하게 됩니다.


14. 결국 DefaultQueryEnhancer 클래스에 선언된 createCountQueryFor 함수를 호출하게 되는데, 이는 QueryUtils에 선언된 static 메서드임을 알 수 있습니다.


그럼 이제, countQuery를 만들어주는 로직을 볼 수 있을 것 같습니다.


그 로직은 다음과 같습니다.


왜 에러가 났을까요?


[문제 발생 원인 파악 및 대책]

1. COUNT_MATCH가 보입니다. 정규식 매칭을 위해서 설정해 놓은 것 같습니다.


하드코딩이 보이는데, 제가 작성했던 예시 쿼리를 생각하면 이 형태에 부합하지 않는 것 같습니다. (CTE 때문)

즉, matcher.matches() 함수의 값은 false가 나올 것으로 보이네요.


2. countProjection은 따로 선언한 적이 없어, null이므로 if문을 타게 됩니다.


3. variable은 null이 나오고, 이에 따라 useVariable도 null을 갖습니다. complexCountValue는 COMPLEX_COUNT_LAST_VALUE를 갖게 됩니다.


4. COMPLEX_COUNT_LAST_VALUE는 $6인데, 이는 matcher를 그룹으로 나눴을 때 6번째 그룹의 값이 바인딩되는 것입니다. 찾아보면 이 값은 테이블의 별칭입니다.


5. matcher.matches()가 false라 하더라도, matcher는 원 쿼리(@Query의 value) 정보를 가지고 있을테고 가장 먼저 정규식에 걸리는 건 CTE일 것입니다.


6. 그렇다면 COMPLEX_COUNT_LAST_VALUE에 바인딩되는 값은 history의 별칭인 h가 들어가게 됩니다.


7. 아래 if문(replacement = "1"로 세팅해주는 부분)은 건너뛰게 되고, else문으로 들어가게 되는데, 그 안에서의 if문도 그대로 통과하게 됩니다.


8. 아래 코드를 따라가겠네요.

countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, replacement));


9. $5가 테이블 명, $6이 테이블 별칭, $7이 나머지 코드임을 고려했을 때, 동적 쿼리에는 정말 count(h)가 들어가겠군요.

* 실제 개발 시 동적 쿼리를 작성했을 때는 CTE가 2개, 본 쿼리가 1개인 구조였는데 맨 첫 CTE에만 count(테이블 별칭)이 적용되고, 나머지 쿼리는 그대로 유지되는 형태를 가졌었습니다.


한 마디로, @Query에 countQuery를 명시하지 않아 jpa에서 countQuery를 생성했고,

그 과정에서 쿼리 구조가 복잡(CTE)하다보니 정규식 조건에 맞지 않아 엉뚱한 동적 쿼리가 생성되었습니다.

원 쿼리에 CTE가 없었다면, countQuery를 명시하지 않았을 경우 그래도 잘 동작했을 것으로 예상됩니다.

(if문 처리가 spring-data-jpa 옛 버전에는 없어서 다중 컬럼을 select하면 문제가 있었을 것 같으나, 지금은 괜찮을 것으로 보입니다.)


그럼... countQuery 명시는 CTE 유무에 상관 없이 잘 해결책을 준다고 치고, countProjection은 어떨까요?


[countProjection]

위에서 createCountQueryFor 함수를 보면, 여전히 Matcher 자체는 COUNT_MATCH를 기반으로 하므로 CTE가 있다면 동일한 문제를 야기할 것으로 보입니다. 다만, CTE가 없는 경우에는 사용할 수 있을 것 같습니다.


결론은, 다음과 같습니다.

1. @Query(nativeQuery=true)를 사용하여 페이징 처리를 하고 싶다면, countQuery를 명시해주는 것이 제일 안전하다.

특히 CTE가 있는 등 복잡한 쿼리일 경우 엉뚱한 동적 쿼리가 생성될 수 있다.


2. 쿼리가 select~로 시작되는 형태라면, countQuery를 명시하지 않아도 동작할 것으로 보이며, countProjection을 사용할 수도 있다.



p.s. 13번 부분을 보면 jSqlParser를 사용하면 jSqlParserQueryEnhancer를 쓰는데, 이 곳에는 CTE를 고려하도록 코드가 짜인 것 같습니다. (withStatements)

최신 블로그