
@Transactional
public void deleteReservation(Long reservationId){
User user = userUtils.getUserFromSecurityContext();
Reservation reservation = queryReservation(reservationId);
reservation.validUserIsHost(user.getId());
reservation.validPastReservation();
notificationUtils.sendNotification(user, reservation,
TitleMessage.RESERVATION_DELETE, ContentMessage.RESERVATION_DELETE); // 방 참가자들에게 알림 전송
notificationReservationUtils.deleteNotificationReservation(reservation); // 예약 알림 삭제
chatRepository.updateReservationNull(reservationId);
reservationRepository.delete(reservation); // 약속방 삭제
}
약속방을 삭제할 때, 참가자들에게 "회원님이 참여한 약속방이 삭제되었습니다."라는 알림을 전송하는 기능을 개발하던 중 문제가 발생하였다.
현재 약속방 삭제 로직과 알림 전송 로직이 강하게 결합되어 있어, 만약 한쪽 기능에서 문제가 발생하면 전체 트랜잭션이 롤백되었다. 하지만 주 기능은 약속방 삭제이므로, 알림 전송이 누락되더라도 약속방 삭제는 정상적으로 이루어지는 것이 더 중요하다고 판단했다.
또한, 알림 전송이 오래 걸릴 경우 약속방 삭제가 완료되었음에도 알림 전송이 끝날 때까지 기다려야 하므로 성능 저하의 우려도 있었다.
이를 해결하기 위해 Spring Event를 활용하여 알림 기능을 비동기로 처리하기로 했다.
Spring Event를 사용하기에 앞서 문제상황을 정리하고 Spring Event를 사용하는 이유에 대해서 정리하겠다.
1. Spring Event를 사용하는 가장 주된 이유는 서비스 간의 강한 결합을 줄이기 위함이다.
위와 같은 상황에서 약속방 삭제 프로세스와 알림 전송 프로세스 간의 강한 결합으로 인해 시스템이 복잡해지고 유지 보수가 어려운 경우가 발생하였다. 그래서 이 둘의 프로세스를 Spring Event를 통해 분리하여 유연성과 확장성을 높일 수 있다.
2. 이벤트를 비동기로 실행 시 프로세스 처리 시간을 줄일 수 있다.
만약 약속방 삭제 프로세스 처리 시간이 10초 소요되고 알림 전송 프로세스 처리 시간이 30초 소요된다고 가정했을 때, 사용자는 총 40초를 기다려야 한다. 하지만 비동기로 실행시킬 경우 알림 전송 프로세스 처리 시간을 기다리지 않고 바로 약속방 삭제가 처리된다.
3. 이벤트를 비동기로 실행 시 트랜잭션을 분리할 수 있다.
위와 같은 상황에서는 한 트랜잭션에 묶여 있기 때문에 하나의 프로세스에 문제가 발생하면 모두 롤백 된다. 그래서 이를 분리해서 관리하는 게 더 효율적이다.
출처: https://dgjinsu.tistory.com/41
다음으로는 Event 구성요소에 대해서 알아보겠다. 구성요소로는 Event Class, Event Publisher, Event Listener 이렇게 3가지가 있다.
public interface DomainEvent {
}
@Getter
@RequiredArgsConstructor
public abstract class NotificationEvent implements DomainEvent {
private final User user;
private final Long reservationId;
private final String titleMessage;
private final String contentMessage;
}
@Getter
public class ReservationDeletedEvent extends NotificationEvent {
public ReservationDeletedEvent(User user,
Long reservationId,
String titleMessage,
String contentMessage) {
super(user, reservationId, titleMessage, contentMessage);
}
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class ReservationService implements ReservationUtils {
private final ApplicationEventPublisher publisher; // ApplicationEventPublisher 주입
@Transactional
public void deleteReservation(Long reservationId){
User user = userUtils.getUserFromSecurityContext();
Reservation reservation = queryReservation(reservationId);
reservation.validUserIsHost(user.getId());
reservation.validPastReservation();
publisher.publishEvent(
new ReservationDeletedEvent(user, reservation.getId(),
TitleMessage.RESERVATION_DELETE.getTitle(),
makeContent(user, ContentMessage.RESERVATION_DELETE, reservation))); // 방 참가자들에게 알림 전송
notificationReservationUtils.deleteNotificationReservation(reservation.getId()); // 예약 알림 삭제
chatRepository.updateReservationNull(reservationId);
reservationRepository.delete(reservation);
}
}
@Component
@RequiredArgsConstructor
public class NotificationEventHandler {
private final NotificationUtils notificationUtils;
@EventListener
public void sendNotification(NotificationEvent notificationEvent) {
notificationUtils
.sendNotification(
notificationEvent.getUser(),
notificationEvent.getReservationId(),
notificationEvent.getTitleMessage(),
notificationEvent.getContentMessage());
}
}
약속방을 삭제한 후, 이벤트를 발행하여 이벤트 리스너가 해당 이벤트를 처리하여 알림이 잘 전송된 것을 확인할 수 있었다. 프로세스를 분리하여 훨씬 유지보수가 수월하게 되었다.
하지만 이 과정은 모두 하나의 쓰레드에서 동작한다. Spring Event로 강한 결합은 해결했지만, 결국에는 프로세스 처리 지연 문제와 트랜잭션 전체 롤백 문제를 아직 해결하지 못했다.
이를 해결하기 위해 다음 포스팅에서 비동기로 실행시켜 보겠다.