| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 | 30 | 31 |
- TypeScript
- 0.25px border
- 클론코딩
- font-size
- entity
- angular
- 0.75px border
- 전역변수
- npm
- 10px
- 타입스크립트
- 당근마켓
- 데이터베이스 #try #이중
- 1px border
- Strict
- 으
- &연산
- jwt
- 문서번호
- ES5
- 서버리스 #
- TS
- Websocket
- es6
- 컴포넌튼
- github
- 0.5px border
- ZOOM
- literal
- Props
- Today
- Total
복잡한뇌구조마냥
[Spring] JOOQ ( + QueryDSL vs JOOQ ) 본문
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
'BE > Spring' 카테고리의 다른 글
| [Spring] Spring Batch + Scheduler 정리 (0) | 2025.11.27 |
|---|---|
| [JPA] OSIV ( Open Session In View ) (0) | 2025.11.25 |
| [Spring] Spring AI ( OpenAI + Rag + MariaDB Vector ) (0) | 2025.11.18 |
| [Spring] Spring Security + OAuth2 로그인 (0) | 2025.10.15 |
| [Spring] JPA + QueryDSL 정리 (0) | 2025.10.02 |