문제 배경
대규모 배치 시스템에서 master/slave로 분리된 DB 환경을 사용하는 경우,
하나의 Step 내에서 slave로부터 읽고, master에 쓰는 구조가 필요하다.
그러나 잘못된 트랜잭션 구성으로 인해 slave의 커넥션이 반환되지 않고 누수(leak)되는 문제가 발생할 수 있다.
해결 전략
- slaveMapper 조회는 TransactionTemplate으로 감싸 트랜잭션 분리
- masterMapper는 기본
@Transactional
환경 (Step 트랜잭션)에 참여 - 예외가 발생해도 slave 쿼리는 이미 커밋 or 롤백되며 커넥션이 안전하게 반환됨
- 이 구조를 모든 Tasklet에 확장할 수 있도록 공통 템플릿 클래스 설계
핵심 코드 예시
① 트랜잭션 분리용 유틸 컴포넌트
@Component
public class SampleSlaveTransactionSupport {
private final TransactionTemplate template;
public SampleSlaveTransactionSupport(@Qualifier("slaveTransactionManager") PlatformTransactionManager txManager) {
this.template = new TransactionTemplate(txManager);
this.template.setReadOnly(true);
}
public <T> T read(Supplier<T> supplier) {
return template.execute(status -> supplier.get());
}
}
② Tasklet 내 안전한 사용 구조
List<SampleEntity> list = slaveTxSupport.read(() -> slaveMapper.selectList());
if (!list.isEmpty()) {
for (int i = 0; i <= list.size() / 1000; i++) {
List<SampleEntity> chunk = ...;
masterMapper.insertBatch(chunk); // 예외 발생 시에도 slave 커넥션은 이미 반환됨
}
}
③ 공통 추상 클래스 템플릿
public abstract class AbstractSlaveMasterTasklet<T> implements Tasklet {
...
protected abstract List<T> readFromSlave();
protected abstract void writeToMaster(List<T> list);
}
④ 실제 구현 예
@Component
public class SampleTasklet extends AbstractSlaveMasterTasklet<SampleEntity> {
@Override
protected List<SampleEntity> readFromSlave() {
return slaveMapper.selectSampleList();
}
@Override
protected void writeToMaster(List<SampleEntity> chunk) {
masterMapper.insertSampleList(chunk);
}
}
기대 효과
커넥션 누수 없이 안전한 트랜잭션 처리
slave/master 분리에도 명확한 책임 분할
공통 구조로 모든 Tasklet에 일관성 있게 적용 가능
대용량 배치 환경에서도 안정적 운영 가능