Jooq 사용법 정리

3 min read
최근 프로젝트에서 JOOQ를 도입할 일이 생겨,
DDL(SQL) 기반으로 JOOQ를 활용하는 방법을 간단하게 정리해보았습니다.
JPA나 QueryDSL에 익숙한 분들도 금방 적응할 수 있는 사용성 덕분에 생각보다 빠르게 손에 익었습니다.
실습 환경
Spring Boot: 3.4
Java: 17
JOOQ: 3.19.21
Gradle Plugin: nu.studer.jooq 9.0
1. 의존성 설정
JOOQ는 단순 DSL 라이브러리가 아니라,
DDL 또는 DB 메타 정보를 기반으로 코드를 미리 생성한 후 사용해야 합니다.
implementation("org.springframework.boot:spring-boot-starter-jooq")
jooqGenerator("org.jooq:jooq-meta-extensions:$jooqVersion")
implementation("org.jooq:jooq-codegen:$jooqVersion")
2. DDL 기반 코드 생성 구조
DDL 정의 (init.sql)
CREATE TABLE team
(
team_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE users
(
user_id SERIAL PRIMARY KEY,
team_id INTEGER REFERENCES team (team_id) ON DELETE CASCADE,
username VARCHAR(100) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
- 파일 위치: src/main/resources/db/init.sql
JOOQ는 해당 DDL을 기반으로 테이블 정의와 관련된 타입 세이프 DSL 코드를 생성합니다.
build.gradle.kts 설정
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.4.4"
id("io.spring.dependency-management") version "1.1.7"
id("nu.studer.jooq") version "9.0" // jooq 플러그인
}
// 부가설정들
jooq {
version.set(jooqVersion)
edition.set(nu.studer.gradle.jooq.JooqEdition.OSS) // the default (can be omitted)
configurations {
create("main") {
generateSchemaSourceOnCompilation.set(true)
jooqConfiguration.apply {
logging = org.jooq.meta.jaxb.Logging.INFO
generator.apply {
name = "org.jooq.codegen.DefaultGenerator"
database.apply {
name = "org.jooq.meta.extensions.ddl.DDLDatabase"
properties.addAll(
listOf(
Property().let { it ->
it.key = "scripts"
it.value = "src/main/resources/db/init.sql"
it
},
Property().let { it ->
it.key = "defaultNameCase"
it.value = "lower"
it
},
Property().let { it ->
it.key = "unqualifiedSchema"
it.value = "none"
it
},
Property().let { it ->
it.key = "sort"
it.value = "semantic"
it
}
)
)
}
generate.apply {
isDeprecated = false
isRecords = true
isImmutablePojos = true
isFluentSetters = true
}
target.apply {
packageName = "nu.studer.sample"
directory = "build/generated-src/jooq/main" // default (can be omitted)
}
strategy.name = "org.jooq.codegen.DefaultGeneratorStrategy"
}
}
}
}
}
3. 코드 사용 예제
DTO 클래스 정의
class User(
val userId: Int,
val username: String,
val teamId: Int,
val createdAt: LocalDateTime,
)
class UserDetailResponse(
val userId: Int,
val teamId: Int,
val username: String,
val email: String,
val teamName: String,
val createdAt: LocalDateTime,
)
단순 조회 예제
@Repository
class UserJooqRepository(
private val dsl: DSLContext,
) {
fun findByUserId(userId: Int): User? = dsl
.selectFrom(USERS)
.where(USERS.USER_ID.eq(userId))
.fetchAny {
User(
userId = it.userId,
username = it.username,
teamId = it.teamId,
createdAt = it.createdAt
)
}
}
Join을 활용한 복합 조회
@Repository
class UserJooqRepository(
private val dsl: DSLContext,
) {
fun findDetailsByUserId(userId: Int): UserDetailResponse? {
return dsl.select(
USERS.USER_ID,
USERS.USERNAME,
USERS.EMAIL,
USERS.CREATED_AT,
TEAM.TEAM_ID,
TEAM.NAME
)
.from(USERS)
.join(TEAM).on(USERS.TEAM_ID.eq(TEAM.TEAM_ID))
.where(USERS.USER_ID.eq(userId))
.fetchAny {
UserDetailResponse(
userId = it.getValue(0, Int::class.java),
username = it.getValue(1, String::class.java),
email = it.getValue(2, String::class.java),
createdAt = it.getValue(3, LocalDateTime::class.java),
teamId = it.getValue(4, Int::class.java),
teamName = it.getValue(5, String::class.java),
)
}
}
}
또는 아래처럼 Kotlin의 inline reified + 확장함수를 써서 더 간결하게 만들 수도 있습니다:
inline infix fun <reified T> Record.getValue(i: Int): T = this.get(i, T::class.java)
위 내용에 대한 실제 코드를 보고 실행하시고 싶다면 아래의 링크에 코드들을 공유해놓았으니 보시고 사용하시면 됩니다.
JOOQ Simple Example Github Code
느낀 점
JOOQ는 JPA와는 다른 접근 방식으로, 쿼리 자체의 유연성과 명시성이 필요할 때 매우 유용합니다.
특히 쿼리 최적화가 중요한 도메인, 복잡한 Join, DB 종속적인 기능을 활용할 때 장점이 큽니다.
또한, DDL 기반으로 코드를 생성하면 CI/CD나 Flyway 기반 스키마 관리와도 쉽게 연계할 수 있어 운영 환경에서 유리합니다.
참고 자료
0
Subscribe to my newsletter
Read articles from 조현준 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
