- ApplicationEventPublisher
독립된 클래스 A, B가 있고, 비즈니스 로직상 A의 메소드 호출 -> B의 메소드 호출이 선형적으로 일어나야 할 때
event를 이용하면 두 클래스간의 결합도를 낮출 수 있습니다.
@FunctionalInterface
public interface ApplicationEventPublisher {
default void publishEvent(ApplicationEvent event) {
publishEvent((Object) event);
}
// ..
스프링에서는 이벤트 publishing 을 위해서 ApplicationEventPublisher라는 인터페이스를 제공합니다.
- 예제
유저가 어떠한 정책에 동의하고, 동의 시 알림이 생성 되는 로직이 있다고 가정해보겠습니다.
class PolicyAgreedEvent(
val memberId: Long
): ApplicationEvent(memberId)
먼저, ApplicationEventPublisher의
publishEvent(applicationEvent : ApplicationEvent)에 파라미터로 넘길 event클래스를 생성합니다.
(ApplicationEvent는 스프링에서 제공하는 추상 클래스입니다.)
publishEvent
@Transactional
override fun agreeToPolicy(command: agreementCommand.Create, memberId: Long) {
// 유저 조회
val member = memberReader.getOrThrow(memberId)
member.agreeToPolicy()
//정책 조회
val policy = policyReader.get(command.policyId)
//동의 내역 저장
val agreement = command.toEntity(policy, memberId)
policyWriter.agree(agreement)
//알림 생성 이벤트 발행
applicationEventPublisher.publishEvent(PolicyAgreedEvent(memberId))
}
이벤트 발행을 원하는 로직에서, publishEvent()를 호출하여 이벤트를 발행합니다.
agreeToPolicy 메소드에서는 알림 생성과 관련된 코드 없이 정책 동의와 관련된 로직과 이벤트를 발행시키는 것까지만 책임을 가지며,
이를 통해 정책 서비스 - 알림 생성 서비스 간의 결합도를 낮출 수 있습니다.
@EventListener
@Component
class NotificationEventListener(
private val memberReader: MemberReader,
private val notificationFactory: NotificationFactory,
) {
@EventListener(ReferenceCheckCompletedEvent::class)
fun policyAgreedListener(event: PolicyAgreedEvent){
val member = memberReeader.getOrThrow(event.memberId)
// .. 알림 생성 로직.. //
notificationFactory.writeAgreeNotification(notification)
}
이벤트 subscribe를 위해서, Listener를 만들어줍니다. (스프링 빈으로 등록하지 않으면 동작하지 않습니다.)
스프링에서 제공하는 @EventListener를 통해서 타겟 이벤트를 subscribe할 수 있게 됩니다.
이때, 이벤트의 발행 ~ 이벤트 리스너 내부의 메소드 마무리까지 일련의 로직은 동기적으로 진행 됩니다.
따라서 이벤트 리스너가 무거운 작업을 수행하게 된다면 전체 로직의 응답속도가 느려질 수 있습니다.
-> @Async등 비동기적으로 수행될 방법을 고려해 보셔야 합니다.
또한, 이벤트 리스너 내부의 로직과, 이벤트 publisher의 로직이 한 트랜잭션으로 묶이기 때문에
이벤트 리스너 내부의 로직에서 예외가 발생한다면 해당 트랜잭션은 롤백 처리가 됩니다.
(try - catch 를 사용하더라도 트랜잭션 자체에 롤백마크가 붙기 때문에 롤백처리를 막을 수 없습니다.)
-> 독립된 트랜잭션의 진행을 원한다면, EventListener측 메소드에
@Transactional(propagation = Propagation.REQUIRES_NEW) 를 붙여서 해결할 수 있습니다.
@TransactionalEventListener
위의 예제에서 알림이라는 개념이 내부 인프라를 이용하는 값이라면 (db 저장) 롤백처리가 된다면 알림 데이터 생성이 안되고 끝나겠지만,
만약 리스너 내부의 로직이 외부의 api를 사용하여 메일을 보낸다는 등의 외부 인프라를 이용하는 상황이라면 (또는 s3 업로드 등..)
이미 발송된 메일 (또는 이미 올라간 이미지 파일)에 대한 롤백처리는 이루어지지 않습니다.
스프링은 @TransactionalEventListener라는 어노테이션을 이용하여 publisher의 트랜잭션 상태를 기준으로 이벤트를 발행시킬 수 있게 합니다.
phase 속성에는 다음과 같은 값들이 있습니다.
- AFTER_COMMIT : 트랜잭션이 성공적으로 commit 되었을 때 이벤트 실행 (default)
- AFTER_ROLLBACK: 트랜잭션이 rollback 되었을 때 이벤트 실행
- AFTER_COMPLETION: 트랜잭션이 마무리 되었을 때(commit or rollback) 이벤트 실행
- BEFORE_COMMIT: 트랜잭션의 커밋 전에 이벤트 실행
default값을 보시면 알 수 있듯이, 주로 AFTER_COMMIT값이 많이 쓰입니다.
해당 속성을 이용하여 publisher단의 트랜잭션이 커밋되지 않으면, 이벤트 리스너의 동작이 실행되지 않도록 보장할 수 있습니다.
주의 점
이 글을 작성하게 된 이유입니다.
만약 이벤트리스너 내부에서 insert등의 로직이 존재할경우, @EventListener를 사용했다면 저장이 정상동작하지만
@TransactionalEventListener의 경우 insert가 제대로 수행되지 않을 수 있습니다.
@Transactional
fun order() {
val initOrder = Order(
memberId = 2L,
orderState = OrderState.PAYMENT_WAITING,
totalPrice = 20000
)
val order = orderWriter.write(initOrder)
applicationEventPublisher.publishEvent(OrderedEvent(order.id!!))
}
예를들어, 정말 간단하게 order 데이터가 생성되고 이벤트가 발행되어 billing데이터가 생성되는 상황을 가정해보겠습니다.
@Component
class BillingHandler(
private val billingWriter: BillingWriter,
) {
@TransactionalEventListener
fun orderCreatedListener(event: OrderedEvent){
val initBilling = Billing(
orderId = event.orderId
)
billingWriter.write(initBilling)
}
}
리스너에서는 Order의 정보를 받아서 billing정보를 저장하려고 하는 상황입니다.
주문 데이터와 결제 데이터 모두의 저장을 원했지만, 해당 로직을 실행하면
다음과 같이 billing의 insert는 실행이 되지 않고 order 데이터만이 저장됩니다.
그 이유는 하나의 트랜잭션에서는 한번의 커밋만 발생할 수 있기 때문에, 이미 커밋이 발생한 트랜잭션 내에서 또 다시 커밋을 시도해도
쿼리가 날아가지 않기 때문입니다.
따라서 @TransactionalEventListener + AFTER_COMMIT 속성을 사용할 경우, 원하는 저장로직을 수행하기 위해서는 트랜잭션의 분리가 이루어져야합니다.
리스너의 메소드에 @Transactional(propagation = Propagation.REQUIRES_NEW) 를 추가하면 정상적으로 insert쿼리가 날아가는 것을 확인할 수 있습니다.
요약
- 스프링에서 제공하는 ApplicationEventPublisher를 이용하면 간단하게 event를 Pub/ Sub 하는 구조를 만들 수 있습니다.
- 해당 인터페이스 / 어노테이션 사용 시에는 퍼블리셔와 리스너의 동작이 동기적으로 이루어진다는 점을 고려해야 합니다.
- 트랜잭션 설정을 적절히 하여야 원하는 내용의 커밋 / 롤백 형태로 비즈니스 로직 작성을 할 수 있습니다.
'스프링' 카테고리의 다른 글
동적 빈주입 (0) | 2024.10.25 |
---|---|
스프링으로 외부 api와 소통하기 (0) | 2024.09.05 |
스프링 고급편 내용정리 <템플릿 메서드 패턴 / 전략 패턴> (0) | 2022.02.01 |
스프링 고급편 내용정리 <쓰레드 로컬> (0) | 2022.01.20 |
스프링 - 컨버터 / 포매터 (0) | 2021.12.17 |