TransactionPhase.AFTER_COMMIT 알고 사용하자

hyyyynjn·2023년 5월 8일
3

개발 기록

목록 보기
1/3
post-thumbnail

문제 상황

@Service
@RequiredArgsContructor
public class MainService {
	private final ApplicationEventPublisher publisher;
    
    @Transactional
    public void mainProcess() {
    	doSomething();
        publisher.publishEvent(MainEvent.create());
    }
}
@Component
@RequiredArgsContructor
public class MainEventHandleService {
	private final MainRepository repository;
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Transactional
    public void postPurchase(MainEvent event) {
    	doAfterMainProcessCommit();
    }
    
    private void doAfterMainProcessCommit() {
    	repository.save(MainEntity.builder().build()); // jpa save
    }
}

MainService의 mainProcess가 commit을 완료하면, event를 통해 MainEntity를 저장하는 후속작업을 진행하려고 했다. 하지만 원하는대로 MainEntity가 저장이 되지 않은 문제가 발생하였다.

그 이유를 찾고자 TransactionalEventListener의 docs를 살펴보았다.

TransactionalEventListener

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {

	/**
	 * Phase to bind the handling of an event to.
	 * <p>The default phase is {@link TransactionPhase#AFTER_COMMIT}.
	 * <p>If no transaction is in progress, the event is not processed at
	 * all unless {@link #fallbackExecution} has been enabled explicitly.
	 */
	TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;
    
    // 중략
}

TransactionalEventListener 의 기본 TransactionPhase는 AFTER_COMMIT이다.

TransactionPhase.AFTER_COMMIT


/**
 * The phase in which a transactional event listener applies.
 *
 * @author Stephane Nicoll
 * @author Juergen Hoeller
 * @author Sam Brannen
 * @since 4.2
 * @see TransactionalEventListener#phase()
 * @see TransactionalApplicationListener#getTransactionPhase()
 * @see TransactionalApplicationListener#forPayload(TransactionPhase, Consumer)
 */
public enum TransactionPhase {

	/**
	 * Handle the event before transaction commit.
	 * @see TransactionSynchronization#beforeCommit(boolean)
	 */
	BEFORE_COMMIT,

	/**
	 * Handle the event after the commit has completed successfully.
	 * <p>Note: This is a specialization of {@link #AFTER_COMPLETION} and therefore
	 * executes in the same sequence of events as {@code AFTER_COMPLETION}
	 * (and not in {@link TransactionSynchronization#afterCommit()}).
	 * <p>Interactions with the underlying transactional resource will not be
	 * committed in this phase. See
	 * {@link TransactionSynchronization#afterCompletion(int)} for details.
	 * @see TransactionSynchronization#afterCompletion(int)
	 * @see TransactionSynchronization#STATUS_COMMITTED
	 */
	AFTER_COMMIT,
	
    // 중략
}

TransactionPhase는 TransactionalEventListener가 적용되는 단계를 의미한다. 이때 TransactionPhase.AFTER_COMMIT은 commit이 성공적으로 완료된 이후 event를 처리하도록 설정하는 단계이다.

mainProcess가 성공적으로 commit이 완료되었고, doAfterMainProcessCommit() 메소드가 실행되는 걸 디버깅을 통해 확인할 수 있었지만 실제 insert query가 나가진 않았다.

AFTER_COMMIT doc에서 link로 달아둔 TransactionSynchronization.afterCommit()에 대한 설명을 더 살펴보자.

TransactionSynchronization.afterCommit()

/**
 * Interface for transaction synchronization callbacks.
 * Supported by AbstractPlatformTransactionManager.
 *
 * <p>TransactionSynchronization implementations can implement the Ordered interface
 * to influence their execution order. A synchronization that does not implement the
 * Ordered interface is appended to the end of the synchronization chain.
 *
 * <p>System synchronizations performed by Spring itself use specific order values,
 * allowing for fine-grained interaction with their execution order (if necessary).
 *
 * <p>Implements the {@link Ordered} interface to enable the execution order of
 * synchronizations to be controlled declaratively, as of 5.3. The default
 * {@link #getOrder() order} is {@link Ordered#LOWEST_PRECEDENCE}, indicating
 * late execution; return a lower value for earlier execution.
 *
 * @author Juergen Hoeller
 * @since 02.06.2003
 * @see TransactionSynchronizationManager
 * @see AbstractPlatformTransactionManager
 * @see org.springframework.jdbc.datasource.DataSourceUtils#CONNECTION_SYNCHRONIZATION_ORDER
 */
public interface TransactionSynchronization extends Ordered, Flushable {

	// 중략
    
    /**
	 * Invoked after transaction commit. Can perform further operations right
	 * <i>after</i> the main transaction has <i>successfully</i> committed.
	 * <p>Can e.g. commit further operations that are supposed to follow on a successful
	 * commit of the main transaction, like confirmation messages or emails.
	 * <p><b>NOTE:</b> The transaction will have been committed already, but the
	 * transactional resources might still be active and accessible. As a consequence,
	 * any data access code triggered at this point will still "participate" in the
	 * original transaction, allowing to perform some cleanup (with no commit following
	 * anymore!), unless it explicitly declares that it needs to run in a separate
	 * transaction. Hence: <b>Use {@code PROPAGATION_REQUIRES_NEW} for any
	 * transactional operation that is called from here.</b>
	 * @throws RuntimeException in case of errors; will be <b>propagated to the caller</b>
	 * (note: do not throw TransactionException subclasses here!)
	 */
	default void afterCommit() {
	}

	// 중략

}

NOTE 부분을 보자. afterCommit에서 호출되는 모든 트랜잭션 작업에는 PROPAGATION_REQUIRES_NEW 을 사용하라고 명시되어있다.

즉, TransactionalEventListener에서 commit이 완료되었고 이후의 추가적인 변경 작업에 대해서는 commit이 발생하지 않기 때문에 (with no commit following anymore!) afterCommit에서 호출되는 모든 트랜잭션 작업에는 PROPAGATION_REQUIRES_NEW 을 사용해야 한다.

Propagation.REQUIRES_NEW

PROPAGATION_REQUIRES_NEW은 @Transactional의 Propagation 옵션으로

/**
 * Enumeration that represents transaction propagation behaviors for use
 * with the {@link Transactional} annotation, corresponding to the
 * {@link TransactionDefinition} interface.
 *
 * @author Colin Sampaleanu
 * @author Juergen Hoeller
 * @since 1.2
 */
public enum Propagation {

	// 중략
    
	/**
	 * Create a new transaction, and suspend the current transaction if one exists.
	 * Analogous to the EJB transaction attribute of the same name.
	 * <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
	 * on all transaction managers. This in particular applies to
	 * {@link org.springframework.transaction.jta.JtaTransactionManager},
	 * which requires the {@code javax.transaction.TransactionManager} to be
	 * made available to it (which is server-specific in standard Java EE).
	 * @see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
	 */
	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
    
    // 중략

Propagation은 Transactional 어노테이션과 함께 사용되는 "트랜잭션 전파 동작"에 관한 enum 클래스이다. 이때 Propagation.REQUIRES_NEW은 새 트랜잭션을 생성하고 현재 트랜잭션이 있다면 이를 일시중지한다.

해결 방법

@Component
@RequiredArgsContructor
public class MainEventHandleService {
	private final MainRepository repository;
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Transactional(propagation = Propagation.REQUIRES_NEW) // 👈👈
    public void postPurchase(PostPurchaseEvent event) {
    	doAfterMainProcessCommit();
    }
    
    private void doAfterMainProcessCommit() {
    	repository.save(MainEntity.builder().build()); // jpa save
    }
}

0개의 댓글