1) 이벤트 정의 (POJO/record 권장)
// src/main/java/com/example/user/event/UserSignedUpEvent.java
package com.example.user.event;
import java.time.Instant;
public record UserSignedUpEvent(Long userId, String email, Instant occurredAt) {}
Spring 6+/Boot 3에서는
ApplicationEvent를 상속할 필요가 없습니다. 평범한 POJO/record가 좋아요.
2) 이벤트 발행 (raise)
// src/main/java/com/example/user/service/SignUpService.java
package com.example.user.service;
import com.example.user.event.UserSignedUpEvent;
import com.example.user.model.User;
import com.example.user.repo.UserRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
@Service
public class SignUpService {
private final UserRepository userRepository;
private final ApplicationEventPublisher publisher;
public SignUpService(UserRepository userRepository, ApplicationEventPublisher publisher) {
this.userRepository = userRepository;
this.publisher = publisher;
}
@Transactional
public Long signUp(String email, String name) {
User user = userRepository.save(new User(email, name)); // JPA 엔티티 저장 가정
// 이벤트 발행 (트랜잭션 안에서)
publisher.publishEvent(new UserSignedUpEvent(user.getId(), user.getEmail(), Instant.now()));
return user.getId();
}
}
3) 동기 리스너 (기본)
// src/main/java/com/example/user/listener/AuditLogListener.java
package com.example.user.listener;
import com.example.user.event.UserSignedUpEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
public class AuditLogListener {
private static final Logger log = LoggerFactory.getLogger(AuditLogListener.class);
@Order(0) // 여러 리스너가 있을 때 순서 제어 가능 (낮을수록 먼저)
@EventListener
public void onUserSignedUp(UserSignedUpEvent event) {
log.info("AUDIT: userId={}, email={}, at={}", event.userId(), event.email(), event.occurredAt());
}
}
기본은 “동기 실행”이라 발행 시점에 즉시 호출됩니다.
4) 비동기 리스너 (@Async)
// src/main/java/com/example/config/AsyncConfig.java
package com.example.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
@Configuration
@EnableAsync
public class AsyncConfig { }
// src/main/java/com/example/user/listener/WelcomeEmailListener.java
package com.example.user.listener;
import com.example.user.event.UserSignedUpEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class WelcomeEmailListener {
private static final Logger log = LoggerFactory.getLogger(WelcomeEmailListener.class);
@Async
@EventListener(condition = "#event.email.endsWith('co.kr')") // 조건부 처리도 가능
public void sendWelcomeEmail(UserSignedUpEvent event) {
log.info("SEND EMAIL: welcome -> {}", event.email());
// 실제 메일 전송 로직 ...
}
}
@EnableAsync+@Async로 발행자 스레드와 분리합니다. 실패가 본 트랜잭션에 영향을 주지 않도록 하고 싶을 때 유용.
5) 트랜잭션 완료 후 처리 (@TransactionalEventListener)
DB 커밋이 성공한 뒤에만 실행하고 싶다면:
// src/main/java/com/example/user/listener/PostCommitListener.java
package com.example.user.listener;
import com.example.user.event.UserSignedUpEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Component
public class PostCommitListener {
private static final Logger log = LoggerFactory.getLogger(PostCommitListener.class);
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterCommit(UserSignedUpEvent event) {
log.info("AFTER_COMMIT: create coupon for userId={}", event.userId());
// 쿠폰 발급, 외부 연동 등 "커밋 보장"이 필요할 때
}
}
같은
publishEvent(...)호출이라도, 이 리스너는 트랜잭션 커밋 이후에 실행됩니다. 반대로 롤백 시 특정 처리를 하고 싶으면phase = TransactionPhase.AFTER_ROLLBACK도 가능.
6) 엔티티/리포지토리 예시 (참고용)
// src/main/java/com/example/user/model/User.java
package com.example.user.model;
import jakarta.persistence.*;
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String email;
private String name;
protected User() {}
public User(String email, String name) { this.email = email; this.name = name; }
public Long getId() { return id; }
public String getEmail() { return email; }
public String getName() { return name; }
}
// src/main/java/com/example/user/repo/UserRepository.java
package com.example.user.repo;
import com.example.user.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> { }
사용 팁 요약
- 이벤트 클래스는 POJO/record로 가볍게.
- 빠른 응답이 필요한 비즈니스 로직 ↔ 부가 작업(메일/알림/로그)을 느슨하게 분리.
- 트랜잭션에 의존하는 후속 작업은
@TransactionalEventListener(AFTER_COMMIT). - 부하/지연이 큰 작업은
@Async로 분리(필요 시 전용TaskExecutor설정). - 조건부 실행(
condition)과 순서(@Order)로 복잡성 제어.