-
Spring Event 사용하기프로젝트/선착순 쿠폰 발급 시스템 2023. 5. 30. 00:01728x90
개요
이벤트를 사용하는 이유는 무엇일까요?
개인적으로 생각했을 때 가장 주된 이유는 서비스 간의 의존성을 줄이기 위해서입니다.
서비스 의존성을 줄이기
예를 들어 회원가입으로 보았을 때, 회원가입을 수행하고 사용자에게 회원가입 성공 알림을 전송하거나, 신규가입 쿠폰을 할당한다고 가정해 보겠습니다.
이때 알림에서 장애가 발생하거나, 신규가입 쿠폰할당 과정에서 오류가 난다면 회원가입도 같이 실패하게 됩니다.
하지만 회원가입은 성공하였는데 회원가입이 끝나고 수행되는 그 외의 작업들로 회원가입이 실패하면 안 됩니다.
이벤트 발행하기
@Service class SignUpService( private val memberRepository: MemberRepository, private val applicationEventPublisher: ApplicationEventPublisher, ) : SignUpUseCase { override fun signUp(command: SignUpCommand) { val member = Member( id = command.memberId, password = command.password, nickName = command.nickName, fullName = command.fullName, ) memberRepository.save(member) applicationEventPublisher.publishEvent( SignUpEvent( memberId = member.id, ) ) } } data class SignUpEvent( val memberId: String, )
Spring Framework 4.2 이전 버전을 사용한다면 ApplicationEvent를 확장해야 하지만 4.2 버전부터는 SignUpEvent처럼 사용할 수 있습니다.
단지 ApplicationEventPublisher를 주입받고 publishEvent메서드를 통해 event를 전송하면 됩니다.
ApplicationEventPublisher에 들어가 보면 2가지 메서드가 존재합니다.
default void publishEvent(ApplicationEvent event) { publishEvent((Object) event); }
//Since:4.2 void publishEvent(Object event);
since 4.2부터는 지정된 이벤트가 ApplicationEvent가 아닌 경우에 Object 타입을 인자로 받는 publishEvent 메서드가 실행됩니다.
발행된 이벤트 사용하기
@Component class SignUpEventListener { @EventListener fun sendMail(signUpEvent: SignUpEvent) { println("${signUpEvent}님 에게 메일을 전송합니다") } @EventListener fun sendPush(signUpEvent: SignUpEvent){ println("${signUpEvent}님 에게 푸시 메시지를 전송합니다") } }
사용할 때도 단지 @EventListener를 붙여주기만 하면 됩니다.
@EventListner를 타고 들어가 보면 다음과 같은 설명이 있습니다.
메서드를 Application Event Listener로 사용하게 하는 어노테이션이다.
여러 이벤트 유형을 지원하는 경우, 클래스 속성을 사용하여 하나 이상의 지원되는 이벤트 유형을 참조할 수 있다.
이벤트 리스터에서 던진 checked Exception은 UndeclaredThrowableException으로 래핑 됩니다.
@Async와 함께 사용할 수 있지만 호출자에게 예외가 전파되지 않음을 유의해야 합니다.
특정 이벤트에 대한 리스너를 호출할 순서를 @Order 어노테이션으로 정의할 수 있습니다.이벤트가 발행, 구독되는지 테스트해 보기
HTTP를 만들어서 Controller를 호출해 보면 다음과 같은 메시지가 출력되어 정상적으로 이벤트가 수신되는지 확인할 수 있습니다.
@Transactional과 이벤트 구독, 발행리스터에 다음과 같은 코드를 추가합니다.
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) fun afterTransactionRollback(signUpEvent: SignUpEvent) { println("Transactional이 rollback된 후에 호출됩니다.") } @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) fun afterTransactionCommit(signUpEvent: SignUpEvent) { println("Transactional이 commit된 후 호출됩니다.") } @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION) fun afterTransaction(signUpEvent: SignUpEvent) { println("Transactional이 끝난후에 호출됩니다.") }
트랜잭션이 완료된 후 수행하도록 하는 이벤트리스터입니다.
@TransactioanlEventListener의 Document에는 다음과 같이 적혀있습니다.
이벤트가 활성 트랜잭션 내에서 게시되지 않은 경우, fallbackExecution 플래그가 명시적으로 설정되지 않는 한 이벤트가 삭제됩니다. 트랜잭션이 실행 중인 경우 이벤트는 해당 트랜잭션 단계에 따라 처리됩니다.
모든 작업을 수행하고 의도적으로 예외를 던지면 이벤트 발행은 어떻게 될까요?
override fun signUp(command: SignUpCommand) { val member = Member( id = command.memberId, password = command.password, nickName = command.nickName, fullName = command.fullName, ) memberRepository.save(member) applicationEventPublisher.publishEvent( SignUpEvent( memberId = member.id, ) ) throw IllegalStateException("signUp시 예외 발생") }
@Trasncational 어노테이션이 없는 경우
이벤트가 발행되어 버리고 예외가 발생하게 됩니다..
이렇다면 예외가 발생하는 지점에 따라 다르겠지만 회원가입이 실패하게 되더라도 메일, 푸시 메시지는 발송되는 참사가 발생하게 됩니다.
다만 위의 문서에 적혀있듯이 @Transcational이 걸려있지 않고 fallbackException 플래그가 명시적으로 적용되지 않아 @TranscationalEventListener 메서드는 호출되지 않았습니다.
@Transcational어노테이션이 붙어있는 경우
이벤트가 4개가 호출되었습니다.
rollback 되었을 때, 트랜잭션이 끝났을 때 이벤트가 같이 호출되었으며, @EventLisnter가 달려있는 2개의 메서드도 모두 호출되었습니다.
단, 다음 메서드는 호출되지 않았습니다.
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) fun afterTransactionCommit(signUpEvent: SignUpEvent) { println("Transactional이 commit된 후 호출됩니다.") }
따라서 예외가 발생했을 때를 고려하여 트랜잭션이 끝난 후에 호출해야 하는 경우에는 @TranscationalEventListner의 AFTER_COMMIT 옵션을 활용해야 합니다!
TransactionEventListener + DB 저장 테스트
@Component class TransactionEventListener( private val testRepository: TestRepository, ) { @TransactionalEventListener fun afterTransactionCommit(transactionEventListenerEvent: TransactionEventListenerEvent){ testRepository.save(TestDTO()) } }
기존 트랜잭션을 이어받아서 그대로 Repository에 저장을 수행하면 어떻게 될까요?
해당 정보가 DB에 반영되지 않습니다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
기존의 트랜잭션에서 이미 commit이 일어났기 때문에 새롭게 데이터를 저장하려면 트랜잭션 전파 속성을 위와 같이 선언해주어야 합니다.
해당 설정은 기존 트랜잭션 이외에 새로운 트랜잭션을 선언한다는 것을 의미합니다.
public static void invokeAfterCompletion(@Nullable List<TransactionSynchronization> synchronizations, int completionStatus) { if (synchronizations != null) { for (TransactionSynchronization synchronization : synchronizations) { try { synchronization.afterCompletion(completionStatus); } catch (Throwable ex) { logger.debug("TransactionSynchronization.afterCompletion threw exception", ex); } } } }
또한 @TransactionEventListener에서 예외가 발생하더라도 TransactionSynchronizationUtils클래스의 invokeAfterCompletion 메서드가 예외를 잡아버려 상위로 예외가 전파되지 않습니다.
또한 logging level이 debug가 아니라면 예외가 확인되지도 않습니다.
반면 @Async를 붙여준 경우에는 SimpleAsyncUncaughtExceptionHandler 클래스가 에러로그를 출력해주고 예외가 확인됩니다.
Spring Event를 사용할 때 주의사항
- 이벤트 리스터가 여러 개 있는 경우 이벤트 리스터나 호출되는 순서를 보장하지 않습니다.
- @Async와 보통 같이 사용하게 되면 비동기에 대한 이해가 필요합니다.
- 트랜잭션의 롤백에 대해 주의깊게 처리해야 합니다 (EventListner와 TranascationalEventListner의 차이 이해)
- 이벤트를 사용할때는 코드가 파편화되어 유지보수성이 떨어지게 되며 다음과 같이 보완할 수 있습니다.
- 명확한 네이밍 규칙 사용
- Event와 Lisnter 문서화
- 중앙 집중식 이벤트 버스 사용
- Event와 Lisnter 테스트
참고자료
https://www.baeldung.com/spring-events
https://wildeveloperetrain.tistory.com/246
'프로젝트 > 선착순 쿠폰 발급 시스템' 카테고리의 다른 글
선착순 쿠폰 발급 시스템 성능테스트 (0) 2023.06.18 RedisTemplate으로 Set 자료구조 사용하기 (0) 2023.06.09 Kotlin JPA Update Query 작성하기 (0) 2023.05.28 Kotlin JPA 양방향 연관관계 매핑 (0) 2023.05.25 Kotlin Jpa Auditing - Entity 수정, 생성기간 저장 (0) 2023.05.24