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 코드를 생성하는 다양한 방식을 지원한다.
- JPADatabase: Code generation from entities
- XMLDatabase: Code generation from XML files
- DDLDatabase: Code generation from SQL files
- LiquibaseDatabase: Code generation from Liquibase XML, YAML, JSON files
하지만 직접적으로 DB를 스캔하는 방식에 비하면, 해당 DB의 기능을 활용하기 하기 힘들다고한다.
특히 SQL 파일로부터 코드를 생성하는 DDLDatabase의 경우 내부적으로 H2 DB를 사용하여 코드를 생성한다.
그렇기 때문에 jOOQ에서는 직접 DB를 스캔하는 방식을 추천하며,
Testcontainers와 Flyway의 조합을 사용하여 어느 개발 환경에서나 재현 가능한 jOOQ 코드 생성을 진행하도록 해보겠다.
- Flyway를 사용하여 DB migration을 관리
- 운영 환경과 동일한 DB container를 실행 후 Flyway migration 진행
- 로컬에서 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
'Java & Kotlin > Spring' 카테고리의 다른 글
[Spring] Swagger UI 대신 Scalar API Reference를 사용하여 API 문서 사용하기 (1) | 2024.10.29 |
---|---|
[Spring Data JPA] @OneToMany Entity 연관 관계에 대하여 (7) | 2023.01.20 |
[Spring Data JPA] entity update 후 JpaRepository.save 호출에 관하여 (6) | 2023.01.09 |
[springdoc-openapi] 고정 header 설정하기 (0) | 2022.12.31 |
Request Rate Limiting with Spring Cloud Gateway - 부록. custom Filter 만들기 (0) | 2022.02.08 |
댓글