문제 상황
예를 들어, 유저가 회원가입 완료시 쿠폰을 지급해야 하는 정책이 있다고 가정해보자
@Transactional
@Service
class UserRegisterService(
private val couponService: CouponService
) {
fun register(email: String, password: String, name: String) {
val user = User(email, password, name)
/***
* 회원 가입 로직
*/
couponService.issueCoupon(user.id)
}
}
만약 회원가입까지는 성공 했는데 쿠폰 서비스에서 쿠폰을 발행하는 과정에 예외가 발생하면 어떻게 될까?
아마 트랜잭션이 롤백되어 유저가 회원가입을 하지 못할 것이다.
이는 UserRegisterService에서 회원가입 쿠폰을 발급하는 책임까지 지고 있기 때문이다.
그리고 유저가 회원가입을 하는것이 중요한거지 쿠폰이 발급되지 않았다면 수동으로 발급할 수 있도록 하는것이 비즈니스 측면에서 훨씬 낫다.
Event를 사용해서 로직 분리하기
스프링에서는 ApplicationEventPublisher를 사용해 사용자가 원하는 이벤트를 발행하고, 수신해서 처리할 수 있다.
컴포넌트간에 이벤트를 사용해서 통신한다면 아래와 같은 장점이 있다.
- 컴포넌트간의 결합을 약하게 만들 수 있다.
- 장애가 전파되는걸 최소화 할 수 있다.
- 단일 책임 원칙을 지킬 수 있다.
data class UserRegisteredEvent(
val userId: Long
)
이벤트는 POJO로 만들어주면 된다.
@Transactional
@Service
class UserRegisterService(
private val applicationEventPublisher: ApplicationEventPublisher
) {
fun register(email: String, password: String, name: String) {
val user = User(email, password, name)
/***
* 회원 가입 로직
*/
applicationEventPublisher.publishEvent(UserRegisteredEvent(user.id))
}
}
쿠폰 지급 로직을 제거하고, 유저가 등록되었다는 뜻의 UserRegisterEvent를 발행해준다.
이렇게 개선하면 UserRegisterService는 단일 책임 원칙을 지키게 된다.
@Component
class UserRegisteredEventListener {
@TransactionalEventListener
fun handleUserRegisteredEvent(userRegisteredEvent: UserRegisteredEvent) {
println("userRegisteredEvent = ${userRegisteredEvent}")
}
}
@TransactionalEventListener
로 이벤트 리스너를 선언해주었다. 이렇게 하면 메인 로직의 트랜잭션이 커밋될때까지 기다린다음, 이벤트를 핸들링 할 수 있다. 그렇게 되면 handleUserRegisteredEvent
내부의 로직이 실패해서 롤백하더라도, 유저가 회원가입한 건 롤백되지 않는다.
이 리스너에서 회원 가입 후 실행할 로직을 작성하면 된다. 만약 쿠폰이 아니라 포인트를 지급해주는걸로 정책이 변경되어도 UserRegisterService는 전혀 수정할 필요가 없다.
@TransactionalEventListener는 이벤트 리스너가 언제 실행되는지 설정하는 phase 속성이 있는데, 기본적으로 커밋 완료 후인 AFTER_COMMIT으로 동작하게 된다. 그 외에도 커밋하기전(BEFORE_COMMIT), 커밋 또는 롤백 완료 후(AFTER_COMPLETION), 롤백 완료 후(AFTER_ROLLBACK) 옵션이 있다.
이벤트를 비동기로 처리
사실 이벤트 리스너는 기본적으로 동기적으로 작동한다. 실제로 스레드 이름을 print하면서 테스트해보면 동일한 스레드를 사용하는걸 확인할 수 있다.
fun register(email: String, password: String, name: String) {
val user = User(email, password, name)
/***
* 회원 가입 로직
*/
println("Thread.currentThread().name = ${Thread.currentThread().name}")
applicationEventPublisher.publishEvent(UserRegisteredEvent(user.id))
}
@Component
class UserRegisteredEventListener {
@TransactionalEventListener
fun handleUserRegisteredEvent(userRegisteredEvent: UserRegisteredEvent) {
println("EventListener Thread.currentThread().name = ${Thread.currentThread().name}")
Thread.sleep(3000)
}
}
@Test
fun 이벤트_리스너_동기_테스트() {
userRegisterService.register("gkscodus11@naver.com", "123456", "test")
}
따라서 이벤트 리스너를 메인 로직과 상관없이 새로운 스레드에서 실행하고 싶다면 @Async
어노테이션을 추가로 붙여줘야한다.
@Component
class UserRegisteredEventListener {
@Async
@TransactionalEventListener
fun handleUserRegisteredEvent(userRegisteredEvent: UserRegisteredEvent) {
println("EventListener Thread.currentThread().name = ${Thread.currentThread().name}")
Thread.sleep(3000)
}
}
만약, 비동기가 아니라 동기적으로 이벤트 리스너가 실행되도록 한 경우 위의 예제와 같이 시간이 오래 걸리는 작업이 있다면 메인로직에도 영향이 가게된다.
따라서 본인 판단하에 동기적으로 이벤트 리스너 로직을 실행해야하는 경우가 아니라면 비동기로 실행되도록 하는것이 좋다고 생각한다.
ApplicationEventPublisher 내부 동작
ApplicationEventPublisher로 이벤트를 발행하면 리스너로 등록된 모든 리스너들에게 멀티캐스트 방식으로 이벤트를 보내게된다.
AbstractApplicationContext에서 publishEvent
메소드를 구현한 것을 확인할 수 있다.
protected void publishEvent(Object event, @Nullable ResolvableType typeHint) {
...
} else if (this.applicationEventMulticaster != null) {
this.applicationEventMulticaster.multicastEvent((ApplicationEvent)applicationEvent, eventType);
}
...
}
여기서 좀 더 들어가, applicationEventMulticaster의 기본 구현체인 SimpleApplicationMulticaster를 확인해보면 리스너 그룹으로 멀티캐스트를 하는 것을 확인할 수 있다.
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = eventType != null ? eventType : ResolvableType.forInstance(event);
Executor executor = this.getTaskExecutor();
Iterator var5 = this.getApplicationListeners(event, type).iterator();
while(true) {
while(var5.hasNext()) {
ApplicationListener<?> listener = (ApplicationListener)var5.next();
if (executor != null && listener.supportsAsyncExecution()) {
try {
executor.execute(() -> {
this.invokeListener(listener, event);
});
} catch (RejectedExecutionException var8) {
this.invokeListener(listener, event);
}
} else {
this.invokeListener(listener, event);
}
}
return;
}
}
더 추가적으로 알고 싶다면 AbstractApplicationEventMulticaster를 찾아보는것도 좋을 것 같다.
참고
'Java,Kotlin,SpringBoot' 카테고리의 다른 글
[SpringBoot] AbstractAggregateRoot로 DomainEvent 발행하기 (3) | 2024.11.27 |
---|---|
[SpringBoot] @Async로 비동기 작업 처리하기 (1) | 2024.09.19 |
[SpringBoot] JPA에서 Enum 값 다루기(AttributeConverter) (3) | 2024.09.13 |
[Java] ExecutorService와 Future (2) | 2024.08.20 |
[SpringBoot] h2 테이블 이름 upper_case 해제하기 (1) | 2024.08.10 |