Skip to content

Conversation

@seokjun01
Copy link
Collaborator

@seokjun01 seokjun01 commented Nov 11, 2025

📌 PR 내용

기존 PostRepository의 메소드는 다수의 쿼리를 @query 어노테이션 기반 JPQL로 작성하고 있었습니다.

  • 쿼리 동작 시간 확인( 주의사항 탭에서 설명)

예시

스크린샷 2025-11-11 오후 10 36 03

이러한 코드작성은 다음과 같은 한계가 존재합니다.

  1. 컴파일 시점에 타입 검증 불가 ( 런타임에 발생)
  2. 가독성 저하 (너무 김)
  3. 동적 조건 작성이 어려움 (where절 등..)

이에 따라 QueryDSL 기반으로 전환하여 ,
타입 안전성, 가독성, 동적 쿼리 확장성을 개선해보았습니다.

흐름은 다음과 같습니다.

  1. Q클래스 생성 (컴파일 시점에 엔티티 기반 Q클래스 자동 생성)
  2. JPAQueryFactory를 통해 DSL 코드 작성
  3. JPQL 빌드 후 Hibernate에서 쿼리로 변환
  4. DB실행

변경사항

PostRepository 내 @query 직접 작성 -> PostRepositoryCustom + PostRepositoryImpl 분리
수정한 2개의 메소드 테스트 코드 작성 (로그 확인)

변경효과

*JPQL 문자열 없이 메서드 체인 방식으로 limit, offset, orderBy 등을 설정할 수 있습니다.
*Slice 기반의 페이징 로직(hasNext, offset, limit)을 직접 제어할 수 있습니다.
*Q클래스를 통해 엔티티 필드 변경 시점에서 IDE가 즉시 오류를 감지합니다.
*BooleanBuilder, where절 재사용으로 중복 쿼리 로직을 제거할 수 있습니다.


🔍 PR 세부사항

솔직히 처음 쿼리 최적화의 목적은 JPQL의 너무 길고 중복되는 로직을 줄이고자 였습니다.
허나 QueryDSL도 만만치 않게 복잡함을 알게되었고 우선 2개의 메소드만 반영해 PR로 작성하였습니다.
위 방식에 대한 다른 분들의 의견이 궁금합니다. (적용하는게 맞을지!)
이 방식이 수용되면 차차 확장적으로 코드를 리팩토링해 나가겠습니다!


📸 관련 스크린샷

스크린샷 2025-11-11 오후 10 11 40

로그가 길어서 다 담지는 못하였지만,
기존에 메소드 (@query 기반 JPQL)는 아래와 같은 로그
select p from Post p where p.category in (leets.weeth.domain.board.domain.entity.enums.Category.StudyLog, ...)
-> → 이런 식으로 엔티티 경로 + enum 풀 경로가 다 찍힘

QueryDSL 로그 예시
Hibernate: select p1_0.id,p1_0.cardinal_number,p1_0.category,p1_0.comment_count,p1_0.content,p1_0.created_at,p1_0.modified_at,p1_0.part,p1_0.parts,p1_0.study_name,p1_0.title,p1_0.user_id,p1_0.week from post p1_0 where p1_0.category in ('StudyLog','Article') and (lower(p1_0.title) like lower(concat('%',?,'%')) escape '' or lower(p1_0.content) like lower(concat('%',?,'%')) escape '') order by p1_0.id desc limit ?


📝 주의사항


실제 @query기반 JPQL과 , QueryDSL을 적용한 코드 쿼리 시간을 측정해보았습니다.
측정 환경은 local에서 스웨거로 4-5개의 글을 만든 뒤 , 적용한 메소드를 사용하는 api를 호출하여
Spring Log4jdb를 이용해 측정하였습니다.

  • 변화가 적용된 메소드(findRecentPart, searchPart)
  • 위 메소드는 각각 게시글 목록 조회(무한스크롤) , 파트 게시글 검색(무한스크롤) api에 사용되며, Spring Log4jdb 사용시 아래처럼 쿼리 실행 시간이 보입니다.
스크린샷 2025-11-12 오후 6 36 41

결론적으로는 첫 번째 api, 게시글 목록 조회에 있어서 JPQL : 8ms, QueryDSL : 3ms
두 번째 api 파트 게시글 검색에 있어서는 JPQL : 3ms , QueryDSL : 1ms 이 소요되어
QueryDSL이 조금 더 빠름을 알 수 있습니다.

이유를 찾아본 결과 QueryDSL은 미리 생성된 QClass 기반 직렬화로 오버헤드가 없지만, JPQL은 런타임에 문자열을 파싱하기 때문에 시간이 조금 더 소요된다고 합니다.

사실 정확한 테스트라고 보기 어렵습니다

  1. 실제 환경이 아닌점
  2. DSL을 적용한 메소드가 2개밖에 없는점
  3. 조회하는 글의 수가 현저히 적었던 점

팀원분들은 쿼리속도 측정 방식에 대하여 어떻게 생각하시는지도 여쭤보고 싶습니다. (뭘로 , 환경제어는 어떻게)
Jmeter의 사용도 고려했으나, JPQL → QueryDSL 교체가 쿼리 자체를 얼마나 줄였나/빨라졌나를 우선 검증에는 Spring Log4jdbc가 적합하다고 판단했습니다!

✅ 체크리스트

  • 리뷰어 설정
  • Assignee 설정
  • Label 설정
  • 제목 양식 맞췄나요? (ex. [WTH-01] PR 템플릿 수정)
  • 변경 사항에 대한 테스트

Summary by CodeRabbit

  • 새로운 기능

    • 포스트 제목·내용 기반 키워드 검색 기능 추가
    • 최신 포스트 조회(부분 조회) 기능 추가
    • 파라미터 기반 필터링(파트, 카테고리, 회차 등) 및 페이지네이션으로 효율적 데이터 로딩 지원
  • 테스트

    • 검색 및 최신 조회 동작을 검증하는 저장소 단위 테스트 추가

@seokjun01 seokjun01 requested review from hyxklee and jj0526 November 11, 2025 13:37
@seokjun01 seokjun01 self-assigned this Nov 11, 2025
@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

Walkthrough

QueryDSL 의존성 및 어노테이션 프로세서가 빌드에 추가되고, JPAQueryFactory 빈이 등록되었으며 PostRepository에 커스텀 인터페이스와 구현체(PostRepositoryImpl)가 추가되어 다양한 QueryDSL 기반 조회·필터링과 Slice 페이징이 구현되고 관련 테스트가 추가되었습니다.

Changes

코호트 / 파일(s) 변경 요약
빌드 설정
\build.gradle``
QueryDSL 및 annotation-processor용 Jakarta API 의존성 추가 (com.querydsl:querydsl-jpa:5.0.0:jakarta, com.querydsl:querydsl-apt:5.0.0:jakarta, jakarta.persistence:jakarta.persistence-api:3.1.0, jakarta.annotation:jakarta.annotation-api:2.1.1)
레포지토리 인터페이스 변경
\src/main/java/leets/weeth/domain/board/domain/repository/PostRepository.java``
PostRepositoryPostRepositoryCustom을 추가로 상속하도록 변경
커스텀 인터페이스 추가
\src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryCustom.java``
새로운 인터페이스 추가: 다수의 Slice 기반 조회·검색·필터 메서드 및 distinct studyName 조회 시그니처 선언
커스텀 구현 추가
\src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java``
PostRepositoryCustom 구현체 추가: QueryDSL(JPAQueryFactory, QPost) 기반의 다양한 조건 빌더, FIND_IN_SET 유사 파트 필터링, offset/limit(pageSize+1) 기반 Slice 페이징 로직 구현
QueryDSL 설정
\src/main/java/leets/weeth/global/config/QuerydslConfig.java``
JPAQueryFactory 빈을 제공하는 Spring 설정 클래스 추가
테스트 추가
\src/test/java/leets/weeth/domain/board/domain/repository/PostRepositoryTest.java``
DataJpaTest 기반의 레포지토리 테스트 추가: findRecentPart_success, searchPart_success 검증

Sequence Diagram

sequenceDiagram
    autonumber
    participant Client
    participant PostRepository
    participant PostRepositoryImpl
    participant JPAQueryFactory
    participant Database

    Client->>PostRepository: findRecentPart(pageable) / searchPart(kw, pageable)
    PostRepository->>PostRepositoryImpl: 위임
    rect rgb(240,248,255)
    PostRepositoryImpl->>JPAQueryFactory: 동적 조건 구성 (part/category/cardinal/studyName/week/kw), 정렬, offset, limit(pageSize+1)
    end
    JPAQueryFactory->>Database: SQL 실행
    Database-->>JPAQueryFactory: 결과 목록 (<= pageSize+1)
    JPAQueryFactory-->>PostRepositoryImpl: List<Post>
    PostRepositoryImpl->>PostRepositoryImpl: hasNext 결정 및 초과 항목 제거 -> SliceImpl 생성
    PostRepositoryImpl-->>PostRepository: SliceImpl<Post>
    PostRepository-->>Client: Slice<Post> 반환
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 분

  • 주의 대상:
    • PostRepositoryImpl의 복잡한 QueryDSL 조건 구성(Part / Category / cardinal / studyName / week / kw)과 FIND_IN_SET 유사 로직 검증
    • Slice 페이징(offset/limit+1) 및 hasNext 계산의 경계 케이스
    • QuerydslConfig의 JPAQueryFactory 빈 설정과 EntityManager 주입
    • PostRepository 인터페이스와 PostRepositoryImpl의 시그니처 일치 여부 및 Spring Data 자동 검색 규약

Poem

🐰 깡충, 새 쿼리 심었네,
Entity와 JPAQueryFactory에 흙 묻히고,
조건을 꿰매어 슬라이스 쥐어주며,
키워드 따라 글 찾고 춤추네,
당근으로 축하, 테스트 초록 빛! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 JPQL에서 QueryDSL로의 리팩토링이라는 주요 변경 사항을 명확하게 요약하고 있으며, 실제 변경 내용과 일치합니다.
Description check ✅ Passed PR 설명은 템플릿의 주요 섹션(PR 내용, PR 세부사항, 스크린샷, 주의사항, 체크리스트)을 모두 작성하였으며, 변경 사항의 배경, 이유, 효과를 상세히 설명하고 있습니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch WTH-65-weeth-쿼리-최적화-작업

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/leets/weeth/domain/board/domain/repository/PostRepository.java (2)

17-25: 중복된 메서드 정의 제거 필요

findRecentPart 메서드가 PostRepositoryCustom에도 선언되어 있고, PostRepositoryImpl에서 QueryDSL로 구현되었습니다. 이 @query 기반 메서드는 제거해야 합니다. 동일한 시그니처를 가진 메서드가 중복 정의되면 런타임에 어느 구현이 사용될지 불명확하며 예상치 못한 동작이 발생할 수 있습니다.

다음 diff를 적용하여 중복된 메서드를 제거하세요:

-	@Query("""
-        SELECT p FROM Post p
-        WHERE p.category IN (
-            leets.weeth.domain.board.domain.entity.enums.Category.StudyLog,
-            leets.weeth.domain.board.domain.entity.enums.Category.Article
-        )
-        ORDER BY p.id DESC
-    """)
-	Slice<Post> findRecentPart(Pageable pageable);
-

34-46: 중복된 메서드 정의 제거 필요

searchPart 메서드도 PostRepositoryCustom에 선언되어 있고 PostRepositoryImpl에서 QueryDSL로 구현되었습니다. 이 @query 기반 메서드를 제거하여 중복을 해소해야 합니다.

다음 diff를 적용하여 중복된 메서드를 제거하세요:

-	@Query("""
-        SELECT p FROM Post p
-        WHERE p.category IN (
-            leets.weeth.domain.board.domain.entity.enums.Category.StudyLog,
-            leets.weeth.domain.board.domain.entity.enums.Category.Article
-        )
-          AND (
-                LOWER(p.title)   LIKE LOWER(CONCAT('%', :kw, '%'))
-             OR LOWER(p.content) LIKE LOWER(CONCAT('%', :kw, '%'))
-          )
-        ORDER BY p.id DESC
-    """)
-	Slice<Post> searchPart(@Param("kw") String kw, Pageable pageable);
-
🧹 Nitpick comments (5)
src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryCustom.java (1)

12-12: 파라미터 포매팅 개선

파라미터 사이에 공백을 추가하여 Java 컨벤션을 따르는 것이 좋습니다.

-	Slice<Post> searchPart(String kw,Pageable pageable);
+	Slice<Post> searchPart(String kw, Pageable pageable);
src/test/java/leets/weeth/domain/board/domain/repository/PostRepositoryTest.java (2)

57-70: 기본 시나리오 테스트 성공적 구현

최신순 조회 로직이 올바르게 검증되고 있습니다. 다만 Slice의 hasNext 동작도 함께 검증하면 페이징 로직의 정확성을 더 확실히 보장할 수 있습니다.

hasNext 검증을 추가하는 것을 권장합니다:

 		//then
 		assertThat(result).isNotNull();
 		assertThat(result.getContent()).hasSize(2);
 		assertThat(result.getContent().get(0).getTitle()).isEqualTo("테스트 제목2");
+		assertThat(result.hasNext()).isFalse();

72-84: 키워드 검색 테스트 적절

검색 기능이 올바르게 동작하는지 검증하고 있습니다. 마찬가지로 hasNext 검증을 추가하면 더 완전한 테스트가 됩니다.

 		//then
 		assertThat(result.getContent()).hasSize(1);
 		assertThat(result.getContent().get(0).getTitle()).isEqualTo("dsl 테스트 제목");
+		assertThat(result.hasNext()).isFalse();
src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java (2)

22-22: QPost 인스턴스 필드 불필요

QPost.post는 static 상수이므로 인스턴스 필드로 저장할 필요가 없습니다. 메서드 내에서 직접 QPost.post를 사용하거나, static import를 활용하는 것이 더 관례적입니다.

다음 중 하나의 방식을 권장합니다:

옵션 1: Static import 사용

+import static leets.weeth.domain.board.domain.entity.QPost.post;
+
 @Repository
 @RequiredArgsConstructor
 public class PostRepositoryImpl implements PostRepositoryCustom {
 
 	private final JPAQueryFactory queryFactory;
-	private final QPost post = QPost.post;

옵션 2: 직접 참조

 @Repository
 @RequiredArgsConstructor
 public class PostRepositoryImpl implements PostRepositoryCustom {
 
 	private final JPAQueryFactory queryFactory;
-	private final QPost post = QPost.post;
 
 	@Override
 	public Slice<Post> findRecentPart(Pageable pageable) {
 		List<Post> result = queryFactory
-			.selectFrom(post)
+			.selectFrom(QPost.post)

34-40: 페이징 로직 중복 제거 고려

두 메서드에서 hasNext 계산 및 결과 트리밍 로직이 동일하게 반복됩니다. 향후 추가 메서드 마이그레이션 시 헬퍼 메서드로 추출하면 유지보수성이 향상될 것입니다.

현재 2개 메서드만 있어 큰 문제는 아니지만, 추가 메서드를 마이그레이션할 때 다음과 같이 공통 로직을 추출할 수 있습니다:

private <T> Slice<T> toSlice(List<T> result, Pageable pageable) {
    boolean hasNext = result.size() > pageable.getPageSize();
    if (hasNext) {
        result.remove(pageable.getPageSize());
    }
    return new SliceImpl<>(result, pageable, hasNext);
}

그러면 각 메서드는:

List<Post> result = queryFactory
    .selectFrom(post)
    // ... query logic ...
    .fetch();

return toSlice(result, pageable);

Also applies to: 56-62

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28951b4 and 90695aa.

📒 Files selected for processing (6)
  • build.gradle (1 hunks)
  • src/main/java/leets/weeth/domain/board/domain/repository/PostRepository.java (1 hunks)
  • src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryCustom.java (1 hunks)
  • src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java (1 hunks)
  • src/main/java/leets/weeth/global/config/QuerydslConfig.java (1 hunks)
  • src/test/java/leets/weeth/domain/board/domain/repository/PostRepositoryTest.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/test/java/leets/weeth/domain/board/domain/repository/PostRepositoryTest.java (1)
src/test/java/leets/weeth/config/TestContainersConfig.java (1)
  • TestConfiguration (9-20)
🔇 Additional comments (8)
src/main/java/leets/weeth/domain/board/domain/repository/PostRepository.java (1)

15-15: LGTM - PostRepositoryCustom 확장

PostRepositoryCustom 인터페이스를 확장하여 커스텀 쿼리 메서드를 사용할 수 있도록 구조가 개선되었습니다.

src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryCustom.java (1)

8-13: LGTM - 커스텀 리포지토리 인터페이스 정의

QueryDSL 기반 커스텀 쿼리를 위한 인터페이스가 적절하게 정의되었습니다. 표준 Spring Data 커스텀 리포지토리 패턴을 따르고 있습니다.

src/main/java/leets/weeth/global/config/QuerydslConfig.java (1)

10-17: LGTM - QueryDSL 설정

JPAQueryFactory 빈 설정이 표준 패턴을 따르고 있으며 올바르게 구현되었습니다. EntityManager를 통해 QueryDSL 쿼리 실행을 위한 팩토리가 적절히 생성됩니다.

src/test/java/leets/weeth/domain/board/domain/repository/PostRepositoryTest.java (2)

24-27: LGTM - 테스트 설정

QuerydslConfig와 TestContainers를 적절히 import하여 QueryDSL 기반 테스트 환경이 올바르게 구성되었습니다.


32-54: 테스트 데이터 준비

두 개의 Post 엔티티가 적절히 생성되어 테스트 시나리오를 지원합니다.

src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java (3)

17-22: LGTM - 기본 구조

JPAQueryFactory를 활용한 QueryDSL 리포지토리 구현이 적절합니다.


25-41: 최신 게시글 조회 로직 검증됨

QueryDSL을 사용한 카테고리 필터링과 Slice 기반 페이징 로직이 올바르게 구현되었습니다. limit(pageSize + 1) 패턴으로 hasNext를 효율적으로 계산하고 있습니다.


44-63: 키워드 검색 로직 검증됨

제목 또는 내용에서 대소문자 구분 없이 키워드를 검색하는 로직이 적절하게 구현되었습니다. containsIgnoreCase가 SQL의 LOWER() 함수를 생성하여 기존 JPQL과 동일한 동작을 보장합니다.

Comment on lines +44 to +48
// QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api:2.1.1'
Copy link

@coderabbitai coderabbitai bot Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

QueryDSL 버전 확인 필요

QueryDSL 5.0.0을 사용 중이지만, 이 버전이 최신 안정 버전인지 확인이 필요합니다. 보안 패치와 버그 수정이 포함된 최신 버전을 사용하는 것이 좋습니다.


🌐 Web query:

What is the latest stable version of QueryDSL for Jakarta EE?

💡 Result:

The latest stable QueryDSL release for Jakarta EE is 5.1.0 (checked Nov 11, 2025). [1][2]

Sources:

  • QueryDSL releases/notes. [1]
  • QueryDSL 5.1.0 Javadoc / Maven artifact listing. [2]

QueryDSL을 5.1.0으로 업그레이드하세요

현재 QueryDSL 5.0.0을 사용 중이지만, 최신 안정 버전은 5.1.0입니다. 보안 패치와 버그 수정을 위해 build.gradle의 44-45번 줄을 다음과 같이 업데이트하세요:

implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
🤖 Prompt for AI Agents
In build.gradle around lines 44 to 48, the QueryDSL dependency and its
annotation processor are pinned to 5.0.0; update the QueryDSL coordinates on
lines 44-45 to use version 5.1.0 by changing both the implementation and the
querydsl-apt annotationProcessor entries to use ':5.1.0:jakarta' while leaving
the other annotationProcessor entries unchanged.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa/5.1.0

확인해보니 쿼리 DSL 전 버전에 취약점이 있네요. 지원 중단돼서 그런가...?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🌐 Web query:

QueryDSL 5.0.0 security vulnerabilities CVE

💡 Result:

Short answer: Yes — Querydsl has a reported HQL/SQL injection vulnerability identified as CVE-2024-49203 that affects Querydsl 5.x (including 5.0.0/<=5.1.0 in multiple advisories). The issue concerns injection via JPAQuery.orderBy when untrusted input is used. Recommend not passing untrusted input into orderBy and upgrading to a patched/non-vulnerable release or forked build where the fix is available. [1][2][3][4]

Sources

  • CSIRT.SK advisory describing CVE-2024-49203 and affected Querydsl versions. [1]
  • GitHub Advisory / OpenFeign querydsl security advisory for the same issue (GHSA-6q3q-6v5j-h6vg). [2]
  • Snyk vulnerability page for CVE-2024-49203. [3]
  • Maven / mvnrepository listing for querydsl-apt 5.0.0 that flags CVE-2024-49203. [4]

If you want, I can: (a) check the current latest Querydsl release and which exact versions include the fix, or (b) show concrete mitigations/workarounds and sample code to safely handle ordering parameters.


🌐 Web query:

QueryDSL maintenance status 2025 CVE-2024-49203 fix

💡 Result:

Short answer: Querydsl is actively maintained in 2025 and the CVE-2024-49203 HQL/SQL‑injection issue was fixed — upgrade to the patched releases (Querydsl 5.6.1, 6.10.1; OpenFeign Querydsl 7.0 also lists fixes). Apply the update or validate/whitelist any user-controlled orderBy input if you cannot upgrade immediately. [1][2][3]

Sources:

  • Querydsl release notes showing fixes for CVE-2024-49203 (releases including 5.6.1 and 6.10.1). [1]
  • GitHub security advisory for the issue (GHSA-6q3q-6v5j-h6vg / CVE-2024-49203) with patched versions listed. [2]
  • CSIRT.SK advisory and disclosure details with mitigation guidance. [3]

@hyxklee 좋은 지적 감사합니다! 취약점 확인 결과를 공유드립니다:

취약점 정보 (CVE-2024-49203)

  • QueryDSL 5.0.0과 5.1.0 모두 HQL/SQL 인젝션 취약점이 존재합니다
  • JPAQuery.orderBy()에 검증되지 않은 사용자 입력이 전달될 때 발생합니다

유지보수 상태
QueryDSL은 여전히 활발히 유지보수되고 있습니다. 지원 중단이 아니며, 해당 취약점은 이미 패치되었습니다.

권장사항
build.gradle의 QueryDSL 버전을 5.6.1 이상으로 업그레이드하세요:

// QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.6.1:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.6.1:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api:2.1.1'

이 PR에서 QueryDSL로 전환하는 시점에 보안 패치가 적용된 버전을 사용하는 것이 중요합니다. 현재 코드를 확인한 결과 orderBy(post.id.desc())처럼 정적 표현식을 사용하고 있어 직접적인 위험은 낮지만, 예방 차원에서 패치된 버전 사용을 강력히 권장합니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java (1)

28-67: 중복 코드를 제거하여 유지보수성을 개선하세요.

findRecentPartsearchPart 메서드 간에 상당한 코드 중복이 있습니다. 페이징 로직(lines 38-44, 60-66)과 쿼리 구조가 동일하며, where 절만 차이가 있습니다. 공통 로직을 private 헬퍼 메서드로 추출하거나 BooleanBuilder를 활용하면 유지보수성이 향상됩니다.

리팩토링 예시:

@Override
public Slice<Post> findRecentPart(Pageable pageable) {
    BooleanBuilder builder = new BooleanBuilder();
    builder.and(post.category.in(Category.StudyLog, Category.Article));
    return executeSliceQuery(builder, pageable);
}

@Override
public Slice<Post> searchPart(String kw, Pageable pageable) {
    BooleanBuilder builder = new BooleanBuilder();
    builder.and(post.category.in(Category.StudyLog, Category.Article));
    
    if (kw != null && !kw.trim().isEmpty()) {
        builder.and(post.title.containsIgnoreCase(kw)
            .or(post.content.containsIgnoreCase(kw)));
    }
    
    return executeSliceQuery(builder, pageable);
}

private Slice<Post> executeSliceQuery(BooleanBuilder whereClause, Pageable pageable) {
    List<Post> result = queryFactory
        .selectFrom(post)
        .where(whereClause)
        .orderBy(post.id.desc())
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize() + 1)
        .fetch();

    boolean hasNext = result.size() > pageable.getPageSize();
    
    if (hasNext) {
        result.remove(pageable.getPageSize());
    }
    
    return new SliceImpl<>(result, pageable, hasNext);
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd2642b and a6647fe.

📒 Files selected for processing (1)
  • src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (2)
src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java (2)

28-45: findRecentPart 메서드 구현이 적절합니다.

Slice 기반 페이징 로직이 올바르게 구현되었습니다. pageSize + 1을 조회하여 hasNext를 판단하고, 초과 항목을 제거하는 패턴이 정확합니다.


48-67: kw 파라미터의 null/empty 검증을 추가하세요.

kw 파라미터가 null이거나 빈 문자열일 경우에 대한 처리가 없습니다. 이는 예상치 못한 쿼리 결과나 예외를 발생시킬 수 있습니다. 메서드 시작 부분에서 검증 로직을 추가하거나, BooleanBuilder를 사용하여 조건부로 where 절을 추가하는 것을 권장합니다.

다음과 같이 검증 로직을 추가할 수 있습니다:

@Override
public Slice<Post> searchPart(String kw, Pageable pageable) {
    if (kw == null || kw.trim().isEmpty()) {
        return new SliceImpl<>(List.of(), pageable, false);
        // 또는 예외를 던지거나, findRecentPart를 호출하는 등의 처리
    }
    
    List<Post> result = queryFactory
        .selectFrom(post)
        // ... rest of the query

또는 BooleanBuilder를 사용한 동적 쿼리:

BooleanBuilder builder = new BooleanBuilder();
builder.and(post.category.in(Category.StudyLog, Category.Article));

if (kw != null && !kw.trim().isEmpty()) {
    builder.and(post.title.containsIgnoreCase(kw)
        .or(post.content.containsIgnoreCase(kw)));
}

List<Post> result = queryFactory
    .selectFrom(post)
    .where(builder)
    .orderBy(post.id.desc())
    // ...

import lombok.RequiredArgsConstructor;


public class PostRepositoryImpl implements PostRepositoryCustom {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

@repository 어노테이션이 누락되었습니다.

클래스에 @Repository 어노테이션이 없습니다. Spring의 컴포넌트 스캔 및 예외 변환을 위해 필요하며, JPA repository 구현체의 표준 패턴입니다.

다음 diff를 적용하세요:

+@Repository
 public class PostRepositoryImpl implements PostRepositoryCustom {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class PostRepositoryImpl implements PostRepositoryCustom {
@Repository
public class PostRepositoryImpl implements PostRepositoryCustom {
🤖 Prompt for AI Agents
In
src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java
around line 19, the class PostRepositoryImpl is missing the Spring @Repository
annotation; add the @Repository annotation to the class declaration and import
org.springframework.stereotype.Repository so Spring can component-scan and apply
exception translation for this JPA repository implementation.

Copy link
Collaborator

@hyxklee hyxklee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트까지 잘 짜진 것 같습니당
테스트의 중요성이라고 할 수 있겠어요

저도 사용해보진 않아서, 잘 모르지만 기존 repository와 쿼리 dsl로 작성된 클래스를 어댑터를 끼워 맞추듯 사용이 가능하다면 더 좋을 것 같다는 생각이 드네요!

Comment on lines +44 to +48
// QueryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api:3.1.0'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api:2.1.1'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://mvnrepository.com/artifact/com.querydsl/querydsl-jpa/5.1.0

확인해보니 쿼리 DSL 전 버전에 취약점이 있네요. 지원 중단돼서 그런가...?

import lombok.RequiredArgsConstructor;


public class PostRepositoryImpl implements PostRepositoryCustom {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 방식의 구현법은 공식문서를 기반으로 구현해주신 걸까요?? 단순 궁금

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://sjh9708.tistory.com/174#google_vignette
위 블로그들 포함하여 여러 구현 블로그 따라 했습니당

Copy link
Collaborator

@jj0526 jj0526 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다!

Comment on lines +52 to +53
.and(post.title.containsIgnoreCase(kw)
.or(post.content.containsIgnoreCase(kw))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kw가 null일 경우 NPE이 발생하지는 않나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

테스트 해보았는데 아무것도 입력하지 않는 경우 호출이 불가합니다! 따라서 npe발생이 안될거라 예상됩니당

Controller 확인 결과 , RequestParam이 붙여져있어서 괜찮을 것 같습니다!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryCustom.java (2)

8-8: 사용하지 않는 import를 제거하세요.

@Param 어노테이션은 인터페이스 메소드 선언에서 사용되지 않습니다. 이는 @Query 어노테이션이나 구현 클래스에서만 필요합니다.

다음 diff를 적용하여 사용하지 않는 import를 제거하세요:

-import org.springframework.data.repository.query.Param;

18-18: 메소드 시그니처의 포맷팅을 수정하세요.

파라미터 리스트에서 콤마 뒤에 공백이 누락되었습니다. 다른 메소드 선언들과의 일관성을 위해 공백을 추가하세요.

다음 diff를 적용하세요:

-	Slice<Post> searchPart(String kw,Pageable pageable);
+	Slice<Post> searchPart(String kw, Pageable pageable);
src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java (2)

3-3: 사용하지 않는 import들을 제거하세요.

다음 import들이 코드에서 사용되지 않습니다:

  • java.beans.Expression (line 3)
  • org.springframework.data.repository.query.Param (line 10)
  • org.springframework.security.core.parameters.P (line 11)
  • lombok.RequiredArgsConstructor (line 24) - 수동 생성자를 사용하고 있으므로 불필요

다음 diff를 적용하여 사용하지 않는 import들을 제거하세요:

-import java.beans.Expression;
 import java.util.Collection;
 import java.util.List;
 
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Slice;
 import org.springframework.data.domain.SliceImpl;
-import org.springframework.data.repository.query.Param;
-import org.springframework.security.core.parameters.P;
 import org.springframework.stereotype.Repository;
 import leets.weeth.domain.board.domain.entity.Post;
 import leets.weeth.domain.board.domain.entity.QPost;
 import leets.weeth.domain.board.domain.entity.enums.Category;
 import leets.weeth.domain.board.domain.entity.enums.Part;
-import lombok.RequiredArgsConstructor;

Also applies to: 10-11, 24-24


142-146: Part.ALL 조건 로직을 단순화하세요.

Line 143의 조건 post.part.eq(Part.ALL).or(post.part.ne(Part.ALL))은 항상 참인 항진식(tautology)입니다. 이는 모든 파트를 매칭하려는 의도로 보이지만, 불필요하게 복잡합니다.

다음과 같이 로직을 명확하게 개선하세요:

 	BooleanBuilder builder = new BooleanBuilder();
 
-	if (part == Part.ALL) {
-		builder.and(post.part.eq(Part.ALL).or(post.part.ne(Part.ALL)));
-	}else {
+	if (part != Part.ALL) {
 		builder.and(post.part.eq(part).or(post.part.eq(Part.ALL)));
 	}
+	// part == Part.ALL인 경우 조건을 추가하지 않아 모든 파트가 조회됨

이렇게 하면 part == Part.ALL일 때는 파트 필터링 없이 모든 게시글을 조회하고, 특정 파트가 지정된 경우에만 해당 파트 또는 ALL 파트의 게시글을 조회합니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6647fe and 33f8145.

📒 Files selected for processing (2)
  • src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryCustom.java (1 hunks)
  • src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java (1 hunks)
🔇 Additional comments (4)
src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java (4)

27-34: 생성자 구현이 적절합니다.

EntityManager를 주입받아 JPAQueryFactory를 생성하는 방식은 @DataJpaTest 환경에서 추가 설정 없이 동작하므로 올바른 접근입니다. 이전 리뷰 논의에서 확인된 대로 이 방식이 CI 빌드 문제를 해결합니다.


36-119: Slice 페이징 구현이 올바릅니다.

네 개의 메소드(findRecentPart, searchPart, findRecentEducation, searchEducation) 모두 표준 Slice 페이징 패턴을 올바르게 구현했습니다. limit(pageSize + 1) 방식으로 다음 페이지 존재 여부를 효율적으로 판단하고 있습니다.


121-135: 조건부 쿼리 로직이 명확합니다.

Part.ALL일 때 모든 파트의 스터디명을 조회하고, 특정 파트일 때는 해당 파트로 필터링하는 로직이 삼항 연산자로 간결하게 구현되었습니다.


181-212: BooleanBuilder를 활용한 동적 쿼리 구현이 적절합니다.

세 개의 메소드 모두 BooleanBuilder를 사용하여 동적 조건을 깔끔하게 구성했으며, Slice 페이징 로직도 일관되게 구현되었습니다.

Also applies to: 214-245, 247-278

Comment on lines +194 to +198
Expressions.numberTemplate(Integer.class,
"FIND_IN_SET({0}, {1})", partName, post.parts).gt(0)
.or(Expressions.numberTemplate(Integer.class,
"FIND_IN_SET('ALL', {0})", post.parts).gt(0))
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

데이터베이스 종속적인 SQL 함수 사용에 대한 검토가 필요합니다.

FIND_IN_SET 함수는 MySQL/MariaDB 전용 함수로, 데이터베이스 이식성(portability) 문제를 야기합니다. 또한 동일한 로직이 세 개의 메소드에서 반복되고 있습니다.

영향:

  • PostgreSQL, Oracle 등 다른 데이터베이스로 마이그레이션 시 코드 수정 필요
  • FIND_IN_SET은 인덱스를 사용하지 못해 성능 저하 가능
  • 중복 코드로 인한 유지보수 부담

권장 사항:

  1. 단기 해결책: 중복된 FIND_IN_SET 로직을 private helper 메소드로 추출하여 재사용하세요:
private BooleanExpression createPartFilter(String partName) {
    if ("ALL".equals(partName)) {
        return null;
    }
    
    BooleanExpression containsPart = 
        Expressions.numberTemplate(Integer.class, 
            "FIND_IN_SET({0}, {1})", partName, post.parts).gt(0);
    
    BooleanExpression containsAll = 
        Expressions.numberTemplate(Integer.class, 
            "FIND_IN_SET('ALL', {0})", post.parts).gt(0);
    
    return containsPart.or(containsAll);
}
  1. 장기 해결책: parts 필드를 별도의 연관 테이블로 정규화하는 것을 고려하세요. 이는 데이터베이스 독립성과 쿼리 성능을 개선합니다.

현재 구현을 유지할지, 데이터 모델 변경을 고려할지 팀 내에서 논의가 필요합니다.

Also applies to: 224-228, 256-261


🏁 Script executed:

#!/bin/bash
# Verify FIND_IN_SET usage at specified lines
echo "=== Checking lines 194-198, 224-228, 256-261 in PostRepositoryImpl.java ==="
fd PostRepositoryImpl.java --type f --exec cat {} \; | head -300 | tail -120

Length of output: 3497


🏁 Script executed:

#!/bin/bash
# Get full method context around the specified lines
echo "=== Getting full method implementations ==="
fd PostRepositoryImpl.java --type f --exec sed -n '170,210p;220,235p;245,270p' {} \;

Length of output: 2425


🏁 Script executed:

#!/bin/bash
# Search for all FIND_IN_SET occurrences in the repository
echo "=== Searching for all FIND_IN_SET usage ==="
rg -n "FIND_IN_SET" -C 5 --type java

Length of output: 9274


🏁 Script executed:

#!/bin/bash
# Check for database configuration files
echo "=== Looking for database/persistence configuration ==="
fd -e yml -e yaml -e properties -e xml | xargs rg -l "database|persistence|jpa|hibernate|datasource" 2>/dev/null | head -10

Length of output: 214


🏁 Script executed:

#!/bin/bash
# Check if there's a query abstraction or utility layer
echo "=== Searching for query helper or expression builder classes ==="
rg -n "class.*Expression|class.*Query|interface.*Builder" --type java | grep -i "helper\|util\|builder" | head -20

Length of output: 202


🏁 Script executed:

#!/bin/bash
# Check database configuration in properties files
echo "=== Checking database configuration ==="
fd application-prod.yml application-dev.yml application-local.yml --type f --exec cat {} \;

Length of output: 214


🏁 Script executed:

#!/bin/bash
# Search for any comments or documentation about database choice
echo "=== Searching for database-related comments or docs ==="
rg -i "mysql|postgres|oracle|database.*driver|h2" --type yml --type yaml --type properties -B 2 -A 2

Length of output: 83


🏁 Script executed:

#!/bin/bash
# Check if there are any GitHub issues or documentation about database portability
echo "=== Looking for README or documentation files ==="
fd -e md -e txt | xargs grep -l -i "database\|mysql\|postgres" 2>/dev/null | head -5

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Check the exact state: compare all three FIND_IN_SET implementations
echo "=== Comparing all three FIND_IN_SET implementations ==="
echo "=== Method 1: findByCategoryAndOptionalCardinalWithPart (lines 193-199) ==="
rg -A 7 "findByCategoryAndOptionalCardinalWithPart" src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java | head -20
echo ""
echo "=== Method 2: findByCategoryAndCardinalNumberWithPart (lines 222-231) ==="
rg -A 10 "findByCategoryAndCardinalNumberWithPart" src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java | grep -A 10 "if.*ALL"
echo ""
echo "=== Method 3: findByCategoryAndCardinalInWithPart (lines 254-264) ==="
rg -A 10 "findByCategoryAndCardinalInWithPart" src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java | grep -A 10 "if.*ALL"

Length of output: 930


🏁 Script executed:

#!/bin/bash
# Check README for database information
echo "=== Checking README.md for database information ==="
cat README.md | head -100

Length of output: 2247


🏁 Script executed:

#!/bin/bash
# Verify database config files with correct fd syntax
echo "=== Checking database configuration files ==="
fd application.*\.y -e yml

Length of output: 240


🏁 Script executed:

#!/bin/bash
# Extract database info from config
echo "=== Database configuration details ==="
find . -name "application*.y*ml" 2>/dev/null | xargs cat 2>/dev/null | grep -A 5 -B 5 -i "database\|datasource\|jpa\|hibernate" | head -50

Length of output: 1030


메서드 1만 리팩토링이 필요합니다. 코드 패턴 일관성을 맞추세요.

세 메서드 중 첫 번째 메서드만 인라인 표현식을 사용하고 있고, 두 번째와 세 번째 메서드는 이미 변수로 추출되어 있습니다. MySQL은 의도적으로 설정되어 있으므로 데이터베이스 이식성은 현재 주요 우려사항이 아닙니다.

조치 사항:
findByCategoryAndOptionalCardinalWithPart (194-198줄)에서 두 번째, 세 번째 메서드처럼 변수로 추출하여 패턴을 일관되게 유지하세요:

if (!"ALL".equals(partName)) {
    BooleanExpression containsPart =
        Expressions.numberTemplate(Integer.class,
            "FIND_IN_SET({0}, {1})", partName, post.parts).gt(0);
    
    BooleanExpression containsAll =
        Expressions.numberTemplate(Integer.class,
            "FIND_IN_SET('ALL', {0})", post.parts).gt(0);
    
    builder.and(containsPart.or(containsAll));
}
🤖 Prompt for AI Agents
In
src/main/java/leets/weeth/domain/board/domain/repository/PostRepositoryImpl.java
around lines 194-198, the first method uses inline Expressions.numberTemplate
calls while the other two methods extract those expressions into variables;
refactor this block to extract the FIND_IN_SET expressions into two
BooleanExpression variables (e.g., containsPart and containsAll) and then use
builder.and(containsPart.or(containsAll)) inside the existing if
(!"ALL".equals(partName)) check so the code pattern matches the other methods.

Copy link
Collaborator

@jj0526 jj0526 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고 많으셨어요~!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants