본문으로 건너뛰기

JpaCursorItemReader 제대로 써보기

· 약 6분

JpaCursorItemReader 관련하여 많은 글이 있습니다.

제대로된 cursor 구현이 안되어 있어 그 효과를 누리지 못 한다고 하네요. 이 말을 듣고 꽤나 시간이 지났는데, 아직도 그럴까? 싶어서 확인해보고 싶었습니다. 제가 만들고 있는 서비스에도 써야하는 상황에 놓이기도 했구요.

그 동안 jpa 스펙도, hibernate도, spring-batch도 많은 변화가 있었을테니까요.

결론 부터 얘기하면 펙트는, 아직은 이전과 동일하다. 다만, 곧 개선될 예정이다. 입니다.

왜 그런지 살펴볼까요? (spring-batch 5.2-m2 기준)

jpa 2.2 스펙 추가

https://download.oracle.com/otn-pub/jcp/persistence-2_2-mrel-spec/JavaPersistence.pdf?AuthParam=1729391161_518d0264462d9cfe3aed562ebae8d20d

위 내용을 살펴보면, Query.getResultStream()가 추가되었습니다. 이제야 JdbcCursor 나 HibernateCursor 방식으로 cursor를 구현하지 않아도 jpa 자체에 스펙이 추가된 것 같네요.

default 메소드를 아래와 같이 구현을 해두었는데요. 해당 구현을 한 hibernate-orm은 버전은 어떻게 될까요?

default Stream getResultStream() {
return getResultList().stream();
}

hibernate-orm 5.5

5.5.Fianl 릴리즈 노트를 찾아보면, jpa 2.2 스펙을 지원한다고 명시되어 있습니다. (항상 hibernate 릴리즈 노트는 보기가 좀 불편한건 저만 그런가요? :thinking_face: )

https://in.relation.to/2021/06/02/hibernate-orm-550-final-release/

문서의 예시를 찾아봐도 jpa 2.2 스펙에서 언급했던 getResultStream()를 활용하는 것을 볼 수 있습니다.

List<Person> persons = entityManager.createQuery(
"select p " +
"from Person p " +
"where p.name like :name", Person.class )
.setParameter( "name", "J%" )
.getResultStream() // <---
.skip( 5 )
.limit( 5 )
.collect( Collectors.toList() );

https://docs.jboss.org/hibernate/orm/5.5/userguide/html_single/Hibernate_User_Guide.html#jpql-api-stream

spring-batch / 과거 JpaCursorItemReader

그렇다면, JpaCursorItemReader가 처음 도입된 시점에는 어땟길래 문제가 있던 걸까요?

https://github.com/spring-projects/spring-batch/commit/41854008e8c1f22cd2f7d4f95326b0a3f159b58b https://github.com/spring-projects/spring-batch/blob/41854008e8c1f22cd2f7d4f95326b0a3f159b58b/build.gradle#L73

hibernate-orm 버전이 5.4.20.Final 이였습니다. hibernate-orm 기준 5.5.x에 도입이 되었으나, 미리 구현되어 되었다는 것을 볼 수 있는데요. 이슈를 따라 올라가봐도 2.2 스펙에 미리 대응하기 위한 작업으로 보이기도 합니다.

그래서 getResultStream을 써도 default 메소드처럼 list.stream()을 쓰는 방식으로 밖에 동작을 안했을 거라 판단됩니다.

그렇다면, JpaCursorItemReader 이제는 정말 괜찮을까?

마지막 릴리즈 버전 (5.1.2)에서는 6.3.2.Final 입니다. https://github.com/spring-projects/spring-batch/blob/v5.1.2/pom.xml#L81 (그런데 build tool을 gradle에서 maven으로 변경된 것 같은데, 이런 경우는 처음 보네요...)

간단한 예제로 확인해보기로 했습니다.

https://github.com/heowc-scratch/spring-batch-5_x-JpaCursorItemReader

MySQL 8.x를 도커로 구성하고 간단한 엔티티 (user)를 만들어서 10만 row 정도 미리 넣어두고 이를 JpaCursorItemReader로 조회해보았습니다. 결과는 초반에 언급한 것 처럼, 예상과 다르게 메모리가 꽤나 튀는 것을 볼 수 있습니다.

img no-fetchsize

왜 그런걸까?

확인해볼 문제는 jdbc-driver 레벨까지 내려가 fetchSize 가 정상적으로 설정이 되었고, 작동하고 있느냐를 봐야합니다. 자세한 내용은 예전에 다룬 적이 있어서 해당 게시글로 대신 하겠습니다.

그러면 fetchSize를 어떻게 설정하나..?

이전에 언급한 것 처럼 driver 설정을 넣는 방법도 있지만, spring-batch에서도 관련 이슈를 언급된 부분이 있습니다.

저는 직접적으로 많이 써보지 않았지만, hint를 설정하면 가능한데 이걸 지원해주지 않는 걸로 보입니다. 그런데 최근에 우리나라 분이 해당 이슈를 해결해주셨습니다..! 감사하게도 요즘 우리나라 분들도 오픈소스에 관심이 많이지고 있어서 그런지 스프링 프로젝트에도 간간히 보이는 것 같아요 🙏

해당 버전은 5.2.0-M1에 추가되었으며, 아직 공식 릴리즈 되진 않았습니다.

https://github.com/spring-projects/spring-batch/releases/tag/v5.2.0-M1

마일스톤 일정에 의하면 2024.11.20 예정인데 당장 써야하는 분들은 쓸 수 없는 상황입니다. 그렇다고 마일스톤 버전을 쓰기엔 조금 위험할 수 도 있습니다.

https://github.com/spring-projects/spring-batch/milestone/145

HintSettableJpaCursorItemReader 임시 구현 및 결과

그래서 저는 만들어주신 hint를 추가한 reader를 임시로 HintSettableJpaCursorItemReader를 만들어서 릴리즈 전까지 쓰려고 합니다.

테스트 해본 결과는 다음과 같습니다.

img fetchsize

확실히 fetchSize 여부가 heap 메모리 부분에서 많은 차이가 나는 것을 볼 수 있습니다.