Java/Spring & Spring Boot

이벤트리스너와 비동기

마손리 2023. 4. 28. 01:11

이벤트리스너

ApplicationEventPublisher를 이용하여 이벤트를 발생시킨뒤 @EventListener@TransactionalEventListener 에너테이션을 사용하여 정해진 대상과 관련된 메서드를 특정한 시점에 호출시킬수 있다.

 

이벤트리스너를 사용하면 정해진 조건에 맞는 이벤트만을 추적하여 관련된 메서드만을 자동으로 실행시킬수 있게 되어 따로 특정한 로직을 실행 시키기위해 다른 객체들과 의존관계를 맺지 않아도 되어 코드간의 의존성을 낮추고 더 독립적인 프로그래밍이 가능하다.

 

 

예제 코드

@Getter
@Component
@Slf4j
public class EventHandler {
	// 1.
    @EventListener
    public void process(Member member){
        log.info("멤버 생성전");
    }
    @EventListener
    public void process(Coffee coffee){
        log.info("커피 생성전");
    }

    	// 2.
    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void beforeTxCommit(Member member){
        log.info("트렌젝션 커밋전");
    }
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void afterTxCommit(Member member){
        log.info("트렌젝션 커밋후");
    }
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void AfterTx(Member member){
        log.info("트렌젝션 끝난후");
    }
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void AfterRB(Member member){
        log.info("롤백 끝난후");
    }
}

위와 같이 이벤트가 발생했을때 'phase' 어트리뷰트 조건에따라 어떤 메서드를 호출할지 정해줄수 있다.

에너테이션이 붙은 모든 메서드들은 한개의 매개변수를 필수로 넣어 주어야한다.

 

  1. @EventListener 에너테이션을 사용하면 이벤트 발생 직후 해당 메서드가 호출된다. 매개변수와 관련된 객체에만 작동을 한다. (예제에서는 실험을 위해 두가지 클래스로 오버라이딩)
  2. @TransactionalEventListener와 어트리뷰트로 트랜잭션과 관련된 특정한 시점에 메서드를 호출한다.

 

실험을 위한 비즈니스 로직

테스트1

@Transactional
@Service
public class MemberService {
    private final MemberRepository memberRepository;
    private final ApplicationEventPublisher applicationEventPublisher; // 1.

    public MemberService(MemberRepository memberRepository, 
    			ApplicationEventPublisher applicationEventPublisher) {
        this.memberRepository = memberRepository;
        this.applicationEventPublisher = applicationEventPublisher;
    }
    @Transactional
    public Member createMember(Member member) {
        applicationEventPublisher.publishEvent(new Member()); // 2.
        Member savedMember = memberRepository.save(member); // 3.
        return savedMember;
    }

}
  1. ApplicationEventPublisher로 이벤트를 발생시킨다.
  2. Member 객체를 매개변수로 넣어주면 Member객체를 받는 @EventListener 메서드를 호출시킨다.
  3. 영속성 컨텍스트에 Member 객체 저장

결과1

 

 

 

테스트2

EventListener에 설정된 Member 객체가아닌 다른객체로 이벤트 발생

@Transactional
@Service
public class MemberService {
    private final MemberRepository memberRepository;
    private final ApplicationEventPublisher applicationEventPublisher; 

    public MemberService(MemberRepository memberRepository, 
    			ApplicationEventPublisher applicationEventPublisher) {
        this.memberRepository = memberRepository;
        this.applicationEventPublisher = applicationEventPublisher;
    }
    @Transactional
    public Member createMember(Member member) {
        applicationEventPublisher.publishEvent(new Coffee()); // 1.
        Member savedMember = memberRepository.save(member); 
        return savedMember;
    }

}
  1. 테스트1과 달리 Coffee 객체로 이벤트를 발생시킴

 

결과2

Coffee 객체를 매개변수로받는 이벤트리스너 메서드만 호출되었다.

 

 

결과로 보아 ApplicationEventPublisher로 이벤트 발생시 전달한 매개변수를 받는 이벤트리스너의 메서드들만 지정된 시점에 호출이된다.

 

즉, ApplicationEventPublisherMember 객체를 전달하면 매개변수로 Member 객체를 받는 이벤트리스너들만 호출이 되는것이다.

 

 

 

비동기화와 같이 사용

같은 코드이지만 트랜잭션 커밋 이후 비동기화를 진행한뒤 분리된 스레드는 3초뒤에 롤백시도를 위한 예외를 강제 발생시킨다.

@Getter
@Component
@Slf4j
@EnableAsync  // 1.
public class EventHandler {
    ...
    ...
    @Async  // 2.
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // 커밋 이후
    public void afterTxCommit(Member member) throws InterruptedException {
        log.info("트렌젝션 커밋후");
        Thread.sleep(3000);	// 3초간 휴식
        log.info("3초 뒤");
        throw new RuntimeException("롤백 시도"); // 롤백을 위한 예외 발생
    }
    ...
    ...
}

EventHandler에서 위의 메서드만 수정

  1. @EnableAsync 에너테이션을 클래스에 선언
  2. @Async를 비동기를 진행할 메서드에 선언하면 비동기화가 끝나며 멀티쓰레드로 작업을 한다.

 

 

결과

로그를 봤을때 "트랜잭션 끝난후"라는 메세지 이후 "3초 뒤"라는 메세지를 받은 걸 봤을때는 분명 비동기화가 이뤄젔다. 하지만 롤백시도를 위한 예외발생이후 롤백에 관한 로그는 나오지 않았다. 

 

데이터베이스의 결과 또한 롤백되지 않고 데이터가 그대로 남아있었다.

 

즉, 스레드가 나눠지면 보조 스레드에서 예외가 발생하더라도 메인스레드의 트랜잭션에 영향을 주지 않는다. 

 

또한 몇번의 실험을 더 한 결과 스레드가 분리되더라도 메인스레드는 트랜잭션의 원자성을 유지하는 반면 보조 스레드는 원자성을 유지하지 않는다. 

 

보조스레드도 트랜잭션 기능을 유지시키기 위해선 @Transactional 에너테이션을 해당 메서드에 선언해주어 새로운 트랜잭션을 적용시켜 주어야 한다.