Spring

[Spring] @EventListener를 통해 도메인 간 결합 끊기

kkang._.h00n 2026. 2. 18. 00:00

개요

서비스가 커질수록 하나의 비즈니스 동작의 여러 부수 효과를 동반하게 된다.

예를 들어 DDD로 설계된 어플리케이션에서 회원가입을 하면 환영 이메일 발송 및 쿠폰 지급 등 회원 관련 서비스에서 다양한 도메인이 경계를 넘나들게 되어 결합성이 높아지게 된다. Spring Event는 결합성을 낮추고 복잡성을 해결하기에 가장 간단하면서도 효율적인 방법 중 하나이다.

 

 

Spring Event 구조와 동작

Spring Event는 애플리케이션 내부의 PUB/SUB 패턴으로 동작한다. 
이벤트 발행을 통해 도메인의 상태의 변경을 알리고, 해당 이벤트에 대한 구독자가 이벤트에 반응하는 구조이다.

 

출처 : https://medium.com/javarevisited/do-you-know-about-eventlistener-and-applicationeventpublisher-in-spring-boot-259b5e74312a

Spring Event의 구조는 이벤트 객체, Publisher, Listener 세 가지로 구성되어 있다.

이벤트 객체는 도메인 상태 변경을 담는 POJO 객체이다. 

Publisher는 이벤트를 발행하며, Listener는 구독한 이벤트가 발생할 경우 관련된 로직을 처리한다.

멀티 캐스팅 방식으로 동작하기에, 하나의 이벤트에 여러 Listener가 각각 반응할 수있다.

 

ApplicationEventPublisher - 이벤트 발행

@Service
class InquiryService(
    private val inquiryJpaRepository: InquiryJpaRepository,
    private val adminRepository: AdminRepository,
    
    //Spring에서 제공하는 ApplicationEventPublisher
    private val eventPublisher: ApplicationEventPublisher
) {

    @Transactional
    fun addInquiry(
        name: String,
        phoneNumber: String,
        content: String
    ) {
    	//문의 저장
        inquiryJpaRepository.save(
            Inquiry(
                name = name,
                phoneNumber = phoneNumber,
                content = content
            )
        )
		
        //관리자 이메일 조회
        val adminEmails = adminRepository.findAll()
            .map { it.email }

        //이벤트 전달
        eventPublisher.publishEvent(
            EmailSendEvent(
                adminEmails,
                "문의가 전송되었습니다",
                getInquiryMail(name, phoneNumber, content)
            )
        )
    }
}

고객이 문의를 하면, 문의 내역을 관리자에 이메일로 전달하는 로직이다. 

관리자에게 이메일을 전달하는 로직은 문의 도메인과 관련이 없기에 ApplicationEventPublisher를 통해 이벤트 객체를 전달하여 이메일 발송 관련 부가 로직이 침투하지 않도록 하였다.

 

이벤트 객체

data class EmailSendEvent(
    val emails: List<String>,
    val subject: String,
    val text: String
)

고객이 문의를 전송하였을 때, 발송되는 이메일 객체는 위와 같다.

 

@EventListener - 이벤트 수신 및 처리

@Component
class EmailEventSubscriber(
    private val emailClient: EmailClient
) {

    //EmailSendEvent를 수신하는 @EventListener
    @EventListener(EmailSendEvent::class)
    fun emailEventListener(event: EmailSendEvent) {

        event.emails.forEach {
            emailClient.sendHtmlEmail(it, event.subject, event.text)
        }
    }

}

@EventListener를 통해 특정 이벤트 객체에 대한 Listener를 등록할 수 있다.

 

@EventListener는 이벤트 발생 시점에 같은 스레드에서 동기적으로 실행된다. 이 뜻은 단순히 @EventListener만 사용할 경우, Listener의 로직은 같은 트랜잭션에서 실행되며, 예외 또한 전파된다는 것이다.

 

실제로 위 로직은 사용자가 문의를 등록할 경우 이메일을 동기적으로 발송하므로, 사용자는 이메일 발송 시간까지 기다려야하는 불필요한 경험을 하게 된다. 더불어 이메일 발송 중 예외가 발생한다면, 문의를 등록하는 로직까지 불필요하게 롤백된다.

 

위와 같이 부가 로직이 핵심 로직에 영향을 줄 필요가 없는 상황이라면, 이벤트 처리를 비동기로 처리할 수 있을 것이다.

 

 

@Async를 통해 이벤트를 비동기로 처리하기

@Component
class EmailEventSubscriber(
    private val emailClient: EmailClient
) {

    //@Async를 통해 비동기 처리
    @Async("asyncEventTask")
    @EventListener(EmailSendEvent::class)
    fun emailEventListener(event: EmailSendEvent) {

        event.emails.forEach {
            emailClient.sendHtmlEmail(it, event.subject, event.text)
        }
    }

}

 

 

@Async를 통해 Listener에서 이벤트를 비동기적으로 처리할 수 있고, 이메일 발송을 별도의 스레드에서 처리하여 API의 처리 시간을 줄일 수 있었다. 

더불어 이메일 발송이 실패하더라도 핵심 로직인 문의 등록은 영향을 받지 않게 되었다.

 

 

@TransactionalEventListener

@Component
class EmailEventSubscriber(
    private val emailClient: EmailClient
) {

    @Async("asyncEventTask")
    //@TransactionalEventListener로 이벤트 실행 시점을 지정
    @TransactionalEventListener(EmailSendEvent::class, phase = TransactionPhase.AFTER_COMMIT)    
    fun emailEventListener(event: EmailSendEvent) {

        event.emails.forEach {
            emailClient.sendHtmlEmail(it, event.subject, event.text)
        }
    }

}

@TransactionalEventListener를 통해 이벤트 처리를 특정 트랜잭션 시점에 지정하여 실행하도록 할 수 있다. 주의할 점은 트랜잭션 없이 이벤트를 발행할 경우, 해당 Listener는 기본적으로 동작하지 않는다. fallbackExecution = true 옵션을 통해 트랜잭션이 없는 상황에도 실행되도록 할 수 있다. 

 

설정할 수 있는 시점은 아래와 같다.

  • BEFORE_COMMIT - 트랜잭션 commit 직전
  • AFTER_COMMIT - 트랜잭션 commit 이후 (기본값)
  • AFTER_ROLLBACK - 트랜잭션 rollback 이후
  • AFTER_COMPLETION - 트랜잭션 완료 후 (commit 혹은 rollback)

 

 

정리

  1. @EventListener - 이벤트가 동기적으로, 같은 트랜잭션 내에서 실행되어야 할 경우
  2. @TransactionalEventListener - 이벤트 처리 시점이 트랜잭션 처리 시점에 따라 달라야 할 경우
  3. @Async + @EventListener or @TransactionalEventListener - 이벤트가 핵심 로직과 연관이 없어서 비동기적으로 실행되어야 할 경우 

 

해당 방법은 Spring Application 내에서 동작하는 이벤트 처리 시스템이기 때문에, 서버 어플리케이션 재시작 혹은 장애 발생 시 이벤트가 유실될 수 있다. 또한 분산 환경일 경우에는 kafka와 같은 메시지 브로커 도입을 고려하여야 한다.