Spring Data JPA의 saveAll() 사용시 주의점
현재 개발 중인 배치 프로그램이 있는데 처리 과정이 모두 끝나고 마지막에 몇 만건의 데이터를 DB에 insert하는 과정이 있다. 대량의 데이터를 한꺼번에 처리해야되니 Spring Data에서 제공하는 saveAll()을 사용할 생각이었는데.. 얼마전 DBA께서 ERD를 보며 하신 말씀이 떠올랐다.
"이 테이블은 commit 단위를 만 건 이하로 끊어주세요."
saveAll을 호출하게 되면 몇 만건의 데이터가 한꺼번에 bulk insert 되기 때문에 데이터를 적절한 chunk로 나누어줄 필요가 있다는 것인데, 그 전에 saveAll의 내부 구현을 확인해보았다.
saveAll 내부에서는 save를 반복 호출하는데, 두 메서드에 모두 @Transactional이 걸려있다. 이 경우 우선순위는 어디에 있을까? 다시 Transactional 애노테이션의 내부 구현을 보면..
Transactional은 Propagation(전파) 속성을 설정할 수 있게 되어있는데 디폴트 값이 Required다. 다시 Required가 어떤 의미인지 보자.
Transation이 이미 있으면 그냥 쓰고, 없으면 만든다고 쓰여 있다. 그렇다면 saveAll을 호출했을 때 이미 Transation이 만들어지고 내부적으로 save를 호출될 때도 같은 Transaction을 계속 사용하게 된다는 것이다. 여기까지 확인했으니 이제 다시 돌아가서, DBA가 요구한 적절한 단위로 데이터를 나누어 처리 하도록 코드를 변경해주면 된다.
Set<BatchResult> transactionChunk = new HashSet<>();
for(BatchResult result : BatchResult){
transactionChunk.add(result);
if(transactionChunk.size() == TRANSACTION_CHUNK_LIMIT) {
batchResultRepository.saveAll(transactionChunk);
transactionChunk.clear();
log.info("BatchResult has saved with chunk unit");
}
}
batchResultRepository.saveAll(transactionChunk);
10,000건 단위로 데이터를 잘라서 insert 해주고, 마지막에 나머지를 처리해주었다. Baeldung을 보니 이렇게 saveAll을 쓰는게 퍼포먼스가 60% 이상 좋다고 하는데, 다건 데이터의 처리를 한 트랜잭션으로 묶어주니 당연한 얘기긴하다. 그러나 bulk insert의 경우 그만큼 DB 커넥션을 오래 잡게 된다는 것이니 얘기치 못한 장애 상황으로 이어질 여지가 있다. 따라서 적절한 commit 단위를 두는 것이 중요하다.
참고
www.baeldung.com/spring-data-save-saveall