jOOQ Code Generation JPA

코드만 봐도 어떤 쿼리가 실행될지 예측하기 쉽다는 것이 jOOQ의 큰 장점이다. 여기서는 jOOQ의 핵심이라 할 수 있는 코드 제너레이션(Code Generation)방식과 무엇을 기준으로 DSL을 만들어야 하는지 알아본다

Code Generation

jOOQ의 코드 제너레이션은 다양한 데이터 소스 중 하나를 참고하여 jOOQ DSL을 생성하는 작업이다. 이를 통해 대부분의 에러를 컴파일 타임에 잡을 수 있지만, 그만큼 초기 설정의 난이도가 높아질 수 밖에 없다

Source of Truth의 중요성

코드 제너레이션에서 가장 중요한 것은 무엇이 스키마의 원본(Source of Truth)인가를 결정하는 것이다. 가능한 옵션은 아래와 같다

  • 실제 데이터베이스 (RDBMS)
  • JPA Entity 클래스
  • DDL 파일 (스키마 덤프)
  • XML 파일
  • Liquibase Changelog 등

실제 데이터베이스 기반

장점
  • 별다른 설정 없이 DSL 사용 가능
  • DB 스키마 변경에 직접적으로 반영
  • DSL 수정이 쉬움
단점
  • DB 스키마 변경이 다른 개발자에게 즉시 영향을 미침
  • 공용 개발 DB 환경에서 협업 시 문제 발생
    • 개발 중인 테이블이나 컬럼을 삭제하면 다른 개발자들은 컴파일 에러 발생
  • 운영 DB를 기준으로 사용 불가 (보안 및 네트워크 분리)
적합한 경우
  • 협업하는 개발자가 매우 적은 경우
  • 로컬에서 DB를 띄어서 개발하는 경우

JPA Entity 기반 DSL 생성

여러 명이 협업할 때 발생하는 문제를 해결하기 위해, JPA Entity를 통해 DSL을 생성하는 방식을 고려할 수 있다. 이는 QueryDSL – JPA를 사용하는 방식과 유사하다

JPA Database 방식의 동작 원리

  • jOOQ 코드 제너레이터가 다른 모듈의 JPA Entity를 스캔
  • Entity 코드 기반이므로 DB 스키마 변경에 직접적인 영향이 적음
  • 커밋 전까지는 다른 개발자에게 영향을 미치지 않음

치명적인 단점 – 멀티모듈 강제

jOOQ는 컴파일되지 않은 코드로부터 DSL을 생성할 수 없다. 즉, 같은 모듈에 있는 JPA Entity로는 DSL을 만들 수 없고, 반드시 별도 모듈로 분리해야 한다

이는 jOOQ 공식 문서에도 명시되어 있으며, 다음과 같은 모듈 구조가 필요하다

프로젝트 루트
├── entity (모듈) - JPA Entity만 포함
│   └── src/main/java
│       └── com.example.entity
│           ├── Actor.java
│           ├── Film.java
│           └── ...
├── application (모듈) - 실제 애플리케이션 코드
│   └── src
│       ├── main/java
│       └── generated (jOOQ DSL 생성 위치)
└── jooq-custom (모듈) - jOOQ 커스터마이징

이미 프로젝트에서 JPA를 사용하고 있다면, 모든 Entity를 별도 모듈로 분리해야 하므로 대규모 리팩토링이 불가피하다

QueryDSL은 되는데 jOOQ는 안 되는 이유

QueryDSL-JPA는 Annotation Processor(APT)를 사용하여 같은 모듈에서도 Q클래스를 생성할 수 있다. jOOQ는 이 방식은 지원하지 않는다. 그 이유에 대한 jOOQ 창시자의 답변은 아래와 같다

  • 모듈에는 단일 컴파일 단계만 존재해야 한다
  • 모듈로 분리하는 방식이 가장 깔끔하고, 나머지는 hacky한 방식이다
  • APT를 사용하면 한 번의 컴파일 요청이 실제로는 여러 번의 컴파일을 수행하게 된다
    • Entity를 컴파일
    • jOOQ DSL을 생성
    • 전체를 다시 컴파일
  • APT 방식은 예측하기 어렵고 디버깅하기 어려운 문제를 발생시킨다
  • 다른 라이브러리들도 APT 때문에 많은 이슈를 겪고 있다

Java Annotation Processing Phase

[소스코드] 
   ↓
[1. Parse and Enter] - AST & 심벌 테이블 생성
   ↓
[2. Annotation Processing] - 모든 Annotation Processor 실행
   ↓ (새 파일 생성 시)
   ↓ ←────────────┐ (컴파일 재시작 - Round)
   ↓              │
[3. Analyse and Generate] - 바이트코드 생성

Annotation Processing 단계에서 새로운 파일이 생성되면 다시 처음부터 컴파일이 시작된다. 이 반복적인 컴파일 과정(Round)이 바로 jOOQ 창시자가 “hacky”하다고 표현한 부분이다

추가적인 제약사항

JPA Entity 기반 방식을 사용하더라도 JPA Entity에서 지원하지 않는 DB 객체는 DSL로 생성이 불가한 제약이 있다

  • Stored Procedure
  • Trigger
  • View
  • Index (Entity에 명시되지 않은 경우)
  • 외래키 (Entity 관계로 표현되지 않은 경우)

JPA Entity 방식

Entity 모듈 생성

entity 모듈을 생성하고 build.gradle 설정

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.3'
    id 'io.spring.dependency-management' version '1.1.4'
}

bootJar { enabled = false }
jar { enabled = true }

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation "org.jooq:jooq:${jooqVersion}"
    
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
}

루트 프로젝트 build.gradle 수정

buildscript {
    ext {
        jooqVersion = '3.19.5'
    }
}

dependencies {
    // Entity 모듈 의존성 추가
    jooqGenerator project(':entity')
    jooqGenerator "org.jooq:jooq-meta-extensions-hibernate:${jooqVersion}"
    
    // H2 데이터베이스 (v1.4.200 권장 - v2 호환성 이슈)
    jooqGenerator 'com.h2database:h2:1.4.200'
}

jooq {
    version = "${jooqVersion}"
    configurations {
        sakilaDB {
            generationTool {
                generator {
                    name = 'org.jooq.codegen.DefaultGenerator'
                    
                    database {
                        name = 'org.jooq.meta.extensions.jpa.JPADatabase'
                        
                        properties {
                            // Entity가 있는 패키지 경로
                            property {
                                key = 'packages'
                                value = 'com.sight.entity'
                            }
                            
                            // JPA AttributeConverter 타입 매핑 여부
                            property {
                                key = 'useAttributeConverters'
                                value = true
                            }
                        }
                        
                        forcedTypes {
                            forcedType {
                                userType = 'java.lang.Long'
                                includeTypes = 'int unsigned'
                            }
                            // ... 기타 타입 매핑
                        }
                    }
                    
                    generate {
                        daos = true
                        records = true
                        fluentSetters = true
                        javaTimeTypes = true
                        deprecated = false
                    }
                    
                    target {
                        directory = 'src/generated'
                    }
                    
                    strategy.name = 'jooq.custom.generator.JPrefixGeneratorStrategy'
                }
            }
        }
    }
}

JPA Entity 준비

  • InteliJ의 JPA 기능으로 데이터베이스로부터 생성
  • JPA Buddy 플러그인 사용 (30일 무료)
  • 직접 작성

DSL 생성 및 검증

./gradlew generateJooq
```

생성 로그에서 다음을 확인할 수 있습니다:
```
INFO org.jooq.meta.extensions.jpa.JPADatabase -- Entities added: Number of entities added: 16
INFO org.hibernate.dialect.Dialect -- HHH000400: Using dialect: org.hibernate.dialect.H2Dialect

JPA Entity 기반 DSL 생성 방식은 아래와 같은 특징이 있다

적합한 경우

  • JPA와 jOOQ를 함께 사용하는 프로젝트
  • 이미 멀티모듈 구조로 Entity가 분리되어 있는 경우
  • DB 스키마 변경이 잦아 독립적인 DSL 생성이 필요한 경우

권장하지 않는 경우

  • 단일 모듈 프로젝트 (모듈 분리 비용이 큼)
  • Stored Procedure, Trigger, View 등을 많이 사용하는 경우
  • 새로운 프로젝트를 시작하는 경우

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