Skip to main content

Command Palette

Search for a command to run...

Jooq 사용법 정리

JOOQ를 활용한 기본 예제 모음집

Updated
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 기반 스키마 관리와도 쉽게 연계할 수 있어 운영 환경에서 유리합니다.


참고 자료

9 views

More from this blog

JVM 객체 할당의 비밀 — TLAB, Bump-the-Pointer, 그리고 할당이 거의 공짜인 이유

Java에서 new를 호출하면 무슨 일이 벌어질까요? "힙에 메모리를 잡는다"는 한 문장 뒤에는 스레드마다 자기만의 분양 구역을 나눠 갖는 정교한 설계가 숨어 있어요. 이 글은 HotSpot JVM이 객체 할당을 어떻게 "거의 공짜"로 만드는지 그 내부를 따라가 보려는 글이에요. JVM 메모리 동작 원리에 관심 있는 분께 권해요. 자바를 쓰다 보면 객체를

May 15, 202614 min read

Java Zero-Copy — FileChannel.transferTo, sendfile, 그리고 Kafka가 디스크를 네트워크로 흘려보내는 방법

"파일을 읽어서 소켓으로 보낸다." 한 줄짜리 요구사항이에요. 그런데 이 한 줄 뒤에서 데이터는 메모리를 네 번이나 복사하고, CPU는 커널과 유저 공간을 네 번이나 들락거려요. Kafka처럼 초당 수십만 건을 흘려보내야 하는 시스템에서 이 비용은 그냥 넘길 수가 없어요. 이 글은 그 복사를 한 겹씩 벗겨내는 zero-copy의 동작 원리를 따라가요. 전통

May 15, 202617 min read

Git merge 내부 동작 — 3-way merge, merge base, 그리고 recursive에서 ort로

git merge를 매일 쓰지만, 그 한 줄이 안에서 무슨 일을 하는지 들여다본 적은 드물어요. 이 글은 merge가 두 갈래의 변경을 어떻게 합치는지, merge base가 왜 필요한지, 그리고 Git이 기본 전략을 recursive에서 ort로 갈아치운 이유를 따라가요. Git을 쓰는 백엔드 개발자를 대상으로 해요. 브랜치 두 개를 합치는 일은 겉보기

May 15, 202612 min read

Java NIO ByteBuffer 내부 구조 — Direct vs Heap, Cleaner, 그리고 off-heap 메모리가 GC를 우회하는 방법

Netty가 빠른 이유, Kafka 클라이언트가 직렬화에 신경 쓰는 이유, MappedByteBuffer로 수 GB짜리 파일을 다루는 이유. 그 한가운데에는 ByteBuffer가 있어요. 이번 글에서는 ByteBuffer의 두 얼굴 — heap과 direct — 가 어떻게 다른지, off-heap 메모리는 어떻게 잡고 어떻게 풀리는지, JVM과 운영체제 사

May 15, 202612 min read

Java Flight Recorder 내부 구조 — Thread-Local Buffer부터 Disk Repository까지

JFR을 켜면 1% 미만 오버헤드로 JVM 내부가 그대로 기록돼요. 어떻게 이렇게 가벼울 수 있는지, 그리고 그 데이터가 어떤 경로를 거쳐 디스크에 쌓이는지 한 번 따라가 봐요. 이 글은 JFR을 "그냥 잘 쓰는 도구"에서 "내부 동작을 아는 도구"로 끌어올리고 싶은 분을 위한 글이에요. 운영 중인 서버에서 갑자기 응답 시간이 튀어요. 메트릭 그래프는 분

May 15, 202614 min read

끄적끄적 테크 블로그

162 posts

물류 회사에 다니고 있는 개발자 블로그입니다. 개발을 너무 좋아해서 정신없이 작업하다가 중간에 끄적거리며 내용들을 몇개 적어봅니다 ㅎㅎ