본문 바로가기

develop

[Skill] @TransactionalEventListener

해당 글은 spring boot와 jdbc를 사용하고 있습니다.

 

비동기 동작으로 외부 API에서 상태를 가져와서 DB에 저장하는 기능 구현을 하면서 EventListener라는 Spring에서 제공하는 이벤트 기반 프로그래밍 기능을 알게 되었습니다.

 

구현하고있는 프로세스는 간략하게 아래와 같습니다.

  1. 외부 API에 데이터의 ci.cd를 요청합니다.
  2. 외부에 요청한 데이터의 상태를 진행중(running) 상태로 변경합니다.
  3. 외부로 요청한 ci.cd의 상태가 유효해질때까지(성공 혹은 실패) 확인합니다.
  4. 외부로의 요청이 성공하면 success, 실패하면 failed 상태로 변경하여 DB에 저장합니다.

 

고려해야할 점은 아래와 같습니다.

  • 외부 API 요청이 완료되는데 최대 10분 이상의 시간이 소요될 수 있습니다. (행위가 오래 걸림)
  • 상태 체크를 하면서 성공시 success 상태로, 실패나 예외 발생시 failed 상태로 저장되어야합니다. (행위의 결과가 명확해야함)

이제 이벤트 리스너에 대해 정리를 한 후, 왜 이벤트 리스너를 선택했는지 정리해보겠습니다.

 


1. EventListener

Spring에서 제공하는 이벤트 리스너는 이벤트를 생성시키는 주체인 publisher와 이벤트를 처리하는 주체인 listener가 있습니다.

이 두 주체는 Observer 패턴을 기반이며 특정 이벤트가 발생했을 때 이를 감지하여 적절한 핸들러가 실행되도록 합니다.

 

구성요소

  • 이벤트 클래스 : 특정 상황을 표현하는 객체
  • 이벤트 발행자 : 이벤트를 생성하고 발행하는 역할
  • 이벤트 리스너 : 특정 이벤트가 발생했을 때 실행하는 역할

 

동작

1. 이벤트 클래스 생성

특정 이벤트를 표현할 이벤트 클래스를 생성합니다.

@Getter
@AllArgsConstructor
public class PipelineEvent {
    private Long projectId;
    private Long pipelineId;
    private String tagName;
}

2. 이벤트 발행

applicationEventPublisher의 publishEvent(...)를 호출하면 이벤트가 Spring의 이벤트 시스템을 통해 전파됩니다.

publishEvent에 발행할 이벤트 클래스 객체와 함께 호출합니다.

public void try(PipelineInput input) {
	//...
    
    //이벤트 클래스 객체 생성 및 이벤트 발행
    applicationEventPublisher.publishEvent(new PipelineEvent(input.getProjectId(), input.getPipelineId(), input.getTagName()));
}

3. 이벤트 리스너 감지

발생한 이벤트를 감지하고 처리하는 @EventListener를 구현합니다.

@EventListener는 메서드에서 동작하는 어노테이션입니다.

 

PipelineEvent 이벤트 클래스의 이벤트가 발행되었을때 해당 이벤트 리스너에서 이벤트를 감지하고 해당 메서드를 동작합니다.

@RequiredArgsConstructor
@Component
public class PipelineEventListener{

	//이벤트를 감지하고 동작하는 메서드
	@EventListner
    public void handlePipelineEvent(PipelineEvent event) {
    
    	try{
        	//외부 요청을 통해 상태를 계속 확인하는 기능...
            //성공시 status = success 후 DB 저장
        }
        catch(RuntimeException exception) {
        	//status = failed 후 DB 저장
        }
        
        
    }
}

 

 


2. @TransactionalEventListener

위에서 설명했던 @EventListener는 트랜잭션과 관계없이 즉시 실행됩니다.

 

@TransactionalEventListener는 트랜잭션의 특정 시점에 이벤트를 실행할 수 있는 옵션이 있습니다.

  • BEFORE_COMMIT : 트랜잭션이 커밋되기 전에 실행
  • AFTER_COMMIT : 트랜잭션이 성공적으로 커밋된 후 실행
  • AFTER_ROLLBACK : 트랜잭션이 롤백된 후 실행
  • AFTER_COMPLETION : 트랜잭션이 완료된 후 실행 (커밋,롤백 상관없이 실행)

아래에서 phase별로 이벤트 발생 순서를 확인해보겠습니다.

@RequiredArgsConstructor
@Component
public class PipelineEventListener {

    @TransactionalEventListener
    public void defaultEvent(PipelineEvent event) throws InterruptedException {
        log.info("defaultEvent!");
    }

    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void beforeCommitEvent(PipelineEvent event) throws InterruptedException {
        log.info("beforeCommitEvent!");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void afterCommitEvent(PipelineEvent event) throws InterruptedException {
        log.info("afterCommitEvent!");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void afterRollbackEvent(PipelineEvent event) throws InterruptedException {
        log.info("afterRollbackEvent!");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void afterCompletionEvent(PipelineEvent event) throws InterruptedException {
        log.info("afterCompletionEvent!");
    }
}

이벤트 발생 순서

결과를 보면 BEFORE_COMMIT > DEFAULT(AFTER_COMMIT) > AFTER_COMMIT > AFTER_COMPLETION 으로 발생하는 것을 확인 할 수 있습니다.

 

public void test() {
	//...
    
    applicationEventPublisher.publishEvent(new PipelineEvent(mr));

	throw new GlobalException("test2 exception");
}

 

예외 발생시 이벤트 발생 순서

다음은 이벤트 발행 후 예외가 발생했을 경우입니다.

결과를 보시면 AFTER_COMPLETION > AFTER_ROLLBACK이 순서대로 발생했습니다.

 

AFTER_ROLLBACK은 커밋, 롤백과 상관없이 수행되고 AFTER_ROLLBACK은 롤백이 발생한 경우에만 발생되기 때문에 두 이벤트 리스너가 발생하는 것입니다.


3. @Async와 @TransactionalEventListener

@TransactionalEventListener를 통해 트랜잭션 특정 시점에 이벤트가 발생되는 것을 확인했습니다.

 

그런데 만약 이벤트가 오래 걸리는 경우에는 어떻게 될까요?

 

@TransactionalEventListener
    public void defaultEvent(PipelineEvent event) throws InterruptedException {
        log.info("defaultEvent!");

        log.info("sleep start");
        Thread.sleep(5000);

        log.info("sleep end");
    }

 

지연 응답 발생

API 요청을 보내면 5초 이상의 지연 응답이 발생하였고, 이벤트 처리가 10분 이상으로 지연된다면 문제가 발생할것입니다.

 

이 문제를 해결하기 위해서는 Spring에서 제공해주는 비동기 기능인 @Async를 사용합니다.

 

@Async
    @TransactionalEventListener
    public void defaultEvent(PipelineEvent event) throws InterruptedException {
        log.info("defaultEvent!");

        log.info("sleep start");
        Thread.sleep(5000);

        log.info("sleep end");
    }

 

응답 속도 개선

 

@Async를 간단하게 설명하면, 별도의 스레드를 실행시켜주기 때문에 메인 비즈니스 로직과 분리되어 별도로 이벤트를 실행시켜줍니다. 그렇기 때문에 발생시킨 이벤트 처리가 오래 걸린다 할지라도 메인이 되는 로직의 응답에 많은 영향을 끼치지 않게 됩니다.

'develop' 카테고리의 다른 글

[Spring] Servlet Container  (1) 2025.06.03
[Skill] Spring 이벤트 예외 전파  (0) 2025.02.10