@TransactionalEventListener 사용법 정리

지능바바·2023년 5월 12일
0

1. @TransactionalEventListener 어노테이션은?

Spring에서 @Transaction 어노테이션을 이용해서 트랜잭션을 처리했을때 추가적인 처리를 할 수 있는 기능이다.

이벤트가 실행되는 시점은 아래의 4가지 경우로 지정이 가능하다.

  • BEFORE_COMMIT : 커밋이 되기전에 이벤트를 실행한다.
  • AFTER_COMMIT : 커밋이 되고난 후에 이벤트를 실행한다.(Default)
  • AFTER_ROLLBACK : 롤백이 되고난 후에 이벤트를 실행한다.
  • AFTER_COMPLETION : 커밋 또는 롤백 이후에 이벤트를 실행한다.

2. 예제코드

spring:
  h2:
    console:
      enabled: true
  datasource:
    hikari:
      driver-class-name: org.h2.Driver
      jdbc-url: jdbc:h2:~/test
      username: sa
      password: 1111
      maximum-pool-size: 1
      connection-timeout: 3000


  jpa:
    generate-ddl: true
    show-sql: true

logging:
  level:
    com.zaxxer.hikari.HikariConfig: DEBUG
    com.zaxxer.hikari: TRACE
@Slf4j
@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public Member insertMember() {
        Member member = new Member();
        member.setName("테스터1");
        memberRepository.save(member);
        log.info("insert after");
        eventPublisher.publishEvent(new MemberEvent(member.getId()));

        return member;
    }
}
@Slf4j
@Component
@RequiredArgsConstructor
public class MemberListener {

    private final MemberRepository memberRepository;

    @TransactionalEventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handle(MemberEvent event) {
        Optional<Member> member = memberRepository.findById(event.getId());
        log.info(member.get().getName());
    }
}
@Getter
public class MemberEvent {
    private final Long id;

    public MemberEvent(Long id) {
        this.id = id;
    }
}
@SpringBootTest
class MemberServiceTest {
    @Autowired
    private MemberService memberService;

    @Test
    void insertMember() {
        memberService.insertMember();
    }
}

위와 같이 예제코드를 작성하고 테스트를 실행해 보았다. 단순히 생각했을때는 별 문제 없이 성공할 것 같지만 실제로는 MemberService 의 insertMember 메소드까지는 잘 실행되지만 MemberListener까지는 실행이 되지 않는다. MemberListener의 handle 메소드가 실행되지 못하고 계속 DB 커넥션을 가져오기위해 대기하다가 타임아웃이 나기 때문이다.
위의 예제코드에서 이런 문제가 발생하는 이유는 maximum-pool-size: 1 설정과 MemberListener handle 메소드의 @Transactional(propagation = Propagation.REQUIRES_NEW) 때문이다.

아래는 TransactionalEventListener의 레퍼런스 문서이다.
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html

위 링크에 들어가보면 아래와 같은 내용을 볼 수 있다.

WARNING: if the TransactionPhase is set to AFTER_COMMIT (the default), AFTER_ROLLBACK, or AFTER_COMPLETION, the transaction will have been committed or rolled back 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, but changes will not be committed to the transactional resource. See TransactionSynchronization.afterCompletion(int) for details.

TransactionPhase가 AFTER_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION 일때 트랜잭션이 완료되면 commit 또는 rollback 이 되지만 커넥션은 아직 유지하고 있다는 내용이다.

결국 DB Connection 사이즈는 1인데, 기존 커넥션이 아직 반환되지 않은 상태에서 새로운 트랜잭션을 만들기위해 커넥션을 요청하니 계속 대기하다가 타임아웃이 발생한 것이다.

이 문제를 해결하기 위해서는 maximum-pool-size를 늘려주는 방법도 있지만, MemberListener 클래스에 아래와 같이 @Async를 추가해서 비동기로 동작하도록 하는 방법도 있다.

@Slf4j
@Component
@Async("asyncExecutor")
@RequiredArgsConstructor
public class MemberListener {
   ...
}

비동기 동작을 위한 세팅은 아래와 같다.

@EnableAsync
@Configuration
public class AsyncConfig {

    @Bean
    public Executor asyncExecutor() {
        return Executors.newFixedThreadPool(3);  // Thread 사이즈 3
    }
}

위의 예제에서는 MemberListener에서 새로운 트랜잭션을 생성하기 위해서 @Async를 추가했지만 만약 새로운 트랜잭션의 추가가 필요없다면 @Async는 추가할 필요는 없다.
새로운 트랜잭션을 생성하지 않는다면 기존의 DB커넥션을 그대로 사용해서 질의 하기 때문에 사용에 문제는 없다.

3. 기타

TransactionalEventListener 를 통해 이벤트 핸들러에서 DB INSERT, UPDATE, DELETE 와 같이 트랜잭션이 필요한 동작을 수행할 필요가 없다면 굳이 @Async를 붙이지는 않아도 된다. 하지만 아래와 같은 이유로 @Async를 붙였을때 응답속도를 향상시킬 수는 있다.
또한 TransactionalEventListener는 별도의 스레드로 실행되는 것이 아니라 처음 요청과 같은 스레드에서 동작한다. 위의 예제에서는 MemberService 의 insertMember 메소드와 같은 스레드로 동작한다. 그러므로 컨트롤러를 통해 요청을 하게되면 EventListener가 동작을 마친 뒤에 응답을 하게 된다.

0개의 댓글