
지난번 작업했던 TaskExecutor를 사용한 성능개선에 이어
기존 사용하던 JpaPagingItemReader를 JdbcCursorItemReader를 통해
조금 더 성능개선 해보자
우선 기존에 구현된 코드를 살펴보자
@Bean
public JpaPagingItemReader<User> userReader() {
return new JpaPagingItemReaderBuilder<User>()
.name("userReader")
.entityManagerFactory(entityManagerFactory)
.queryString("SELECT u FROM User u")
pageSize(CHUNK_SIZE)
.build();
}
Jpa Paging 기반으로 데이터를 읽어오고 있다
하지만 jpa 기반으로 코드를 작성할 경우 영속성 컨텍스트에 저장하는 작업이
추가 되기때문에 성능 저하 가능성이 있고
page처리를 할 경우 이미 처리한 데이터까지 실행시 다시 읽어주어야하기 때문에
추가적인 성능 저하 가능성이 존재했다
@Bean
@StepScope
public JdbcCursorItemReader<User> userReader() {
JdbcCursorItemReader<User> reader = new JdbcCursorItemReader<>();
reader.setDataSource(dataSource);
reader.setSql("SELECT id FROM tb_user ORDER BY id");
reader.setRowMapper((rs, rowNum) -> new User(rs.getLong("id")));
reader.setVerifyCursorPosition(false);
return reader;
}
이를 해결하기 위해 JdbcCursor 방식을 도입했다
Jdbc는 영속성 컨텍스트를 거치지않고 데이터 베이스에 바로 쿼리를 날리기때문에
처리해야하는 데이터가 많은 경우 속도가 더 우세하다
또한 Cursor 방식은 이전에 읽어왔던 데이터를 다시 읽지않고
특정 지점에서부터 원하는 크기의 데이터를 바로 가져올 수 있기때문에
성능상 더 빠르다
테스트는 각각 포스트맨과 Jmeter를 사용해 5번정도 진행시켜봤다
유저는 역시 10만명을 기준으로 했다


지표를 살펴보면 기존 평균 7~8초정도 걸리던 작업을 5초까지 개선시킨걸 볼 수 있다
또한 JPA를 사용하지 않으므로써 최소 시간과 최대시간 또한 동일해진걸 볼 수 있다
기존 평균 58초 -> 5초로 91%의 성능 최적화를 이루어냈다
처음 JdbcCursorItemReader를 구현하고 실행시켰을때 아래와 같은 에러가 발생했다
java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.study.petory.domain.notification.entity.Notification.getUserId()" is null
로그를 찍어 확인해보니 User는 정상적으로 매핑되지만
user의 Id값이 제대로 들어가지 않고 있었다

JdbcCursorItemReader에서 User 객체를 매핑할때
.rowMapper(new BeanPropertyRowMapper<>(User.class))
해당 코드를 사용했는데
BeanPropertyRowMapper의 경우 리플렉션 기반으로
setter를 호출하여 값을 주입하는 방식을 사용한다고 한다
하지만 user의 pk 값인 id에는 setter가 없었기때문에
id 값을 매핑하지 못해 에러가 나던 것이었다
유저 테이블에 id 값만을 받는 생성자를 만들고
reader.setRowMapper((rs, rowNum) -> new User(rs.getLong("id")));
rowMapper를 직접 구현하는 방식을 선택했다
이 과정에서
User 도메인의 PK 필드인 id를 어떻게 주입할 것인가에 대한 고민이 컸다
일반적으로 도메인 객체의 식별자에는 setter를 사용하지 않는 것이
객체지향적인 설계 원칙에 부합하기 때문에 setter 방식은 처음부터 지양하고자 했다
초기에는 BeanPropertyRowMapper를 사용하면서 setter가 필요했지만
리플렉션 기반 매핑의 한계와 객체 설계 원칙을 모두 고려한 끝에
생성자 기반 rowMapper 구현 방식으로 전환하여 setter 없이 문제를 해결할 수 있었다
이 경험을 통해 객체지향적인 설계 원칙을
실제 상황에 어떻게 적용할지에 대해 더 깊이 고민해볼 수 있었고
단순히 작동하는 코드를 넘어서
설계 방식과 그 근거를 함께 고민하는 습관을 들일 수 있어 의미 있었다
정말 놀라운 성능 개선이었다고 생각해요! 저였다면 8초에 만족! 했을 텐데 5초까지 줄이시는 모습이 정말 멋있었습니다. 최종 프로젝트 하시느라 고생많으셨어요!