1. 발생 상황
- 작업 내용:
스프링 배치의 Tasklet에서 2만 건의 데이터를 100건씩 나눠서 처리하려고 함.- API에서 데이터를 조회하여 DB에 저장.
- 저장된 데이터를 기반으로 응답 데이터를 다른 API에 전달.
- 처리 방식:
JPA를 사용하여 데이터 저장 및 업데이트를 처리하려고 했으나, 대량 데이터 작업에서 JPA의 더티체킹 문제로 인해 트랜잭션 충돌이 발생함.
2. 문제 상황 및 원인
1) JPA 더티체킹으로 인한 에러
- 현상:
entities.flush()와 entities.clear()를 사용하여 100건씩 트랜잭션을 처리하려 했으나, DTO를 사용하지 않고 동일한 엔티티를 반복적으로 처리하면서 더티체킹이 실패하고 이후 업데이트가 누락됨. - 원인:
- flush() 이후 clear()가 호출되면서 영속성 컨텍스트가 초기화되어 엔티티 관리가 중단됨.
- 이후 작업에서 이미 clear()된 영속 엔티티가 더티체킹되지 않아, JPA가 업데이트 작업을 수행하지 않음.
2) DTO 미사용 및 엔티티 관리 실패
- 문제점:
기존 영속 상태의 엔티티를 재사용하려 했으나, clear() 호출로 인해 영속성 컨텍스트에서 관리되지 않음. - 결과:
- 첫 100건 이후부터 데이터 업데이트가 적용되지 않고 누락됨.
- 영속 상태를 잃은 엔티티가 JPA 관리 대상에서 벗어나면서 예외 발생.
3. 해결 방안
1) DTO 사용 및 엔티티 재생성 + jdbcTemplate.batchUpdate 활용
JPA 대신 jdbcTemplate.batchUpdate를 활용하여 DTO로 데이터를 받아 처리하고, 대량 데이터를 효율적으로 업데이트.
- 장점:
- JPA의 영속성 컨텍스트 문제를 회피.
- 배치 쿼리를 통해 대량 데이터를 빠르게 저장/업데이트 가능.
- 구현 코드:
public void processBulkData(List<MyDto> dtos) {
String sql = "INSERT INTO my_table (field1, field2) VALUES (?, ?) " +
"ON DUPLICATE KEY UPDATE field2 = VALUES(field2)";
List<Object[]> batchArgs = new ArrayList<>();
for (MyDto dto : dtos) {
batchArgs.add(new Object[]{dto.getField1(), dto.getField2()});
}
// 100건 단위로 배치 업데이트 실행
int batchSize = 100;
for (int i = 0; i < batchArgs.size(); i += batchSize) {
List<Object[]> batchChunk = batchArgs.subList(i, Math.min(i + batchSize, batchArgs.size()));
jdbcTemplate.batchUpdate(sql, batchChunk);
}
}
- 설명:
- DTO로 데이터를 받아서 처리하여 JPA의 영속성 컨텍스트와 완전히 분리.
- jdbcTemplate.batchUpdate를 통해 한 번의 DB 호출로 100건씩 업데이트 수행.
- 업데이트 성능을 극대화하고 JPA 관련 문제를 해결.
2) Spring Batch의 Chunk 방식으로 전환
JPA를 계속 사용하면서도 대량 데이터를 효율적으로 처리하기 위해, Tasklet 대신 Chunk 기반의 처리 방식을 사용.
- 장점:
- Chunk의 트랜잭션 단위로 데이터를 처리하여 메모리 사용량을 제어.
- 각 Chunk 처리 후 자동으로 flush() 및 clear()가 수행되므로, 별도의 명시적 호출이 불필요.
- 구현 코드:
@Bean
public Step chunkStep() {
return stepBuilderFactory.get("chunkStep")
.<InputDto, OutputEntity>chunk(100)
.reader(reader())
.processor(processor())
.writer(writer())
.build();
}
@Bean
public ItemWriter<OutputEntity> writer() {
return items -> myRepository.saveAll(items);
}
- 설명:
- Chunk 단위로 데이터를 읽고 처리한 뒤 저장.
- 트랜잭션이 Chunk 단위로 관리되어 메모리와 트랜잭션 문제를 예방.
4. 정리된 해결법
- 1) DTO 사용 + jdbcTemplate.batchUpdate 활용:
DTO로 데이터를 분리하여 JPA를 완전히 배제하고, jdbcTemplate.batchUpdate를 사용해 대량 데이터를 빠르고 안정적으로 업데이트. - 2) Chunk 기반 처리 방식 사용:
Tasklet 대신 Chunk 방식을 사용하여 JPA의 강점을 살리면서도 대량 데이터를 효과적으로 처리. - 주의사항:
- JPA를 사용하는 경우 flush() 및 clear() 호출 후에는 반드시 새로운 엔티티를 생성해야 함.
- 기존 엔티티를 재사용하면 더티체킹 문제로 인해 업데이트가 누락될 수 있음.
이 두 가지 해결법을 적절히 활용하면 JPA 더티체킹 문제를 효과적으로 방지하면서 대량 데이터를 안정적으로 처리할 수 있습니다.
'장애 개선 > Error' 카테고리의 다른 글
GraalVM Native 실행 시 clone3 때문에 발생한 에러 정리 (0) | 2025.04.01 |
---|---|
GraalVM Native 빌드 시 Missing character set id 846 오류 해결기 (0) | 2025.03.31 |
graalvm 빌드 시 에러 |Logging system failed to initialize using configuration from 'null' (2) | 2024.12.23 |
UnsatisfiedLinkError: already loaded in another classloader 에러 (0) | 2023.04.05 |
ClassNotFoundException: javax.xml.bind.DataTypeConverter (0) | 2023.01.18 |