복잡한뇌구조마냥

[Spring] JOOQ ( + QueryDSL vs JOOQ ) 본문

BE/Spring

[Spring] JOOQ ( + QueryDSL vs JOOQ )

지금해냥 2025. 11. 21. 21:31

1. JOOQ란 무엇인가?

JOOQ = 타입 안전한 SQL 빌더 + DB 스키마 기반 코드 생성기

ORM처럼 객체 중심이 아니라,
“SQL 중심(SQL-first)” 으로 설계된 DSL이다.

즉,

  • SQL 문법을 Java DSL로 그대로 표현
  • MySQL, PostgreSQL 등 Dialect에 맞춰 SQL을 자동 생성
  • DB 스키마 기반 코드 생성 → 컴파일 타임 안정성
  • 복잡한 SQL(CTE, Window Function, Inline Subquery)도 손쉽게 작성 가능

2.🔥 JPA의 한계 — 왜 QueryDSL과 JOOQ가 필요해지는가?

JPA는 객체 중심 개발을 돕는 훌륭한 ORM이지만,
조회(특히 고급 SQL) 관점에서는 구조적 한계를 가진다.

2.1 JPQL의 구조적 한계

❌ 1) FROM 절 서브쿼리 불가능

예:

SELECT *
FROM (SELECT ...) t

이런 SQL은 JPQL로 표현 불가.
Hibernate 6에서 일부 CTE/서브쿼리 기능이 들어오긴 했지만 여전히 불완전함.

❌ 2) 다양한 SQL 함수를 표현하기 어려움

  • MySQL 윈도우 함수
  • JSON 함수
  • ARRAY, UNNEST
  • GIS
    JPA는 이런 걸 원천적으로 막아놓음.

"필요하면 SQL을 써야 하는데… ORM은 그걸 못 쓰게 막아둔다"
라는 구조적 충돌이 있다.

 

❌ 3) 결국 Native SQL을 쓰는 경우 문자열 기반

 
em.createNativeQuery("SELECT * FROM post WHERE ...")
  • 문자열 → 오타 잡기 불가능
  • IDE 지원 없음
  • 유지보수성 급락

❌ 4) 성능 비용이 있음 (JPA가 나쁘다는 뜻이 아니라 특성)

  • 영속성 컨텍스트 관리
  • Dirty checking
  • Lazy Loading
  • flush 타이밍

→ 조회 성능이 순수 SQL보다 좋을 수가 없음.


3. 🔥 JPA의 한계를 보완하기 위한 두 가지 요구

JPA를 쓰다 보면 자연스럽게 다음 두 가지 욕구가 생긴다.

2.1 강타입 쿼리 빌더(Type-safe Query Builder) 필요

문자열 기반 SQL 너무 불편해서 →
→ QueryDSL, JOOQ 같은 DSL이 필요해짐.

2.2 JPQL vs SQL을 자유롭게 선택하고 싶음

  • SQL-first가 필요할 때는 SQL을 써야 한다.
  • ORM이 막아놓은 고급 SQL을 써야 한다.

👉 JOOQ는 두 가지 모두 충족.
👉 QueryDSL은 1번만 충족, 2번은 불가능.


4. JOOQ vs QueryDSL

항목 QueryDSL JOOQ
철학 ORM-first SQL-first
기반 엔티티 메타 모델(QClass) DB 스키마(Codegen)
실행 JPQL → SQL SQL 바로 실행
성능 JPA가 관여 순수 SQL 성능
SQL 자유도 낮음 매우 높음
N+1 발생 가능 없음
fetch join 지원 개념 자체 없음
DTO 매핑 편함 직접 해야 함 (또는 Record 매핑)
학습 난이도 JPA 사용자에게 쉬움 SQL 잘해야 함
용도 도메인 중심 조회 분석/검색/통계/고급조회

✔ QueryDSL = JPA를 위한 DSL (ORM-first)

  • 엔티티 기반
  • 도메인 모델 중심
  • 영속성 컨텍스트 고려
  • fetch join을 통한 객체 그래프 조회 중심

→ JPA의 한계를 “조금 더 편하게” 해주는 도구

✔ JOOQ = DB를 위한 DSL (SQL-first)

  • DB 스키마 기반 코드 생성
  • SQL 기능 100% 활용
  • 순수 SQL 성능
  • SQL 문법 그대로 Java에 표현

→ 엔티티가 아닌 “데이터 자체”가 중심

📌 Transaction / Entity 관리 관점 비교

항목 QueryDSL JOOQ
영속성 컨텍스트 활용함 없음
Dirty Checking 있음 없음
엔티티 그래프(fetch join) 핵심 기능 없음
DTO 중심 조회 불편(Projection 필요) 기본
Change Detection ORM 레이어 SQL 레이어

변경 작업이 많은 Command는 QueryDSL
조회/통계/검색은 JOOQ

 

✔ 프로젝트에서 성능 비교 ( JOOQ를 도입하게 된 계기...)

동일 조건 게시글 작성자 리뷰 통계 테스트 결과 (50건 워밍업 후 1000건에 대해 테스트)

동일 조건 게시글 리뷰 통계 테스트 결과 (50건 워밍업 후 1000건에 대해 테스트)


5. ⚙️ JOOQ Gradle 설정

JOOQ는 “코드 생성이 핵심”이다.
DB 스키마 → Java Table 클래스 자동 생성.

* QueryDSL은 빌드시에 자동으로 DSL을 생성하지만, JOOQ는 스키마 기반으로 별도 생성해줘야함

 
plugins {
    id("org.springframework.boot")
    id("nu.studer.jooq") version "9.0"
}

dependencies {
    implementation("org.jooq:jooq")
    jooqGenerator("org.mariadb.jdbc:mariadb-java-client")
}

jooq {
    version.set("3.19.3")
    configurations {
        create("main") {
            generateSchemaSourceOnCompilation.set(true)

            jooqConfiguration.apply {
                jdbc.apply {
                    url = "jdbc:mariadb://localhost:3306/app"
                    user = "root"
                    password = "pass"
                }

                generator.apply {
                    name = "org.jooq.codegen.DefaultGenerator"

                    database.apply {
                        name = "org.jooq.meta.mariadb.MariaDBDatabase"
                        inputSchema = "app"
                    }

                    generate.apply {
                        isPojos = true
                        isFluentSetters = true
                    }

                    target.apply {
                        packageName = "com.example.jooq"
                        directory = "$buildDir/generated/jooq"
                    }
                }
            }
        }
    }
}

6. JOOQ 기본 사용 예시

- QueryDSL은 엔티티 기반 코드인데 반해 JOOQ는 DB 테이블 기반임

1) SELECT

 
dsl.select(POST.ID, POST.TITLE)
   .from(POST)
   .where(POST.CATEGORY.eq("TRAVEL"))
   .orderBy(POST.CREATED_AT.desc())
   .fetch();

2) JOIN + 복잡 조건

 
dsl.select(POST.ID, POST.TITLE, MEMBER.NICKNAME)
   .from(POST)
   .join(MEMBER).on(POST.AUTHOR_ID.eq(MEMBER.ID))
   .where(
       POST.CATEGORY.eq("TRAVEL"),
       MEMBER.STATUS.eq("ACTIVE")
   )
   .fetch();

3) 윈도우 함수 예시 (QueryDSL/JPA에서는 매우 어려움)

 
dsl.select(
        POST.ID,
        POST.TITLE,
        rank().over(orderBy(POST.VIEW_COUNT.desc()))
)
.from(POST)
.fetch();

 


7. DSL 생성 원리 차이 (QueryDSL vs JOOQ)

✔ QueryDSL DSL 생성 원리

  • 엔티티 클래스(예: Member.java)를 분석
  • annotation processor가 QMember 같은 “메타 모델”을 생성
  • 즉, 엔티티 기반 DSL

➡ 그래서 JPA가 못 쓰는 SQL은 QueryDSL도 못 쓴다.

✔ JOOQ DSL 생성 원리

  • DB 스키마에서 직접 코드 생성
  • 테이블/컬럼 정보를 기반으로
    • Tables.MEMBER
    • MEMBER.NAME
      같은 클래스를 만든다.

➡ DB → 코드 생성 → DSL

→ 즉, SQL 세계의 정보를 그대로 Java에 반영
→ DB Dialect 수준까지 코드로 제어 가능


결론 — QueryDSL과 JOOQ는 경쟁제가 아니다. “목적이 다르다.”

QueryDSL과 JOOQ는 경쟁제가 아니라 목적이 다른 도구이고,
둘 다 써야 한다면 CQRS 관점에서 역할을 분리해야 한다.

그리고 정확하게 말하면:

✔ JOOQ가 상위호환

  • SQL 완성도
  • DSL 표현력
  • Dialect 대응
  • 성능
  • 쿼리 튜닝 자유도

✔ QueryDSL은 JPA-friendly한 보조 도구

  • 엔티티 중심
  • 트랜잭션/도메인 규칙 필요할 때 유용
  • fetch join 같이 ORM이 필요한 쿼리만 적합

📌 마무리

JPA + QueryDSL은 엔티티 중심 개발을 돕는 훌륭한 조합이지만,
점점 더 복잡한 SQL과 고성능 조회가 필요해지는 순간 JOOQ의 가치가 드러난다.
QueryDSL은 JPA의 한계를 보완해주고,
JOOQ는 SQL의 자유를 되찾아준다.
 

 

 

 

 

참고자료:

https://nowsun.tistory.com/194

 

[Spring] JPA + QueryDSL 정리

Spring Data JPA만으로도 웬만한 CRUD는 해결되지만,현실적인 서비스에서는 점점 이런 생각이 든다.“조건이 자꾸 늘어나는데… 이거 진짜 findByAAndBOrCAndD... 로 버티는 게 맞나?”JPA + QueryDSL 조합으로

nowsun.tistory.com

 

LIST