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

조현준
조현준