본문 바로가기
Java & Kotlin/Spring

Kotlin SpringBoot 환경에서 jOOQ 설정

by devson 2024. 12. 3.

JPA가 주는 개발의 편리함은 많은 사람들에 의해 검증되었지만, dirty checking과 같이 under the hood에서 동작하는 기능들로 인해 배포 후 운영 시에 마주치는 문제도 생기게된다.

(관련해서 읽어보면 좋은 글: Hibernate 의 ‘불편한’ 편의 기능들)

 

가끔은 MyBatis를 쓰던 시절이 품은 조금 더 들지만 동작하는 방식은 명확해서 좋았던 것 같다.

하지만 MyBatis를 쓰면서 생기는 가장 큰 불편함은 문자열로써 SQL query를 다루기 때문에 쿼리 상의 오류를 런타임에서나 알 수 있다는 것이다.

반면 jOOQ의 경우 DB를 직접 스캔하여 query 용 class를 생성하는데 이 class를 사용하여 type safe 하게 SQL query를 사용할 수 있다.

(DB를 스캔 방식 외 방식을 지원하나 그 한계가 명확하다)

 

이번 포스팅에서는 Kotlin SpringBoot 환경에서 jOOQ를 설정하는 방법에 대해 알아보도록 하겠다.

 

(코드 예제는 여기에서 확인할 수 있다)

 


jOOQ의 코드 생성 방식

먼저 jOOQ의 코드 생성 방식에 대해 알아본 후 어떤 식으로 jOOQ를 설정을 할지에 대해 알아보자.

 

jOOQ는 DB를 직접 스캔하는 방식 외에도 jOOQ query 코드를 생성하는 다양한 방식을 지원한다.

하지만 직접적으로 DB를 스캔하는 방식에 비하면, 해당 DB의 기능을 활용하기 하기 힘들다고한다.

https://blog.jooq.org/using-testcontainers-to-generate-jooq-code/

 

특히 SQL 파일로부터 코드를 생성하는 DDLDatabase의 경우 내부적으로 H2 DB를 사용하여 코드를 생성한다.

https://www.jooq.org/doc/latest/manual/code-generation/codegen-ddl/

 

https://github.com/jOOQ/jOOQ/blob/main/jOOQ-meta-extensions/src/main/java/org/jooq/meta/extensions/ddl/DDLDatabase.java#L77-L79

 

 

그렇기 때문에 jOOQ에서는 직접 DB를 스캔하는 방식을 추천하며,

Testcontainers와 Flyway의 조합을 사용하여 어느 개발 환경에서나 재현 가능한 jOOQ 코드 생성을 진행하도록 해보겠다.

  1. Flyway를 사용하여 DB migration을 관리
  2. 운영 환경과 동일한 DB container를 실행 후 Flyway migration 진행
  3. 로컬에서 DB container 스캔하여 jOOQ query 생성

 

jOOQ 코드 생성 설정

nu.studer.jooq gradle plugin을 사용하여 jOOQ query 코드를 생성할 수 있는데,

이 plugin을 추가하면 jOOQ 코드 생성 task인 generateJooq task가 등록된다.

 

이 task가 실행되기 전에 Testcontainer를 사용하여 DB container를 실행시키고 Flyway migrate을 실행시키면 된다.

buildscript {
    dependencies {
        classpath("org.flywaydb:flyway-mysql:10.20.1") // for flyway task
        classpath("org.testcontainers:mysql:1.20.4")
        classpath("com.mysql:mysql-connector-j:9.1.0")
    }
}

jooq {
    version.set("3.19.15") // Match your jOOQ version
    edition.set(JooqEdition.OSS)

    configurations {
        create("main") {
            generateSchemaSourceOnCompilation.set(false) // Disable auto compilation
            jooqConfiguration.apply {
                logging = org.jooq.meta.jaxb.Logging.WARN
                jdbc.apply {
                    driver = "com.mysql.cj.jdbc.Driver"
//                    url = mySqlContainer.jdbcUrl // set in generateJooq task
//                    user = mySqlContainer.username // set in generateJooq task
//                    password = mySqlContainer.password // set in generateJooq task
                }
                generator.apply {
                    name = "org.jooq.codegen.KotlinGenerator" // Generate Kotlin code
                    database.apply {
                        name = "org.jooq.meta.mysql.MySQLDatabase"
//                        inputSchema = mySqlContainer.databaseName // set in generateJooq task
                        excludes = "flyway_schema_history"
                    }
                    generate.apply {
                        isDeprecated = false
                        isRecords = true
                        isImmutablePojos = true
                        isFluentSetters = true
                    }
                    target.apply {
                        packageName = "com.tistory.devs0n.jooq.models"
                        directory = "build/generated-src/jooq/main"
                    }
                }
            }
        }
    }
}
tasks.named("generateJooq") {
    lateinit var mySqlContainer: MySQLContainer<Nothing>

    doFirst {
        // run MySQL container
        mySqlContainer = MySQLContainer<Nothing>("mysql:8.0").apply {
            withDatabaseName("playground")
            withUsername("root")
            withPassword("root")
            withEnv("TZ", "Asia/Seoul")
            withCommand("mysqld", "--character-set-server=utf8mb4")
            withReuse(false)
            start()
        }

        // setup flyway plugin configuration
        flyway.url = mySqlContainer.jdbcUrl
        flyway.user = mySqlContainer.username
        flyway.password = mySqlContainer.password
        flyway.locations = arrayOf("filesystem:src/main/resources/db/migration")

        // run `flywayMigration` task
        val flywayMigrateTask = tasks.named("flywayMigrate").get()
        flywayMigrateTask.actions.forEach { it.execute(flywayMigrateTask) }

        // setup jooq plugin configuration
        jooq.configurations["main"].jooqConfiguration.apply {
            jdbc.url = mySqlContainer.jdbcUrl
            jdbc.user = mySqlContainer.username
            jdbc.password = mySqlContainer.password

            generator.database.inputSchema = mySqlContainer.databaseName
        }
    }

    doLast {
        // shutdown MySQL container after code generation
        mySqlContainer.stop()
    }
}

plugin, dependency 등이 포함된 전체 build.gradle.kts 코드

더보기
import nu.studer.gradle.jooq.JooqEdition
import org.testcontainers.containers.MySQLContainer

plugins {
    kotlin("jvm") version "1.9.24"
    kotlin("plugin.spring") version "1.9.24"
    id("org.springframework.boot") version "3.4.0"
    id("io.spring.dependency-management") version "1.1.6"
    id("org.flywaydb.flyway") version "11.0.0"
    id("nu.studer.jooq") version "9.0"
}

group = "com.tistory.devs0n"
version = "0.0.1-SNAPSHOT"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-reflect")

    runtimeOnly("com.mysql:mysql-connector-j")
    implementation("org.springframework.boot:spring-boot-starter-jooq")
    implementation("org.flywaydb:flyway-core")
    implementation("org.flywaydb:flyway-mysql")
    jooqGenerator("com.mysql:mysql-connector-j")

    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
    testImplementation("io.kotest:kotest-runner-junit5-jvm:5.9.1")
    testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
    testImplementation("org.springframework.boot:spring-boot-testcontainers")
    testImplementation("org.testcontainers:junit-jupiter")
    testImplementation("org.testcontainers:mysql")
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll("-Xjsr305=strict")
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

buildscript {
    dependencies {
        classpath("org.flywaydb:flyway-mysql:10.20.1") // for flyway task
        classpath("org.testcontainers:mysql:1.20.4")
        classpath("com.mysql:mysql-connector-j:9.1.0")
    }
}

jooq {
    version.set("3.19.15") // Match your jOOQ version
    edition.set(JooqEdition.OSS)

    configurations {
        create("main") {
            generateSchemaSourceOnCompilation.set(false) // Disable auto compilation
            jooqConfiguration.apply {
                logging = org.jooq.meta.jaxb.Logging.WARN
                jdbc.apply {
                    driver = "com.mysql.cj.jdbc.Driver"
//                    url = mySqlContainer.jdbcUrl // set in generateJooq task
//                    user = mySqlContainer.username // set in generateJooq task
//                    password = mySqlContainer.password // set in generateJooq task
                }
                generator.apply {
                    name = "org.jooq.codegen.KotlinGenerator" // Generate Kotlin code
                    database.apply {
                        name = "org.jooq.meta.mysql.MySQLDatabase"
//                        inputSchema = mySqlContainer.databaseName // set in generateJooq task
                        excludes = "flyway_schema_history"
                    }
                    generate.apply {
                        isDeprecated = false
                        isRecords = true
                        isImmutablePojos = true
                        isFluentSetters = true
                    }
                    target.apply {
                        packageName = "com.tistory.devs0n.jooq.models"
                        directory = "build/generated-src/jooq/main"
                    }
                }
            }
        }
    }
}
tasks.named("generateJooq") {
    lateinit var mySqlContainer: MySQLContainer<Nothing>

    doFirst {
        // run MySQL container
        mySqlContainer = MySQLContainer<Nothing>("mysql:8.0").apply {
            withDatabaseName("playground")
            withUsername("root")
            withPassword("root")
            withEnv("TZ", "Asia/Seoul")
            withCommand("mysqld", "--character-set-server=utf8mb4")
            withReuse(false)
            start()
        }

        // setup flyway plugin configuration
        flyway.url = mySqlContainer.jdbcUrl
        flyway.user = mySqlContainer.username
        flyway.password = mySqlContainer.password
//        flyway.locations = arrayOf("classpath:db/migration") // <- cannot find the path
        flyway.locations = arrayOf("filesystem:src/main/resources/db/migration")

        // run `flywayMigration` task
        val flywayMigrateTask = tasks.named("flywayMigrate").get()
        flywayMigrateTask.actions.forEach { it.execute(flywayMigrateTask) }

        // setup jooq plugin configuration
        jooq.configurations["main"].jooqConfiguration.apply {
            jdbc.url = mySqlContainer.jdbcUrl
            jdbc.user = mySqlContainer.username
            jdbc.password = mySqlContainer.password

            generator.database.inputSchema = mySqlContainer.databaseName
        }
    }

    doLast {
        // shutdown MySQL container after code generation
        mySqlContainer.stop()
    }
}

 

jOOQ 코드 생성

generateJooq task를 실행시키면 앞서 설정에 맞춰 build/generated-src/jooq/main 하위 디렉토리에 코드가 생성되어있음을 확인할 수 있다.

 ./gradlew generateJooq

 

댓글