발생한 문제

트랜잭션 커밋과 관계없이 TaskScheduler를 통해 작업을 예약한 결과, 커밋이 완료되기 전에 스케줄된 Task가 실행되어 DB에 저장되지 않은 데이터를 조회하려다 실패하는 문제가 발생했습니다.

문제 되는 코드

  • 아래에서 registerTaskSchedule를 사용해서 비동기로 동작을 등록하였는데 messageLog가 저장되기 전에 읽어서 읽히는 메시지의 row가 0인 문제가 발생하였습니다.
@Transactional  
public List<MessageLog> createMessageAllGroup(Channel channel,  ...) {  
 
	// 로직들이 실행된다.
	...
 
    // 메시지를 저장한다.
    List<MessageLog> messageLogList = messageLogRepository.saveAll(groupInfo.stream()  
        .map(g -> createMessageLogForGroup(g, channel, typeInfo.get(),  
            reserveTime, content, exceptLineIds))  
        .toList());  
  
    messageUtils.registerTaskSchedule(messageLogList, reserveTime);  
  
    return messageLogList;  
}

해결 과정

  • TransactionSynchronizationManager.registerSynchronization + afterCommit을 사용해서 트랜잭션 커밋이 완료된 후에 TaskScheduler 등록이 될 수 있도록 설정하였다.

TransactionSynchronizationManager란

  • 현재 스레드에 바인딩된 트랜잭션 리소스(DB 커넥션, 세션 등)와 트랜잭션에 연관된 동기화 콜백(후처리 작업 등)을 관리하는 ThreadLocal 기반 유틸리티

대표 메소드

  • 트랜잭션 리소스 등록/조회
TransactionSynchronizationManager.bindResource(DataSource, Connection);
TransactionSynchronizationManager.getResource(DataSource);
TransactionSynchronizationManager.unbindResource(DataSource);
  • 트랜잭션 동기화 콜백 관리
TransactionSynchronizationManager.registerSynchronization
	(TransactionSynchronization synchronization);
  • 트랜잭션 상태 조회
TransactionSynchronizationManager.isActualTransactionActive();
TransactionSynchronizationManager.isSynchronizationActive();
TransactionSynchronizationManager.getCurrentTransactionName();

예시 코드

// commit이 된 이후에 실행을 한다.  
TransactionSynchronizationManager.registerSynchronization(  
    new TransactionSynchronization() {  
        @Override  
        public void afterCommit() {  
            messageUtils.registerTaskSchedule(messageLogList, reserveTime);  
        }  
    }  
);

다른 방법

@TransactionalEventListener

트랜잭션 이벤트 리스너 — 스프링 트랜잭션의 상태에 따라 특정 이벤트를 처리하는 리스너.

  • 동기적으로 동작한다.

예시 코드

  • 이벤트 클래스
public class MessageLogCreatedEvent {
    private final List<MessageLog> logs;
    private final LocalDateTime reserveTime;
 
    // 생성자 + Getter
}
  • 이벤트 발행
@Transactional
public void createMessageLog(...) {
    messageLogRepository.saveAll(...);
 
    eventPublisher.publishEvent(new MessageLogCreatedEvent(logs, reserveTime));
}
  • 이벤트 리스너
    • 이벤트 객체의 타입 및 파라미터를 기준으로 이벤트를 매칭한다.
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleMessageLogCreatedEvent(MessageLogCreatedEvent event) {
    messageUtils.registerTaskSchedule(event.getLogs(), event.getReserveTime());
}p

@Async

스프링의 비동기를 진행하는 어노테이션

  • 메서드 호출을 별도의 스레드에서 비동기적으로 실행.
  • 기본적으로 Spring이 제공하는 TaskExecutor를 사용.
  • 메인 쓰레드를 차단하지 않고 비동기로 처리할 수 있다는 장점이 있다.

예시 코드

  • 스프링 비동기 활성화
@EnableAsync
@Configuration
public class AsyncConfig {
}
  • Transaction commit이 끝난 후 비동기 적으로 동작한다.
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleMessageLogCreatedEvent(MessageLogCreatedEvent event) {
    messageUtils.registerTaskSchedule(event.getLogs(), event.getReserveTime());
}

taskExecutor 설정예시

@Bean(name = "taskExecutor")
public Executor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10);
    executor.setMaxPoolSize(50);
    executor.setQueueCapacity(100);
    executor.setThreadNamePrefix("async-executor-");
    executor.initialize();
    return executor;
}