모르지 않다는 것은 아는것과 다르다.

Jdbc

JOOQ (Java Object Oriented Querying)

채마스 2022. 3. 24. 21:30

JOOQ 란?

  • 테이블 스키마로부터 자바 코드를 만들어주는 라이브러리 이다.
  • 시스템의 설계가 자바 코드(엔티티)가 아닌, DB 에서 시작될 때 유용하다.
  • 당연히 ORM framework 가 아니다.
    • ORM 은 오히려 반대로 Java class 를 DB 스키마로 매핑하기 때문이다.
  • "jOOQ is not a replacement for JPA"
  • SQL이 잘 어울리는 곳엔, Jooq가 잘 맞는다.
  • 반면, Object Persistence 가 잘 어울리는 곳엔, JPA 가 잘 맞는다.

 

JOOQ 의 장점

  • Jooq 방식으로 사용한다면, 엔티티를 작성할 필요가 없다.
    • 대신 JPA 와 반대로 스키마는 정의 되어 있어야 된다.
  • 로그가 보기 좋다. -> 결과 로그가 한눈에 보기 좋다.
  • query 로그 안에 binding parameter 가 함께 포함된다.

 

JOOQ 의 단점

  • ORM기술이 아니기 때문에,SpringDataJPA와 결이 잘 맞지 않는다.
  • JPA 와 정반대의 매커니즘: Jooq 클래스가 엔티티 클래스를 방해한다.
  • JPA 기술이 아니어서 오는 문제점이 있다.
  • Spring Data JPA 트랜잭션 연동이 힘듬: 자체 트랜잭션 기술 -> Jooq 코드가 서비스에 노출된다.
  • 하이버네이트 auto-ddl 사용 불가, DB 스키마는 이미 준비되어 있어야 한다.
  • 스프링과 연동되어있지 않아서 오는 불편하다.
  • 스프링 Pageable 정보로부터 Jooq 쿼리를 조립하기가 상당히 까다롭다.
  • gradle 플러그인을 쓸 경우, DB 정보가 build.gradle 에 침투한다.
    • DB 기본 정보, 로깅, 타입 변환(enum 등) 정보, 패키지 정보, DB dialect, JDBC 드라이버 정보
  • 그리고 매뉴얼도 꽤 어렵고, 레퍼런스도 적다.
  • Open Source 플랜만 무료이다.

 

Gradle 설정

plugins {
    id 'nu.studer.jooq' version '5.2.2'
}

project.ext {
    jooqVersion = dependencyManagement.importedProperties['jooq.version']
    mysqlVersion = dependencyManagement.importedProperties['mysql.version']
}

dependencies {

    // Jooq
    implementation 'org.springframework.boot:spring-boot-starter-jooq'
    jooqGenerator "mysql:mysql-connector-java:${project.mysqlVersion}"

}

//// Jooq 설정부
def generatedJooq = 'src/main/generated-jooq'

jooq {
    version = project.jooqVersion

    configurations {
        main {  // name of the jOOQ configuration
            generationTool {
                logging = org.jooq.meta.jaxb.Logging.WARN
                jdbc {
                    driver = 'com.mysql.cj.jdbc.Driver'
                    url = 'jdbc:mysql://localhost:3306/service_desk'
                    user = 'root'
                    password = '12356'
                }
                generator {
                    name = 'org.jooq.codegen.JavaGenerator'
                    database {
                        name = 'org.jooq.meta.mysql.MySQLDatabase'
                        inputSchema = 'service_desk'
                        includes = '.*'
                        excludes = ''
                        forcedTypes {
                            forcedType {
                                userType = 'com.hyunwook.servicedesk.common.enumclass.ErrorCode'
                                enumConverter = true
                                includeExpression = '.*\\.error_code'
                                includeTypes = '.*'
                            }
                            forcedType {
                                userType = 'com.hyunwook.servicedesk.common.enumclass.RequestGroupState'
                                enumConverter = true
                                includeExpression = '.*\\.request_group_state'
                                includeTypes = '.*'
                            }
                        }
                    }
                    generate {
                        deprecated = false
                        records = true
                        immutablePojos = true
                        fluentSetters = true
                        javaTimeTypes = true
                    }
                    target {
                        packageName = 'com.hyunwook.servicedesk'
                        directory = generatedJooq
                    }
                    strategy.name = 'org.jooq.codegen.DefaultGeneratorStrategy'
                }
            }
        }
    }
}

// incremental build (증분 빌드) - Jooq 오브젝트 생성 퍼포먼스 향상
tasks.named('generateJooq').configure {
    allInputsDeclared = true
    outputs.cacheIf { true }
}

// java source set 에 Jooq 디렉토리 추가
sourceSets {
    main.java.srcDir generatedJooq
}

// gradle clean 시에 Jooq 디렉토리 삭제 (본래의 Jooq 의도에는 맞지 않는 사용법)
tasks.named('clean') {
    dependsOn 'cleanGenerateJooq'
}
  • plugin, version, dependences 를 설정한다.
  • 위와 같이 증분 빌드를 설정해서 성능을 높일 수 있다.
  • 또한, clean 시 cleanGenerateJooq 실행하도록 설정한다.

 

DB 스키마 -> Java Class

  • 위와 같이 DB 스키마를 정의했다.

  • gradle 에서 설정한 위치에 java class 가 생성되는 것을 확인할 수 있다.

 

코드 예시

@RequiredArgsConstructor
@Repository
public class CustomerRepositoryJooqImpl implements CustomerRepositoryJooq {

    private final DSLContext dslContext;

    @Override
    public Page<CustomerViewResponse> findCustomerViewPageBySearchParams(
            String placeName,
            String customerName,
            CustomerStatus customerStatus,
            LocalDateTime customerStartDatetime,
            LocalDateTime customerEndDatetime,
            Pageable pageable
    ) {
        final Customer CUSTOMER = customer.CUSTOMER;
        final Place PLACE = Place.PLACE;

        Condition condition = trueCondition();

        SelectField<?>[] select = {
                CUSTOMER.ID,
                PLACE.PLACE_NAME,
                CUSTOMER.CUSTOMER_NAME,
                CUSTOMER.CUSTOMER_STATUS,
                CUSTOMER.CUSTOMER_START_DATETIME,
                CUSTOMER.CUSTOMER_END_DATETIME,
                CUSTOMER.CURRENT_NUMBER_OF_PEOPLE,
                CUSTOMER.CAPACITY,
                CUSTOMER.MEMO
        };

        if (placeName != null && !placeName.isBlank()) {
            condition = condition.and(PLACE.PLACE_NAME.containsIgnoreCase(placeName));
        }
        if (customerName != null && !customerName.isBlank()) {
            condition = condition.and(CUSTOMER.CUSTOMER_NAME.contains(customerName));
        }
        if (customerStatus != null) {
            condition = condition.and(CUSTOMER.CUSTOMER_STATUS.eq(customerStatus));
        }
        if (customerStartDatetime != null) {
            condition = condition.and(CUSTOMER.CUSTOMER_START_DATETIME.ge(customerStartDatetime));
        }
        if (customerEndDatetime != null) {
            condition = condition.and(CUSTOMER.CUSTOMER_END_DATETIME.le(customerEndDatetime));
        }

        int count = dslContext
                .selectCount()
                .from(CUSTOMER)
                .innerJoin(PLACE)
                .onKey()
                .where(condition)
                .fetchSingleInto(int.class);

        List<CustomerViewResponse> pagedList = dslContext
                .select(select)
                .from(CUSTOMER)
                .innerJoin(PLACE)
                .onKey()
                .where(condition)
                .limit(pageable.getOffset(), pageable.getPageSize())
                .fetchInto(CustomerViewResponse.class);

        return new PageImpl<>(pagedList, pageable, count);
    }

}
  • 위에서 생성된 Customer 클래스는 JOOQ 에 의해서 만들어진 클래스 이다.
  • 위와 같이 queryDsl 과 유사하게 조건과 조인, pageable 을 설정할 수 있다.
  • 하지만 위에서 언급했던 단점들 때문에 실무에서는 queryDsl 에 비해서 적게 사용한다.




REFERENCES

  • 김은호 님의 Spring 강의

'Jdbc' 카테고리의 다른 글

HikariCP 코드 분석하기 2편 (HikariCP 커넥션 풀 초기화 과정)  (0) 2023.01.21
HikariCP 코드 분석하기 1편 (HikariCP란?)  (0) 2023.01.21
Spring JDBC  (0) 2022.02.27
JPA vs MyBatis  (0) 2022.02.27
JDBC  (0) 2022.02.27