코드만 봐도 어떤 쿼리가 실행될지 예측하기 쉽다는 것이 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 등을 많이 사용하는 경우
- 새로운 프로젝트를 시작하는 경우