jOOQ DSL 커스터마이징

jOOQ를 프로젝트에 도입한 후, DSL 생성 방식을 커스터마이징하면 더욱 효율적으로 사용할 수 있다.

jOOQ DSL 커스터마이징 3가지 방법

DSL Generate Strategy

  • 생성되는 DSL 클래스의 이름을 커스터마이징한다. prefix나 suffix를 추가하여 jOOQ가 자동 생성한 클래스임을 명확히 표시할 수 있다

Generate 옵션

  • 어떤 코드 산출물(DAO, Record, POJO 등)을 생성할지 세밀하게 제어한다. DAO, Record, POJO 등의 생성 여부와 타입 변환 방식을 설정한다

jOOQ Runtime Configuration

  • 런타임에서 jOOQ의 동작 방식을 커스터마이징한다. SQL 렌더링, 로깅, 리스너 등을 설정한다

Generate Strategy – 클래스 이름 커스터마이징

QueryDSL의 Q prefix처럼, jOOQ에서도 j prefix를 사용하여 자동 생성된 클래스를 구분할 수 있다

서브모듈 생성

jOOQ-custom/build.gradle

plugins {
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jooq:jooq-codegen:${jooqVersion}"
    runtimeOnly "com.mysql:mysql-connector-j"
}

Custom Strategy 구현

package jooq.custom.generator;

import org.jooq.codegen.DefaultGeneratorStrategy;
import org.jooq.meta.Definition;

public class JPrefixGeneratorStrategy extends DefaultGeneratorStrategy {
    
    @Override
    public String getJavaClassName(Definition definition, Mode mode) {
        if (mode == Mode.DEFAULT) {
            return "J" + super.getJavaClassName(definition, mode);
        }
        return super.getJavaClassName(definition, mode);
    }
}
  • Mode.DEFAULT는 주로 테이블 기반 DSL 클래스에 적용된다. POJO, DAO, Record 등은 별도의 Mode를 가지므로, 필요시 각 Mode별로 다른 네이밍 규칙을 적용할 수 있다

메인 프로젝트 설정/build.gradle

dependencies {
    // ... 기존 dependencies
    
    jooqGenerator project(":jOOQ-custom")
    jooqGenerator "org.jooq:jooq:${jooqVersion}"
    jooqGenerator "org.jooq:jooq-meta:${jooqVersion}"
}

jooq {
    configurations {
        sakilaDb {
            generationTool {
                // ... jdbc 설정
                
                generator {
                    // ... 기존 설정
                    
                    strategy {
                        name = 'jooq.custom.generator.JPrefixGeneratorStrategy'
                    }
                }
            }
        }
    }
}
서브모듈이 필요한 이유

jOOQ 코드 생성 시점에는 커스텀 Strategy 클래스가 이미 컴파일되어 있어야 한다. 메인 프로젝트와 동일한 모듈에서 Strategy를 정의하면 순환 참조 문제가 발생할 수 있다

  • jOOQ DSL 생성을 위한 Strategy가 필요
  • Strategy를 컴파일하려면 jOOQ 의존성이 필요
  • jOOQ DSL이 생성되어야 컴파일 완료

이러한 순환 참조를 피하기 위해 Strategy는 별도 서브모듈로 분리하여 독립적으로 컴파일한다

생성 결과 – Gradle 태스크 실행
./gradlew generateSakilaDbJooq
  • 혹은 IntelliJ의 Gradle/Tasks/jooq의 파일을 실행해줍니다.
생성된 클래스
  • Actor → JActor
  • Film → JFilm
  • Category → JCategory

Generate 옵션 – 생성 항목 제어

주요 옵션 정리

옵션기본값설명
daosfalseDAO 클래스 생성 (기본 CRUD 메서드 포함)
recordsfalseActiveRecord 패턴 클래스 생성
pojosfalse순서 Java 객체 생성
fluentSettersfalseSetter가 자기 자신을 반환 (메서드 체이닝)
javaTimeTypesfalsejava.time.* 사용 (권장)
deprecatedtrue@Deprecated 멤버 생성 여부

필수 권장 설정

generate {
    daos = true
    records = true
    fluentSetters = true
    javaTimeTypes = true
    deprecated = false
}

fluentSetters 비교

true (권장)
Actor actor = new Actor()
    .setActorId(1)
    .setFirstName("PENELOPE")
    .setLastName("GUINESS");
false
Actor actor = new Actor();
actor.setActorId(1);          // void 반환
actor.setFirstName("PENELOPE");
actor.setLastName("GUINESS");

javaTimeTypes 비교

true (권장)
private LocalDateTime lastUpdate;
private LocalDate birthDate;
private LocalTime openTime;
false
private java.sql.Timestamp lastUpdate;
private java.sql.Date birthDate;
private java.sql.Time openTime;
  • java.time 패키지의 타입은 불변(immutable)이며 더 나은 API를 제공하므로 반드시 활성화하는 것을 권장한다

Unsigned 타입 처리

  • MySQL의 UNSIGNED 타입은 Java에 직접 대응되는 타입이 없다
database.unsignedTypes = true (기본값)
private UInteger actorId;  // jOOQ의 커스텀 타입
database.unsignedTypes = false (권장)
private Integer actorId;   // 표준 Java 타입
  • 대부분의 경우 unsignedTypes = false를 권장한다. jOOQ의 UInteger, ULong 등은 표준 Java 생태계와 호환성이 떨어질 수 있다. 필요시 데이터베이스 컬럼을 더 큰 타입(INT → BIGINT)으로 변경하는 것이 더 나은 선택이다

선택적 옵션

JPA Annotations
generate {
    jpaAnnotations = true
}
생성 결과
@Entity
@Table(name = "actor")
public class Actor {
    @Id
    @Column(name = "actor_id")
    private Integer actorId;
    
    @Column(name = "first_name", length = 45)
    private String firstName;
}
주의사항
  • 연관관계는 자동 생성되지 않으므로, JPA 엔티티의 뼈대로만 활용하고 필요한 연관관계는 직접 추가해야 한다
Validation Annotations
generate {
    validationAnnotations = true
}
생성 결과
public class Actor {
    @NotNull
    private Integer actorId;
    
    @Size(max = 45)
    private String firstName;
}
Spring DAO (비권장)
generate {
    springAnnotations = true
    springDao = true
}
  • 이 옵션은 DAO에 @Transactional(readOnly = true) 등의 어노테이션을 자동으로 추가하지만, 트랜잭션 경계를 서비스 계층에서 명확히 정의하기 어려워지는 문제가 있다. 일반 DAO를 생성하고 필요한 곳에서 수동으로 @Transactional을 사용하는 것을 권장한다

Runtime Configuration

  • 런타임에서 jOOQ의 동작을 제어한다

스키마 렌더링 비활성화

  • 기본적으로 jOOQ는 SQL에 스키마명을 포함한다
-- 기본 동작
SELECT `sakila`.`actor`.`actor_id` FROM `sakila`.`actor`

-- 원하는 결과
SELECT `actor`.`actor_id` FROM `actor`
  • 스키마명을 제거하려면 Configuration을 커스터마이징한다

JooqConfig.java

package com.example.jooqfirstlook.config;

import org.springframework.boot.autoconfigure.jooq.DefaultConfigurationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JooqConfig {
    
    @Bean
    public DefaultConfigurationCustomizer jooqDefaultConfigurationCustomizer() {
        return configuration -> configuration
                .settings()
                .withRenderSchema(false);  // 스키마명 제거
    }
}

유용한 Settings

@Bean
public DefaultConfigurationCustomizer jooqDefaultConfigurationCustomizer() {
    return configuration -> configuration
            .settings()
            .withRenderSchema(false)              // 스키마명 제거
            .withRenderFormatted(true)            // SQL 포맷팅
            .withExecuteLogging(true)             // 쿼리 로깅
            .withRenderNameCase(RenderNameCase.LOWER);  // 컬럼명 소문자
}

로깅 설정

application.yaml
logging:
  level:
    org.jooq.tools.LoggerListener: DEBUG

spring:
  jooq:
    sql-dialect: MySQL

  datasource:
    url: jdbc:mysql://localhost:3306/sakila
    username: root
    password: password

테스트

JooqCustomPracticeTest.java
@SpringBootTest
public class JooqCustomPracticeTest {

    @Autowired
    DSLContext dslContext;

    @Test
    void testJooqConfiguration() {
        dslContext.selectFrom(JActor.ACTOR)
                .limit(10)
                .fetch()
                .forEach(System.out::println);
    }
}
실행 결과
-- 스키마명이 제거된 깔끔한 SQL
SELECT `actor`.`actor_id`, 
       `actor`.`first_name`, 
       `actor`.`last_name`, 
       `actor`.`last_update` 
FROM `actor` 
LIMIT 10

권장 설정 요약

build.gradle (최종)
jooq {
    version = "${jooqVersion}"

    configurations {
        sakilaDb {
            generationTool {
                jdbc {
                    driver = 'com.mysql.cj.jdbc.Driver'
                    url = 'jdbc:mysql://localhost:3306'
                    user = "${dbUser}"
                    password = "${dbPasswd}"
                }

                generator {
                    name = 'org.jooq.codegen.DefaultGenerator'
                    
                    database {
                        name = 'org.jooq.meta.mysql.MySQLDatabase'
                        unsignedTypes = false  // 권장
                        
                        schemata {
                            schema {
                                inputSchema = 'sakila'
                            }
                        }
                    }

                    generate {
                        daos = true
                        records = true
                        fluentSetters = true
                        javaTimeTypes = true
                        deprecated = false
                    }

                    target {
                        directory = 'src/generated'
                    }

                    strategy {
                        name = 'jooq.custom.generator.JPrefixGeneratorStrategy'
                    }
                }
            }
        }
    }
}

jOOQ의 DSL 커스터마이징은 초기 설정 비용이 있지만, 한 번 구성하면 다음과 같은 이점을 얻는다

  • 타입 안정성: 컴파일 타임에 SQL 오류 검증
  • 가독성: 일관된 네이밍과 깔끔한 SQL
  • 생산성: DAO, POJO 자동 생성으로 보일러플레이트 제거
  • 유지보수성: 스키마 변경 시 컴파일 에러로 즉시 감지

특히 javaTimeTypes, fluentSetters, unsignedTypes = false 설정은 거의 모든 프로젝트에서 필수로 권장된다

출처 – 실전 jOOQ! Type Safe SQL with Java