본문 바로가기

Spring Boot/비동기 처리

Spring Event : 비동기 이벤트 처리 구현

애앵앷

 

 - Spring Event와 @Async를 결합하여 비동기 이벤트를 처리하는 원리

 - @Async의 동작방식, 스레드 풀(Thread Pool)

 - @EventListener와 @Async 조합을 통해 서비스 성능을 높이는 구조 구현

 - @TransactionalEventListener의 역할과 시점(BEFORE_COMMIT, AFTER_COMMIT 등) 구분하기

 - 트랜잭션과 비동기 이벤트의 관계를 실무적으로 이해하고 주의사항 익히기

 


@Async와 비동기 이벤트 처리의 필요성

Spring Event 시스템은 기본적으로 동기(Synchronous) 방식으로 동작함

즉, 이벤트를 발행하면 리스너가 모두 처리될 때까지 다음 코드가 실행되지 않는다

그러나 메일전송, 로그기록, 외부 API 호출 등 시간이 오래 걸리는 작업의 경우 비동기(Asynchronous)로 처리하는게 효율적이다

그걸 도와주는게 @Async 애너테이션이다

 

1. 동기 vs 비동기 이벤트 처리 비교

구분 동기 비동기
실행방식 이벤트 발행 후, 리스너가 끝날때까지 대기 리스너를 별도 스레드에서 실행함
장점 순서 보장, 예측 가능 빠른응답, UI/서비스 블로킹 방지
단점 느림, 메인로직 지연 순서제어가 어려움, 예외처리 주의 필요

 

 

2. @Async 활성화 설정

@Async를 사용하려면 가장 먼저 @EnableAsync를 통해 스프링에서 비동기 처리를 활성화해야함!!

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

@Configuration
@EnableAsync
public class AsyncConfig {

}

 

이걸 작성해주면, @Async가 붙은 메서드는 별도의 스레드 풀(Thread Pool)에서 실행된다

 


@Async와 @EventListener 조합하기

@Async와 @EventListener 두 조합은 이벤트 리스너를 비동기적으로 실행되게 만들어준다.

 

1.비동기 이벤트 리스너 구현

리스너 예시

@Component
public class NotificationEventListener {
    @Async
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        System.out.println(Thread.currentThread().getName() + " : 이메일 발송 시작");
        sendWelcomeMail(event.getEmail());
        System.out.println(Thread.currentThread().getName() + " : 이메일 발송 완료");
    }

    private void sendWelcomeMail(String email) {
        try {
            Thread.sleep(3000); // 이메일 전송 예시 시뮬레이션
            System.out.println("이메일을 전송했습니다. : " + email);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

@Async가 붙은 리스너의 경우, 별도 스레드에서 실행되기 때문에 메인 로직이 차단되지 않는다.

 

 

2. 이벤트 발행자 코드 예시

@Service
public class UserService {
	private final ApplicationEventPublisher publisher;
    
    public UserService(ApplicationEventPublisher publisher) {
    	this.publisher = publisher;
    }
    
    public void register(String email) {
    	System.out.println("회원가입 처리 완료 : " + email);
        userRepository.save(new User(email)); // 이 시점에서 커밋이 발생한다고 한다.
        
        publisher.publishEvent(new UserRegisteredEvent(email)); // 이메일 발송 이벤트 발행
        System.out.println("회원가입 API 응답 완료");
    }
}

 

출력 예시

회원가입 처리 완료: user@example.com
회원가입 API 응답 완료 // 비동기적 처리
SimpleAsyncTaskExecutor-1 - 이메일 발송 시작 // 리스너 시작
환영 이메일을 전송했습니다: user@example.com
SimpleAsyncTaskExecutor-1 - 이메일 발송 완료 // 리스너 끝

 

회원가입 로직의 경우 즉시 종료되며, 이메일 발송은 별도의 스레드에서 처리된다.

 

 

3. 스레드 풀 설정

기본적으로 Spring은 SimpleAsyncTaskExecutor를 사용하지만, 직접 스레드 풀 크기나 큐 크기를 지정해주는게 좋다고 함(실무)

@Configuration
public class AsyncThreadPoolConfig {
	@Bean(name = "taskExecutor")
    public Executor taskExecutor() {
    	ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4); // 최소 스레드의 수
        executor.setMaxPoolSize(8); // 최대 스레드의 수 설정
        executor.setQueueCapacity(50); // 큐 용량
        executor.setThreadNamePrefix("AsyncExecutor-"); // ?
        executor.initialize();
        return executor;
    }
}

 

@Async("taskExecutor") 같은 형태를 통해 명시적으로 특정 스레드 풀을 지정할 수 있다고 한다.

 


@TransactionalEventListener - 트랜잭션 시점별 이벤트 처리

@TransactionalEventListener는 트랜잭션의 상태에 따라 이벤트를 처리할 수 있게 해준다.

이 애너테이션을 통해서 트랜잭션이 완료된 시점에 맞춰 이벤트를 실행할 수 있다고 함

 

1. 동작 개념

시점 의미 예시
BEFORE_COMMIT 트랜잭션 커밋 직전에 실행됨 검증 로직 실행
AFTER_COMMIT 트랜잭션 커밋 완료 후에 실행됨 이메일 전송, 로그 저장
AFTER_ROLLBACK 롤백시 실행됨 보상 트랜잭션 수행
AFTER_COMPLETION 트랜잭션 완료시(성공, 실패 무관) 리소스 정리

 

이 개념을 @TransactionalEventListener과 함께 쓸 수 있다

@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)

 

 

2. 예시

@Component
public class TransactionalEventHandler {
	@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) // 커밋되고 실행
    public void afterCommit(UserRegisteredEvent event) {
    	System.out.println("트랜잭션 커밋 이후 처리 : " + event.getEmail());
    }
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) // 롤백시 실행
    public void afterRollback(UserRegisteredEvent event) {
    	System.out.println("트랜잭션 롤백 감지 : " + evnet.getEmail());
    }
}

 


동작 흐름

비동기 이벤트와 트랜잭션 이벤트의 처리 순서를 비교한 것이라고 함......

비동기 동작 흐름

 


상황
이메일, 알림 등 지연 작업 @Async 사용으로 서비스 응답 속도 향상
데이터 일관성이 중요한 작업 @TransactionalEventListener(phase = AFTER_COMMIT) 사용
대량 트래픽 서비스 ThreadPoolExecutor 커스터마이징을 통해 안정적 운영
테스트시 비동기 실행이 어렵다.. @Async를 MockExecutor로 대체하거나 SynchronousExecutor 설정하기