리서치

[Springboot] Jpa 프로젝트에 jOOQ 도입

꾸준함. 2023. 10. 3. 02:36

jOOQ를 도입하게 된 배경

현재 진행하고 있는 프로젝트의 기술스택 중 이번 게시글과 연관된 기술들과 버전은 아래와 같습니다.

  • Springboot 2.6.3
  • Java 1.8
  • spring-boot-starter-data-jpa (기본 키 생성 전략: IDENTITY)
  • MariaDB 10.9.2 버전
  • QueryDSL 5.0.0 버전
  • Maven

 

프로젝트 내 기능 중 대용량 엑셀 업로드 기능과 json import 기능이 있는데 데이터가 커질수록 병목현상이 심해지는 것을 발견할 수 있었습니다. (json import의 경우 멀티 쓰레드 방식으로 과장님이 어느 정도 해결하시긴 하셨습니다.)

대용량으로 업로드할 때 entity를 하나하나 저장할 경우 불필요한 DB 커넥션이 많이 생기므로 1000개씩 묶어서 bulk insert 하도록 코드를 작성하였는데 로그를 확인하니 실상은 청크 단위로 JpaRepository의 saveAll() 메서드를 사용했는데도 불구하고 DB에 1000개를 한꺼번에 저장하는 것이 아니라 한 개씩 천 번 저장하고 있는 것을 확인할 수 있었습니다.

즉, 네트워크를 저장하고자 하는 데이터의 개수만큼 타기 때문에 병목현상을 야기하고 있었습니다.

 

병목현상이 발생하는 코드 예시

 

 

 

JpaRepository의 saveAll() 메서드가 원하는 대로 작동하지 않는 이유

 

위와 같은 현상이 발생한 원인은 아래 stackoverflow 게시글에 잘 나와있습니다.

https://stackoverflow.com/questions/27697810/why-does-hibernate-disable-insert-batching-when-using-an-identity-identifier-gen

 

Why does Hibernate disable INSERT batching when using an IDENTITY identifier generator

The Hibernate documentation says: Hibernate disables insert batching at the JDBC level transparently if you use an identity identifier generator. But all my entities have this configuration:...

stackoverflow.com

 

간단하게 요약하자면 Hibernate에서 "transactional write-behind" 원칙을 고수하고 기본 키 생성 전략이 IDENTITY일 때 INSERT문을 실행하기 전까지 ID에 할당될 값을 미리 알 수 없습니다.

정리하자면 Entity를 영속성 컨텍스트에 등록하기 위해서는 반드시 PK 값이 필요한데 이를 가져오기 위해서는 DB로 INSERT 쿼리를 보내야 합니다.

따라서, JpaRepository를 통해서는 Bulk Insert문을 수행할 수 없으며 해결 방법으로는 기본 키 생성 전략을 TABLE 방식으로 변경하는 것인데 TABLE 방식은 기본적으로 IDENTITY 전략보다 비효율적이기 때문에 상용 서비스에는 거의 채택되고 있지 않으므로 근본적인 해결방법이 아닙니다.

그렇다면 세 가지 방식 중 언급되지 않은 SEQUENCE 방식을 채택하면 되지 않냐고 하시겠지만 이와 관련된 내용은 향로님의 블로그에 잘 작성이 되어있습니다.

 

SEQUENCE 방식으로 전환하는 것이 해답이 아닌 이유를 간단히 정리하면 아래와 같습니다.

  • 이미 auto_increment로 대량의 데이터가 쌓여있는 테이블에서 채번 전략을 변경 및 마이그레이션 하는 과정이 부담스러운 작업
  • HikariCP 데드락 이슈 발생 가능성 (제가 이해한 바에 따르면 SEQUENCE 방식은 다음 시퀀스를 받아오는 connection과 실제 삽입하는 connection 두 개의 conneciton이 필요하므로 Hikari CP의 maximum pool size를 잘 못 선택한 상태에서 thread pool 내 쓰레드를 모두 사용 중일 경우 데드락이 발생할 수 있습니다.)
  • 동일하게 Bulk Insert가 지원되는 환경에서는 Auto Increment가 더 성능이 뛰어납니다.

 

정리를 하자면 Bulk Insert가 지원되는 환경에서 Auto Increment가 성능이 뛰어나지만 기본키 전략을 IDENTITY로 사용하는 Jpa 환경에서는 Bulk Insert가 불가능합니다.

따라서, Jpa가 아닌 JdbcTemplate과 같은 네이티브 쿼리 작성을 통해 해결해야 하는데 이 또한 아래와 같은 이유로 부담스러웠습니다.

  • JdbcTemplate과 같은 문자열 기반의 SQL 프레임워크는 IDE 자동 지원이 제한적이며 컴파일 시점에 에러를 찾기 쉽지 않습니다.
  • 개발 과정에서 Entity 변경이 자주 발생하는데 이때마다 연관된 쿼리 문자열을 매번 수정하는 것 또한 쉽지 않습니다.

 

그래서 저는 QueryDSL처럼 Typesafe 하면서 IDE 자동 지원, 타입 지원, 그리고 컴파일 시점에 문법 오류를 체크할 수 있는 오픈소스를 찾기 시작했고 아래와 같은 후보군이 나왔습니다.

  • Querydsl-SQL
  • Querydsl-EntityQL
  • jOOQ

 

여기서 Querydsl-SQL과 jOOQ는 Jpa처럼 어노테이션 기반이 아닌 실제 테이블 스캐닝 방식이라 향로님처럼 Querydsl-EntityQL을 채택하려고 했습니다.

하지만 해당 프로젝트는 2020년 이후 업데이트가 없는 상태이며 레퍼런스도 현저히 부족했기 때문에 2023년 10월인 현재에 도입할 수 없었습니다.

만약 EntityQL을 사용하고 싶으신 분들은 https://jojoldu.tistory.com/558?category=637935 블로그를 참고하시는 것을 강력 추천 드립니다.

 

 

마찬가지로 Querydsl-SQL 또한 Querydsl의 고질적인 문제인 업데이트가 느린 것이 문제였습니다.

QueryDSL도 2021년 7월을 기점으로 업데이트가 없는 상태이기 때문에 사실 추후 프로젝트에 QueryDSL을 도입해야 하는지 여부를 고민해봐야 하는 시점으로 보입니다.

 

 

반면, jOOQ는 오픈소스이기도 하지만 실제 라이선스를 판매하는 큰 프로젝트였습니다.

비용 처리에 인색한 회사이기에 라이선스를 구입하는 것은 힘들겠지만 어차피 저희는 MariaDB를 사용하고 있기 때문에 오픈소스 버전을 사용하는 것이 딱히 문제가 되진 않았습니다.

실제로 사용해 본 결과 MyBatis보다 편했고 꾸준히 업데이트가 이루어지고 있기 때문에 테이블 스캐닝 방식인데도 불구하고 저는 jOOQ를 선택했습니다.

테이블 스캐닝 방식의 경우 flyway나 liquibase와 같이 DB 마이그레이션 및 버저닝 툴을 사용한다면 jOOQ code를 생성하는데 별로 문제가 없을 것이라고 판단했습니다.

 

https://www.jooq.org/download/

 

Free downloads and pricing for jOOQ

jOOQ, a fluent API for typesafe SQL query construction and execution.

www.jooq.org

 

 

Jpa 프로젝트에 jOOQ 도입하기 위한 환경 설정

jOOQ 환경설정은 sightstudio님의 "Jooq를 JPA와 같이 써보자" 블로그와 공식 문서를 많이 참고했습니다.

다만 저랑 sightstudio님이 처한 상황이 달랐기 때문에 저는 비교적 더 쉽게 환경 설정을 진행할 수 있었으며 차이점은 아래와 같습니다.

 

1. sightstudio님 팀 같은 경우 모든 팀원이 공통 DB를 바라보고 있는 구조이기 때문에 개발하는 구조이기 때문에 jOOQ DSL을 직접 버저닝 해야 하는 허들이 발생했습니다.

  • 저희 팀 같은 경우 로컬 환경에서는 각자 Docker로 띄운 로컬 DB를 바라보는 구조이고 테스트를 완료한 후 flyway와 같은 툴을 통해 DB 변경사항을 미리 DBA를 통해 개발 DB 및 상용 DB에 반영 후 빌드하는 구조이기 때문에 이 부분에 대해서는 딱히 허들이 없을 것으로 판단됩니다.
  • 각자 로컬 DB 형상으로 mvn clean install 혹은 mvn clean generate-sources 명령어를 통해 jOOQ DSL 코드를 생성하면 문제가 없을 것 같습니다.

2. sightstudio님은 모듈 분리를 통해 JPA 엔티티를 통해 jOOQ DSL을 생성하는 수고를 감행하셨습니다. (멀티-모듈 강제)

  • 1번에서 언급한 대로 저희 팀은 각자 로컬 DB에 붙고 DB 수정사항을 반영하는 DML 및 DDL을 버저닝 하고 있기 때문에 JPA 엔티티를 기반으로 DSL을 생성하지 않아도 됩니다.
  • 즉, 그냥 각자 로컬 DB 형상을 기준으로 DSL 코드를 생성해도 되기 때문에 이 부분 또한 생략해도 될 것 같습니다.
  • sightstudio님도 결국에는 testcontainer와 flyway를 통해 DSL을 생성하시는 방법으로 수정하신 것 같은데 해당 방법을 원하시면 sightstudio님께 문의를 드려야 할 것 같습니다.

 

환경 설정 시작

 

저는 앞서 언급했던 프로젝트 기술스택 기준으로 환경설정을 했습니다.

pom.xml에 jOOQ 관련 dependency를 추가해 줍니다.

저 같은 경우 java 1.8을 사용하고 있고 jOOQ 오픈소스 버전 중 3.14.16이 java 8+를 지원하는 최신 버전이기 때문에 3.14.16을 사용했습니다.

 

<!-- jOOQ Dependencies -->
<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq</artifactId>
    <version>3.14.16</version>
</dependency>
<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq-meta</artifactId>
    <version>3.14.16</version>
</dependency>
<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>jooq-codegen</artifactId>
    <version>3.14.16</version>
</dependency>

 

그리고 DSL을 생성하기 위해서는 아래와 같이 plugin도 추가를 해야 합니다.

 

 

 

이렇게 설정한 뒤 mvn clean install 명령어를 통해 프로젝트 build를 진행하면 plugin에 명시한 DB 테이블 스캔을 진행한 뒤 필요한 DSL 코드를 생성한 것을 볼 수 있습니다.

저는 QueryDSL도 같이 사용한다고 가정을 했기 때문에 maven dependency에 QueryDSL도 있었으며 이에 따라 QClass와 DSL 코드 모두 생성되는 것을 확인할 수 있었습니다.

 

 

참고로 대상 DB에 변경사항이 미리 반영되어 있어야 DSL 코드가 알맞게 생성되므로 저처럼 flyway와 같은 DB 버저닝 툴을 사용하시는 것을 추천드립니다.

저는 아래와 같이 샘플 쿼리를 추가했습니다.

 

 

공용 DB를 사용할 경우에는 testcontainer와 flyway를 통해 DSL 코드를 생성하는 것을 추천드리는데 방법은 알지만 구현을 하지 못했습니다.

혹시 성공하신 분들은 댓글로 방법을 알려주시면 감사하겠습니다.

testcontainer와 flyway를 통해 DSL 코드를 생성하기 위해서는 아래와 같은 과정을 거쳐야 합니다.

  • Create an instance of database using Testcontainers
  • Apply Flyway or Liquibase database migrations
  • Run jOOQ code-generator to generate Java code from the database objects.
  • Run integration tests

 

https://testcontainers.com/guides/working-with-jooq-flyway-using-testcontainers/

 

Working with jOOQ and Flyway using Testcontainers

This guide will explain how to test your jOOQ and Flyway based application by generating java code from database using Testcontainers.

testcontainers.com

 

jOOQ를 도입한 결과 성능은?

힘들게 환경설정을 마쳤다면 이제 결실의 열매를 먹을 시간입니다.

저는 간단하게 성능을 비교하기 위해 아래와 같이 테스트 코드를 작성했습니다.

 

Entity

 

 

 

Test Code

 

 

두 테스트 코드는 정확히 같은 기능을 수행하며 하나의 팀 당 세 명의 멤버를 배치한 뒤 DB에 저장합니다.

팀은 천 개 단위로 DB에 저장하는 방식을 수행했고 jpa 버전은 saveAll() 기능을 사용하지만 앞서 언급한 이유 때문에 실제로는 saveAll()을 수행하지 못하고 save()를 천 번씩 수행하는 것과 같이 동작하기 때문에 상당히 느리게 동작했습니다.

수행 결과는 아래와 같습니다.

jOOQ 코드에 메서드명이 with_jooq_and_jpa라고 쓰여있지만 실질적으로 jooq로 작성되어 있고 jpa가 같이 쓰일 수 있을지 테스트를 위해 조회하는 부분만 jpa를 사용했습니다.

 

Team 1000개 저장할 때 (팀: 1,000, 멤버: 3,000)

 

 jpa 2.68초 vs jOOQ 0.92초

 

Team 10,000개 저장할 때 (팀: 10,000, 멤버: 30,000)

 

 jpa 49.92초 vs jOOQ 1.67초

 

Team 100,000개 저장할 때 (팀: 100,000, 멤버: 300,000)

 

 jpa 3389초 vs jOOQ 5.51초

 

위 내용 도식화 및 결론

 

 

결과는 상당히 충격적이었습니다.

jpa 코드를 테스트할 때 제 컴퓨터에 부하가 걸려서 훨씬 더 시간이 소요될 수 있다고 가정하더라도 jOOQ를 통한 bulk insert가 훨씬 빠른 것을 확인할 수 있었습니다.

호시 jOOQ 테스트 코드가 오동작하여 빠르게 끝난 것 아닐까 싶어 데이터 개수를 확인했는데 데이터 개수는 정확했고 member 테이블에 외래키 매핑도 잘 되어있는 것을 확인했습니다.

 

 

여태까지 jOOQ의 존재는 알았지만 도입하지 않았던 이유가 아래와 같았습니다.

  • 한글 레퍼런스가 상대적으로 부족하고
  • Jpa의 Cascade 옵션을 이용한 Insert문을 native query로 풀어서 사용하기 힘듦

 

하지만, 이처럼 성능 차이가 많이 발생한다면 jOOQ를 함께 사용하지 않을 이유가 없는 것 같습니다.

그리고 jOOQ는 typesafe 하면서 쿼리를 QueryDSL처럼 상대적으로 쉽게 작성할 수 있기 때문에 bulk insert 뿐만 아니라 Jpa가 상대적으로 취약한 조건 SELECT문에도 사용해도 될 것 같습니다.

 

제 부족한 글이 jOOQ 도입에 조금이라도 도움이 되었길 바라고 저보다 훨씬 훌륭하신 분들이 작성하신 글들을 참고 링크로 남기니 꼭 읽어보시길 바랍니다.

감사합니다.

 

부족하거나 틀린 부분 있으면 언제든지 댓글로 지적 부탁드립니다.

하루빨리 수정하도록 하겠습니다.

 

샘플 프로젝트 링크

https://github.com/jaimemin/spring-jooq-with-jpa/tree/master

 

GitHub - jaimemin/spring-jooq-with-jpa: test jooq with jpa

test jooq with jpa. Contribute to jaimemin/spring-jooq-with-jpa development by creating an account on GitHub.

github.com

  •  

 

참고

https://jojoldu.tistory.com/558?category=637935 

 

(MySQL) Auto Increment에서 TypeSafe Bulk Insert 진행하기 (feat.EntityQL, JPA)

여러 글에서 언급하고 있지만, JPA환경에서 키 생성 전략을 Auto Increment로 할 경우 BulkInsert가 지원되지 않습니다. Spring Batch Item Writer 성능 비교 MySQL 환경의 스프링부트에 하이버네이트 배치 설정

jojoldu.tistory.com

https://sightstudio.tistory.com/68

 

Jooq를 JPA와 같이 써보자

Jooq에 대해 좀 더 알아보자 라는 글을 쓴 지 벌써 8개월 가까이 지났습니다. 현재 재직 중인 회사에서는 아직 JOOQ를 사용하지 않고 있는데요. 그래도 언젠가는 쓰일 수 있다는 생각에 기술검증을

sightstudio.tistory.com

https://imksh.com/113

 

JPA saveAll이 Bulk INSERT 되지 않았던 이유

실습 환경 MySQL 5.7버전 사용 Windows 10 Entity ID 전략은 IDENTITY Java 11 서론 실무에서 MySQL 5.7 버전을 사용하고 있고, JPA Entity의 ID 전략은 IDENTITY를 사용하고 있는데, 이때 JPARepository를 이용한 saveAll과 sa

imksh.com

https://zepinos.tistory.com/52

 

[프롤로그] JOOQ 을 사용하게 된 계기

제목을 좀 거창하게 지은 것 같긴 하네요. 그냥, 제가 JOOQ 을 써보게 된 계기를 담담하게 적어보려고 합니다. Java 을 오랜 기간동안 사용해서 개발하다보니, 당연히 Connection, PreparedStatement, ResultSet

zepinos.tistory.com

 

반응형