기존에는 NotificationService를 주입받아 적용했는데 서비스간의 의존성이 생기고 결합도가 높아서 이슈가 생길것이 염려되었습니다. 결국 관련해서 에러가 터지기도 했습니다. 의존성을 제거하기 위해 여러가지 방법을 찾아보았는데 EventListener가 있다는 사실을 알게 되었습니다. EventListener는 말 그대로 이벤트를 리스닝(?)하고 있다가 이벤트가 발생하면 처리하는 로직입니다.
서비스간의 강한 결합, 강한 의존성을 낮출수 있는 방법입니다.
@TransactionalEventListener
@TransactionEventListener를 사용하면, 트랜잭션 흐름에 따라 이벤트를 제어할 수 있습니다.
@TransactionalEventListener는 4가지 옵션이 있습니다.
- AFTER_COMMIT(default): 트랜잭션이 성공적으로 마무리(commit)이 되었을 때 이벤트를 실행합니다.
- AFTER_ROLLBACK: 트랜잭션이 rollback이 되었을 때 이벤트를 실행합니다.
- AFTER_COMPLETION: 트랜잭션이 마무리 되었을 때(commit or rollback) 이벤트를 실행합니다.
- BEFORE_COMMIT: 트랜잭션의 커밋 전에 이벤트를 실행합니다.
문제점,
- 이벤트 전달 시점을 트랜잭션 커밋 시점으로 설정한 경우 트랜잭션이 끝나 DB에 데이터를 저장하는 것이 불가능합니다.
- 이때 @Transactional(propagation = Propagation.REQUIRES_NEW)으로 새로운 트랜잭션을 생성시켜 데이터를 저장하는 방식으로 문제를 해결합니다.
NotificationListener 구현 (1)
@Component
@RequiredArgsConstructor
@Slf4j
public class NotificationListener {
private final NotificationService notificationService;
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Async
public void handleNotification(RequestNotificationDto requestNotificationDto){
notificationService.send(
requestNotificationDto.getReceiver(), requestNotificationDto.getSender(), requestNotificationDto.getNotificationType(),
requestNotificationDto.getContent(), requestNotificationDto.getType(), requestNotificationDto.getTypeId(),
requestNotificationDto.getPostId());
}
}
RequsetDto를 따로 만들어서 Event가 발생하면 받아오게 하였습니다.
BeanInitializationException
위의 핸들러를 실행시키자 다음과 같이 BeanInitializationException이 발생했습니다.
org.springframework.beans.factory.BeanInitializationException: Failed to process @EventListener annotation on bean with name 'notificationService'; nested exception is java.lang.IllegalStateException: Maximum one parameter is allowed for event listener method: public void com.bluehair.hanghaefinalproject.sse.service.NotificationService.send(com.bluehair.hanghaefinalproject.member.entity.Member,com.bluehair.hanghaefinalproject.member.entity.Member,com.bluehair.hanghaefinalproject.sse.entity.NotificationType,java.lang.String,com.bluehair.hanghaefinalproject.sse.entity.RedirectionType,java.lang.Long,java.lang.Long) at org.springframework.context.event.EventListenerMethodProcessor.afterSingletonsInstantiated(EventListenerMethodProcessor.java:157) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:974) ~[spring-beans-5.3.24.jar:5.3.24] at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:918) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.6.jar:2.7.6] at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) ~[spring-boot-2.7.6.jar:2.7.6] at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) ~[spring-boot-2.7.6.jar:2.7.6] at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) ~[spring-boot-2.7.6.jar:2.7.6] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[spring-boot-2.7.6.jar:2.7.6] at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[spring-boot-2.7.6.jar:2.7.6] at com.bluehair.hanghaefinalproject.HanghaeFinalProjectApplication.main(HanghaeFinalProjectApplication.java:13) ~[main/:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na] at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na] at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) ~[spring-boot-devtools-2.7.6.jar:2.7.6] Caused by: java.lang.IllegalStateException: Maximum one parameter is allowed for event listener method: public void com.bluehair.hanghaefinalproject.sse.service.NotificationService.send(com.bluehair.hanghaefinalproject.member.entity.Member,com.bluehair.hanghaefinalproject.member.entity.Member,com.bluehair.hanghaefinalproject.sse.entity.NotificationType,java.lang.String,com.bluehair.hanghaefinalproject.sse.entity.RedirectionType,java.lang.Long,java.lang.Long) at org.springframework.context.event.ApplicationListenerMethodAdapter.resolveDeclaredEventTypes(ApplicationListenerMethodAdapter.java:127) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.context.event.ApplicationListenerMethodAdapter.<init>(ApplicationListenerMethodAdapter.java:117) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.transaction.event.TransactionalApplicationListenerMethodAdapter.<init>(TransactionalApplicationListenerMethodAdapter.java:65) ~[spring-tx-5.3.24.jar:5.3.24] at org.springframework.transaction.event.TransactionalEventListenerFactory.createApplicationListener(TransactionalEventListenerFactory.java:56) ~[spring-tx-5.3.24.jar:5.3.24] at org.springframework.context.event.EventListenerMethodProcessor.processBean(EventListenerMethodProcessor.java:200) ~[spring-context-5.3.24.jar:5.3.24] at org.springframework.context.event.EventListenerMethodProcessor.afterSingletonsInstantiated(EventListenerMethodProcessor.java:154) ~[spring-context-5.3.24.jar:5.3.24] ... 15 common frames omitted
Maximum one parameter is allowed for event listener method
EventListener는 최대 하나의 parameter만을 가진다고 합니다. 보니까 Singleton으로 설정되어 있어서 그렇습니다. 다시 코드를 변경해서 parameter 하나만 보내주는 것으로 notificationService.send를 변경했습니다.
NotificationListener 구현 (2)
아아... 그게 아니었습니다.
@EventListener 어노테이션을 2군데 붙여놔서 그런거였습니다.
HandleNotification 메서드에만 붙어놔야 했는데 NotificationService안에도 붙여놔서 send 메서드에는 파라미터가 여러개여서 문제가 생긴거였습니다. send 메서드 위에 있던 @EventListener 어노테이션을 제거하니 제대로 작동이 되었습니다.
Service 구현
여러가지 서비스가 알림서비스를 이용하고 있었는데 게시글 좋아요 메서드 관련한 코드입니다.
이렇게 고쳤습니다.
우선 ApplicationEventPublisher를 가져옵니다.
private final ApplicationEventPublisher eventPublisher;
@Transactional
public PostLikeDto postLike(Long postId, Member member){
Post postliked = postRepository.findById(postId)
.orElseThrow(()-> new NotFoundException(LIKE, SERVICE, POST_NOT_FOUND, "Post ID : " + postId)
);
PostLikeCompositeKey postLikeCompositeKey
= new PostLikeCompositeKey(member.getId(), postliked.getId());
boolean likecheck;
Optional<PostLike> postLike= postLikeRepository.findByPostLikedIdAndMemberId(postliked.getId(), member.getId());
if(postLike.isPresent()){
postLikeRepository.deleteById(postLikeCompositeKey);
postliked.unLike();
postRepository.save(postliked);
likecheck = false;
return new PostLikeDto(likecheck, postliked.getLikeCount());
}
postLikeRepository.save(new PostLike(postLikeCompositeKey, member, postliked));
likecheck=true;
postliked.like();
postRepository.save(postliked);
Member postMember = memberRepository.findByNickname(postliked.getNickname())
.orElseThrow(() -> new NotFoundException(COMMENT, SERVICE, MEMBER_NOT_FOUND, "Nickname : " + postliked.getNickname()));
if(!postMember.getNickname().equals(member.getNickname())) {
String content = postliked.getTitle() + "을(를) " + member.getNickname() + "님이 좋아합니다.";
notify(postMember, member, NotificationType.POST_LIKED, content, RedirectionType.detail, postId, null);
}
return new PostLikeDto(likecheck, postliked.getLikeCount());
}
notify 메서드를 만들어서 event를 publish 합니다.
private void notify(Member postMember, Member sender, NotificationType notificationType,
String content, RedirectionType type, Long typeId, Long postId){
eventPublisher.publishEvent(new RequestNotificationDto(postMember,sender, notificationType,content,type, typeId, postId));
}
이제 notify가 실행되면서 Eventlistener가 NotificationService의 send 메서드를 실행해줍니다.
@Component
@RequiredArgsConstructor
@Slf4j
public class NotificationListener {
private final NotificationService notificationService;
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Async
public void handleNotification(RequestNotificationDto requestNotificationDto){
notificationService.send(requestNotificationDto.getReceiver(), requestNotificationDto.getSender(),requestNotificationDto.getNotificationType(),
requestNotificationDto.getContent(), requestNotificationDto.getType(), requestNotificationDto.getTypeId(), requestNotificationDto.getPostId());
log.info("EventListener has been operated. Sender Id: " + requestNotificationDto.getSender().getId() + "NotificationType: " +requestNotificationDto.getNotificationType());
}
}
참고 문서
'TIL' 카테고리의 다른 글
TIL WIL 최종프로젝트도 어느새 막바지 230129 (0) | 2023.01.29 |
---|---|
TIL 에러 넘어 에러 (feat. 객체직렬화) 230128 (0) | 2023.01.29 |
TIL @Async 비동기 동기 230126 (0) | 2023.01.27 |
TIL SSE 에러 트러블슈팅 230125 (0) | 2023.01.26 |
TIL SSE 에러 230124 (0) | 2023.01.25 |