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 옵션 – 생성 항목 제어
주요 옵션 정리
| 옵션 | 기본값 | 설명 |
| daos | false | DAO 클래스 생성 (기본 CRUD 메서드 포함) |
| records | false | ActiveRecord 패턴 클래스 생성 |
| pojos | false | 순서 Java 객체 생성 |
| fluentSetters | false | Setter가 자기 자신을 반환 (메서드 체이닝) |
| javaTimeTypes | false | java.time.* 사용 (권장) |
| deprecated | true | @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 설정은 거의 모든 프로젝트에서 필수로 권장된다