728x90
[Spring] Transaction - @TransactionalEventListener를 이용한 이벤트 처리
일반 @EventListener는 트랜잭션의 성공/실패와 독립적으로 동작합니다. 반면 @TransactionalEventListener는 트랜잭션의 상태(커밋/롤백/완료 직전) 에 맞춰 리스너 실행 시점을 제어합니다.
트랜잭션 안팎의 경계를 정확히 지키면서 후속 작업(이메일 발송, 캐시 무효화, 메시지 발행, 인덱싱 등) 을 안전하게 실행하고 싶다면 @TransactionalEventListener를 사용할 수 있습니다.
1. 핵심 옵션
- phase = AFTER_COMMIT(기본): 커밋 성공 후 실행
- phase = AFTER_ROLLBACK: 롤백 시에만 실행
- phase = AFTER_COMPLETION: 커밋/롤백 관계없이 완료 후 실행
- phase = BEFORE_COMMIT: 커밋 직전에 실행(예외 발생 시 커밋 중단 가능)
참고로 현재 트랜잭션이 없으면 이벤트를 버리고, fallbackExecution=true 를 주면 트랜잭션이 없어도 실행됩니다.(단, 정말 필요한 경우에만 사용하세요)
2. 예제
Async에서 사용할 ThreadPoolTaskExecutor 등록
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "eventExecutor")
public ThreadPoolTaskExecutor eventExecutor() {
ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
ex.setCorePoolSize(4);
ex.setMaxPoolSize(8);
ex.setQueueCapacity(1000);
ex.setThreadNamePrefix("event-");
ex.initialize();
return ex;
}
}
💡 참고
@Async 규칙
- 유일한 TaskExecutor 타입 빈이 있으면 해당 빈을 쓴다.
- 여러 개의 TaskExecutor 타입 빈이 있을 경우 taskExecutor 이름의 빈을 찾아 사용한다.
- 둘 다 해당되지 않을 경우 SimpleAsyncTaskExecutor 로 동작한다.
❗️ 참고
스프링 부트(3.5 기준) : 부트가 자동으로 applicationTaskExecutor 빈을 만들어 @Async 의 기본 실행기로 쓴다.
예전(<=3.4)엔 taskExecutor 이름도 함께 제공했지만 3.5부터는 제공하지 않는다.
→ 기존에 이름으로 taskExecutor 를 참조하던 코드는 applicationTaskExecutor 로 바꾸거나 직접 빈을 정의해야 한다.
이벤트 정의
// 조회 이력 적재에 필요한 최소 식별자만 담는다.
// (리스너에서 불필요한 재조회 방지 → 성능/정합성에 유리)
public record ArticleViewedEvent(
Long userId, // 누가
Long articleId, // 무엇을
Instant occurredAt // 언제
) { }
서비스: Article 조회 → 이벤트 발행
@Service
@RequiredArgsConstructor
public class ArticleReadService {
private final ArticleRepository articleRepository;
private final ApplicationEventPublisher eventPublisher;
// readOnly 있어도 트랜잭션 경계가 열리므로 AFTER_COMMIT 리스너 동작 가능
@Transactional(readOnly = true)
public ArticleDetailDto readAndRecord(Long userId, Long articleId) {
// 1) Article 조회 (필요하면 조회수 증가 등 쓰기는 별도 서비스에서 처리 권장)
Article article = articleRepository.findById(articleId)
.orElseThrow(() -> new IllegalArgumentException("Article not found: " + articleId));
// 2) 조회 이력 이벤트 발행 (리스너는 커밋 성공 후 실행)
eventPublisher.publishEvent(new ArticleViewedEvent(
userId, article.getId(), Instant.now()
));
// 3) 조회 결과 반환 (이력 적재는 비동기로 진행)
return ArticleDetailDto.from(article);
}
}
- 이벤트 페이로드에 충분한 정보(userId, articleId, occurredAt)를 담아 리스너에서 DB 재조회를 최소화합니다.
- read-only 라우팅을 쓰는 경우라도, 리스너 메서드에 @Transactional만 붙이면 기본적으로 마스터로 쓰기가 라우팅되도록 구성하기 쉽습니다(프로젝트 라우팅 정책에 따름).
리스너: 커밋 후(AFTER_COMMIT) 조회 이력 저장
@Component
@EnableAsync // @Async 사용을 위한 설정
@RequiredArgsConstructor
public class ArticleViewHistoryListener {
private final ArticleViewHistoryRepository historyRepository;
@Async("eventExecutor") // 요청 스레드와 분리 → 응답 지연 최소화
@Transactional // 리스너 스레드에서 별도 트랜잭션을 열어 INSERT 수행
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onViewed(ArticleViewedEvent event) {
// 커밋 성공 후에만 호출되므로 정합성 확보
ArticleViewHistory history = ArticleViewHistory.of(
event.userId(), event.articleId(), event.occurredAt()
);
historyRepository.save(history);
}
}
- 이력 저장과 같이 성공 여부가 서비스 로직과 무관하다면 Async를 이용하여 응답 속도를 향상 시키는 것이 좋습니다.
3. 장점
- 커밋 인지(Commit-aware) 실행
기본 AFTER_COMMIT으로 DB 커밋 성공 후에만 리스너가 실행되어 부수효과(이력 적재, 캐시 무효화 등)가 트랜잭션 정합성을 따른다. - 요청 지연 최소화
리스너에 @Async를 더하면 커밋 후 별도 스레드에서 실행되어 API 응답 시간을 줄인다(특히 외부 I/O 분리 → Spring Boot 성능 최적화). - 관심사 분리·확장 용이
핵심 트랜잭션(쓰기/조회)과 후속 작업(로그/이력/알림)을 분리해 코드 가독성과 테스트 용이성↑. - 유연한 실행 시점
BEFORE_COMMIT, AFTER_ROLLBACK, AFTER_COMPLETION 등 상황 맞춤 실행이 가능(감사/검증/보상 로직에 유리). - JPA 도메인 이벤트와 궁합
@DomainEvents/@AfterDomainEventPublication과 결합해 풍부한 모델링과 내부 이벤트 흐름을 구성하기 좋다(JPA 활용법). - 간단한 도입 비용
메시지 브로커/Outbox 없이 프로세스 내부에서 시작할 수 있어 초기 적용 장벽이 낮다.
728x90
'Backend > Spring' 카테고리의 다른 글
[String] Transaction - rollback 속성 (0) | 2025.08.31 |
---|---|
[Spring] Transaction - 격리 수준(Isolation) (3) | 2025.08.17 |
[Spring] Transaction - 트랜잭션 전파(Transaction Propagation) (3) | 2025.08.17 |
[TDD] Test Double (0) | 2025.08.03 |
[Spring] @Value와 @ConfigurationProperties (0) | 2023.07.09 |