티스토리 뷰
spring batch 에서 chunk oriented 방식을 이용해 로직을 작성하다보면 reader-writer 쌍이 필요해진다. 이때 JPA 를 사용하고 있다면 JpaPagingItemReader 와 JpaItemWriter 를 많이 이용하게 될텐데 이때 writer 에서 저장하는게 신규 엔티티라면 문제가 없지만 기존 엔티티의 업데이트라면 원치않는 select 쿼리가 날아가는걸 경험할 수 있다.
public Step chunkStep1() {
return stepBuilderFactory.get("chunkStep1")
.<Person, Person>chunk(500)
.reader(chunkReader())
.processor(chunkProcessor())
.writer(chunkWriter())
.build();
}
@Bean
public ItemReader<Person> chunkReader() {
return new JpaPagingItemReaderBuilder<Person>()
.saveState(false)
.entityManagerFactory(entityManagerFactory)
.queryString("SELECT p FROM Person p")
.pageSize(500)
.build();
}
@Bean
public ItemProcessor<Person, Person> chunkProcessor() {
return p -> {
p.ageUp();
return p;
}
}
@Bean
public ItemWriter<Person> chunkWriter() {
return new JpaItemWriterBuilder<Person>()
.entityManagerFactory(entityManagerFactory)
.build();
}
위 코드는 기존 DB 에 존재하는 person 을 조회하여 한 살씩 나이를 증가시키고 다시 저장하는 배치 로직이다.
위 코드의 예상 로직은 다음과 같다.
paging select 쿼리 -> 건별로 process 적용 -> update 쿼리
하지만 예상과는 다른 한 종류의 쿼리가 더 날아게된다.
Hibernate:
/* load com.batch.jpa.person.Person */ select
person0_.id as id1_0_0_,
person0_.age as age2_0_0_,
person0_.name as name3_0_0_
from
person person0_
where
person0_.id=?
paging 외 id 로 단건 조회하는 쿼리가 row 갯수만큼 날아가는 것이다.
이 이슈는 JpaItemWriter 와 JPA EntityManager 의 합작품인데 JpaItemWriter 의 내부 구현은 아래와 같이 되어있다.
usePersist 가 false 가 되면 EntityManager 의 merge 를 호출하게되는데 이 merge 메서드의 동작 방식은 id 필드가 채워져있는 경우 select 을 호출하여 DB 존재 유무를 판별하고, 그 결과를 이용하여 insert/update 를 구분하게된다.
참고로 100% 새로운 엔티티라서 insert 를 호출해야하는 경우엔 usePersist 속성을 true 로 만들어주면 된다. 이 속성을 설정하는 법은 아래와 같다.
@Bean
public ItemWriter<Person> chunkWriter() {
return new JpaItemWriterBuilder<Person>()
.entityManagerFactory(entityManagerFactory)
.usePersist(true)
.build();
}
다시 본론으로 돌아와서 우리의 person 은 DB 에서 조회한 값이므로 id 필드가 당연히 채워져있고, 그렇기때문에 select-update 쿼리가 날아가게 되는 것이다. JPA 는 기본적으로 dirty check 기능을 지원하므로 이 기능을 사용한다면 굳이 merge 를 호출하지 않아도 될 것이라고 생각했다.
@Bean
public ItemWriter<Person> chunkWriter() {
return persons -> { };
}
사실 dirty check 는 JPA 사용자가 직접 뭔가를 호출해서 처리하는 기능이 아니므로 writer 에서 아무것도 안하면 되지않을까 라고 생각하고 위와 같은 빈 구현을 사용했는데, 생각대로 작동하는걸 확인했다.
class JpaDirtyCheckWriter<T> implements ItemWriter<T> {
@Override
public void write(List<? extends T> items) throws Exception {
}
}
@Bean
public ItemWriter<Person> chunkWriter() {
return new JpaDirtyCheckWriter<>();
}
이처럼 구현 클래스를 만들어놓으면 의미전달도 코드로 잘 표현할 수 있을 것 같다.
# 2021.08.07 추가
댓글로 페이지 갯수와 실제 dirty check 로 인한 업데이트 간에 상관관계에 대한 이슈를 알려주셨다. 해당 이슈의 정확한 문제는 마지막 페이지에 있는 엔티티에 대해서 업데이트를 하지 않는 것이다. 그 이유는 JpaPaigingItemReader 의 구현을 보면 알 수 있는데 doReadPage() 메서드를 확인해보면 아래처럼 오버라이딩을 하고있다.
@Override
@SuppressWarnings("unchecked")
protected void doReadPage() {
EntityTransaction tx = null;
if (transacted) {
tx = entityManager.getTransaction();
tx.begin();
entityManager.flush();
entityManager.clear();
}//end if
// 중략... 이 지점에서 실제 paging select 쿼리를 날림
if (!transacted) {
List<T> queryResult = query.getResultList();
for (T entity : queryResult) {
entityManager.detach(entity);
results.add(entity);
}//end if
} else {
results.addAll(query.getResultList());
tx.commit();
}//end if
}
dirty checking 으로 인한 업데이트 쿼리는 EntityManager 에서 flush() 를 호출하는 시점에 이루어지는데, 위 구현부를 보면 실제 select 쿼리를 날리기 전에 flush() 를 호출하고 있다. 원인은 이것때문인데 총 페이지가 2개 라고 가정할시
- flush() 호출(조회한적이 없으므로 지연쿼리가 날아갈 대상 엔티티가 없음)
- 1번째 조회
- flush() 호출(1번째 조회시 변경된 엔티티들에 대해 업데이트 쿼리 실행)
- 2번째 조회
- 종료
위와 같은 흐름으로 진행되기때문에 2번째 조회에서 변경된 엔티티들은 flush() 가 호출되지 못하는 것이다. 참고로 2번째 페이지가 페이지 총 갯수에 딱 맞는 row 가 조회되게되면 2번째 페이지도 업데이트가 잘 되는걸 볼 수 있는데 이건 마지막 페이지에 대한 조건문같은게 있는건 아니고 해당 페이지가 마지막인지 아닌지 알 수 없기때문에 3번째 조회 쿼리가 날아가기 때문에 flush()가 호출될 수 있는 것이다.
해당 문제는 JpaPagingItemReader 가 아닌 RepositoryItemReader 를 사용해서 해결할 수 있었다. 참고로 RepositoryItemReader 는 직접 EntityManager 에 대한 의존성을 갖지 않기 때문에 상위(아마도 step)에서 트랜잭션을 관리할 것으로 보인다.
정리하면 batch 에서 JPA 를 사용하고, dirty checking 도 사용해야 한다면 JpaPagingItemReader 대신에 RepositoryItemReader 를 사용하자. 그리고 가능하다면 batch 에서는 JPA 말고 다른 대안을 찾는게 좋을 것 같다.(spring-data-jdbc 라던가)
'Java > batch' 카테고리의 다른 글
JdbcPagingItemReader 에서 column not found (0) | 2021.08.28 |
---|---|
spring-batch 에서 TaskExecutor 사용시 jvm 종료 안 되는 현상 (1) | 2021.05.02 |
spring batch builder 설정 메서드 정리 (0) | 2021.04.18 |
tasklet 에서 @BeforeStep 이 동작하지 않을때 (0) | 2021.01.07 |
Spring batch RunIdIncrementer 사용시 이슈 (0) | 2020.08.21 |
- Total
- Today
- Yesterday
- backend개발환경
- servlet
- code
- OOP
- TEST
- Spring
- frontend개발환경
- JavaScript Core
- generics
- clean code
- programming
- spring cloud
- javascript
- db
- java
- Design Pattern
- Kotlin
- MySQL
- java8
- go-core
- Jackson
- 정규표현식
- http
- Git
- frontcode
- JPA
- DesignPattern
- mariadb
- EffectiveJava
- toby
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |