본문 바로가기

프로그래밍

Spring Boot 이벤트핸들링 과 @TransactionalEventListener, @Transactional

혹시나 읽을지도 모를 개발자 분들께 미리 말씀드리자면 코드는 Spring Boot 2에서 작성되었고, 이전 버전에서는 다르게 작업해야 할 수도 있습니다.

문제점과 결론

당면한 문제는 이렇다.
Spring Boot Project에서 비동기 이벤트리스너를 통해서 사용자 이력이 정상적으로 저장되면 푸시를 발송하고, 푸시를 발송했다는 사실을 다시 데이터베이스에 저장하는 기능을 구현하는데, 푸시를 발송한 후에 데이터베이스에 값이 저장되지 않고 핸들링이 종료되고 있었다. 해당 문제를 해결하기 위해서는 푸시를 발송하고 DB에 저장하는 함수에 @Transactional(Propagation.REQUIRES_NEW) 를 붙여줘야 한다.

이벤트 핸들링

스프링에서는 다음과 같이 이벤트 핸들링을 구현할 수 있다.

@Service
public class ServiceWithEventPublish { // 이벤트를 발행하는 서비스
    private final ApplicationEventPublisher applicationEventPublisher; // 부트에서는 별 다른 설정을 안해도 bean으로 등록된다.
    private final SomeRepository someRepository; //SomeEntity 의 JpaRepository

    public ServiceWithEventPublish(ApplicationEventPublisher applicationEventPublisher, SomeRepository someRepository) {
        this.applicationEventPublisher = applicationEventPublisher;
        this.someRepository = someRepository;
    }

    @Transactional // 트랜잭셔널이 붙어야 하냐 안붙어야 하냐에 따라서 상당히 달라질 수 있다.
    public void eventPublishAfterSave(SomeEntity someEntity) {
        SomeEntity someEntitySaved = someRepository.save(someEntity); // 저장
        applicationEventPublisher.publishEvent(SomeEntitySaveEvent(this, someEntitySaved)); //이벤트 발행
        doSomethingAfterwards();
    }

    private void doSomethingAfterwards() {
        //이하 생략
    }
}

public class SomeEntitySaveEvent extends ApplicationEvent { //이벤트 클래스 ApplicationEvent를 상속받아야 한다.
    public SomeEntitySaveEvent(
            SomeEntity someEntity,
            Object source
    ) {
        super(source);
        this.someEntity = someEntity;
    }

    public SomeEntity someEntity;
}

@Entity
public class SomeEntity { // JPA에서 관리해줄 엔티티 클래스

    @Id
    Long id;

}

interface SomeRepository extends JpaRepository<SomeRepository, Long> { // JPA 저장소
}

@Service
public class EventListenerService { // 발행한 이벤트를 통해 무언가를 처리할 서비스

    @EventListener(SomeEntitySaveEvent.class)
    public void sendPush(SomeEntitySaveEvent event) {
        // ...푸쉬 전송
    }
}

간단하게 정리를 하자면,

  1. 스프링이 제공하는 ApplicaitonEvent를 상속한 우리가 원하는 정보를 전달할 이벤트 클래스 구현
  2. 이벤트 발행자에게 ApplicationEventPublisher오토와이어
  3. 이벤트를 받아서 처리할 함수에 @EventListner를 붙여주고, 해당 함수는 이벤트 클래스의 객체를 인자로 받을 수 있게 만든다.

여기다가 비동기로 처리하고 싶으면 @Async를 실행 함수 위에 붙여주기만 하면 끝이다. TMI일 수도 있지만, @Async는 이벤트 처리랑 상관이 없다. 비동기 처리하고 싶은 함수에 가져다 붙이면 된다.

문제점

여기서 위의 예제 코드처럼 단순히 구현했을 경우 문제가 발생할 수 있다.

  1. 이벤트 리스너가 익셉션을 던진다. -> 트랜잭션이 롤백된다. -> 이벤트 발행했던 서비스의 저장하려던 정보가 날아가 버린다.

  2. 이벤트 리스너가 넘겨받은 객체의 정보는 아직 데이터베이스에 들어간 정보가 아니다. Async로 해도 보장되지 않는다. 따라서 해당 정보로 추가 데이터베이스 작업이 필요할 경우 제약이 생긴다.

스프링에서 제공하는 @TransactionalEventListener를 통해서 이런 문제를 해결할 수 있다.

@TransactionalEventListener

사용방법은 굉장히 간단하다. @EventListner 대신에 @TransactionalEventListener를 등록하기만 하면 된다. 기능을 간단하게 얘기하면 Event가 트랜잭션이 걸려있는 상태에서 발생하면 트랜잭션 진행 상황에 따라서 이벤트 리스너를 진행하는 것이다. 설정은 다음과 같이 4가지로 할 수 있다.

AFTER_COMMIT (default setting) - specialization of AFTER_COMPLETION, used when transaction has successfully committed
AFTER_ROLLBACK - specialization of AFTER_COMPLETION, used when transaction has rolled back
AFTER_COMPLETION - used when transaction has completed (regardless the success)
BEFORE_COMMIT - used before transaction commit

디폴트로 세팅 후에 리스너에서 에러가 발생해도 발행하는 서비스가 처리(영속성 부여)했던 모든 작업들은 DB에 실제로 들어간다.

하지만...

Propagation.REQUIRES_NEW

문제는 여기서 끝이 아니다!
만약 이벤트 리스너에서 트랜잭셔널을 걸고 추가로 디비 insert update delete를 진행할 경우 오류 없이 실행되었는데도 불구하고 저장되지 않는다!!! 트랜잭셔널이벤트리스너에 어떤 설정을 해도 퍼블리셔의 트랜잭션 안에서 동작한다. 그리고 추가 커밋은 허용하지 않는다. 따라서 추가 설정이 들어가지 않으면 리스너에서 데이터 변경 작업을 할 수 없다. (실제로 스프링 문서에도 해당 내용이 나와있다. 문서를 읽지 않고 삽질하는 바람에 한두 시간 정도 날린 듯... ㅠ 문서를 꼼꼼히...) 데이터 변경 작업이 필요할 경우
이벤트 리스너에서 @Transactional(propagation = Propagation.REQUIRES_NEW)를 설정해야 한다. 해당 설정은 새로운 트랜잭션을 시작하겠다는 설정이다. 그러면 퍼블리셔의 커밋을 보장하고, 이벤트 리스너에서도 새로운 트랜잭션 안에서 데이터 변경 작업을 진행할 수 있다.

끝.