ApplicationEventPublisher를 활용해보자

허진혁·2023년 3월 30일
0

고민의 시작

댓글이 달렸을 때 포스터 주인에게, 좋아요를 눌렀을 때 포스터 주인에게 알림을 보내는 기능을 구현을 해봤어요. 첫 구현에는 단순하게 구현만 생각하여 댓글을 작성할 때, 알림 서비스에서 알림을 넣어주었죠. 좋아요 눌렀을 때도 이와 같이 구현헀어요.

기능 작동이 잘 되어서 뿌듯했지만 문득 생각에 잠겼어요. 댓글 작성과 알림 보내는 기능이 한 서비스 로직안에 존재하게 되었는데, 이렇게 되면 알람이 보내지지 않을 때, 댓글 작성도 실패하게 되는 것이지요.

알림은 부가적으로 사용자의 편의성을 도와줄 뿐이지, 댓글 작성과는 별개여야해요. 즉, 알림의 유무가 댓글에 영향을 주어서 안되는데, 저는 이에 어긋난 설계를 했어요.

ApplicationEventPublisher는 무엇인가?

  • Spring에서 어노테이션을 기반으로 이벤트 처리를 지원해요.
  • 직접 Publisher와 Listener를 커스텀해서 기능을 구현할 수도 있어요.
  • 옵저버 패턴을 활용

옵저버 패턴이란?

옵저버 이름처럼 행위자, 행위자를 관찰하는 관찰자가 존재해요.
이벤트를 발생시키는 publisher와 관찰하는 observer(=EventListener)가 있는 것이지요.

옵저버 패턴은 보통 언제 사용할까?

다양한 상황에서 사용할 수 있지만 요약하면 객체가 다른 객체에 메시지를 알릴 수 있어야 하고 이러한 객체가 밀접하게 결합되는 것을 원하지 않을 때 옵저버 패턴을 적용할 수 있다고 말할 수 있습니다. 비동기 이벤트가 하나 이상의 그래픽 구성 요소에 알려야 할 때 이 패턴을 사용했습니다.

ApplicationEventPublisher는 적용하기

(스프링 4.2부터 ApplicationEvent를 상속하지 않아도 되요)

ApplicationEventPublisher 흐름

  1. 이벤트가 발생하면 ApplicationEventPublisher가 이벤트를 받아요.
  2. Event를 EventMultiCaster에게 전달해요.
  3. EventMultiCaster는 각 Listener들에게 broadcasting 하듯 이벤트를 뿌려줘요.
  4. 각 Listenr들은 자신의 파라미터를 확인하여 이벤트 타입과 맞지 않으면 무시하고, 맞으면 실행시켜요.

AlarmEvent - 이벤트 클래스 생성

@Getter
public class AlarmEvent {
    private final Alarm alarm;

    public AlarmEvent(Alarm alarm) {
        this.alarm = alarm;
    }

    public static AlarmEvent from(Alarm.AlarmType alarmType, User target, User from) {
        return new AlarmEvent(Alarm.builder()
                .alarmType(alarmType)
                .targetUser(target)
                .fromUser(from)
                .build());
    }
}

AlarmEventHandler - 이벤트 핸들러 생성

  • 발행할 이벤트를 핸들링할 이벤트 핸들러를 만들었어요.
  • 스프링이 관리하도록 bean으로 만들어주고, @EventListener을 활용하여 해당 메서드가 Listener 역할임을 명시해줘요.
  • 파라미터에 있는 이벤트로 해당 리스너에게 전달된 이벤트인지 아닌지 구분해요.
@Component
public class AlarmEventHandler {
    private final AlarmRepository alarmRepository;

    public AlarmEventHandler(AlarmRepository alarmRepository) {
        this.alarmRepository = alarmRepository;
    }

    @EventListener
    @Async
    public void createAlarm(AlarmEvent event) {
        alarmRepository.save(event.getAlarm());
    }
}

CommentService - 이벤트 발행

@Service
@RequiredArgsConstructor
public class CommentService {
	private final CommentRepository commentRepository;
    private final PostRepository postRepository;
    private final UserRepository userRepository;
    private final ApplicationEventPublisher publisher;
    
	@Transactional
    public CommentCreateResponseDto createComment(CommentCreateRequestDto commentCreateRequestDto,
                                                  Integer postId, String userName) {
        Post post = findPost(postId);
        User user = findUser(userName);

        Comment comment = commentCreateRequestDto.toEntity(user, post);
        commentRepository.save(comment);

        // 내 게시물에 내 댓글을 알람 저장 x
        if (!post.getUser().getUserName().equals(userName)) {
            publisher.publishEvent(AlarmEvent.from(NEW_COMMENT_ON_POST, post.getUser(), user));
        }
        return CommentCreateResponseDto.from(comment);
    }
}

여전히 고민해보아야 할 문제

한 트랜잭션에 두 기능이 묶여져 있어요.

@TransactionalEventListener 사용하기

@TransactionalEventListener은 phase 옵션을 통해 트랜잭션에 따른 이벤트처리를 지원해요.

1. @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)

default 값이며, 트랜잭션이 Commit 됐을 때 이벤트를 실행해요.

2. @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)

트랜잭션이 Rollback 됐을 때 이벤트를 실행해요.

3. @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)

트랜잭션이 COMPLETION 됐을 때 이벤트를 실행해요.
AFTER_COMMIT 또는 AFTER_ROLLBACK이 발생했을 때를 의미해요.
위 1,2번을 합친 것이죠.

4. @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)

트랜잭션이 COMMIT 되기 전에 이벤트를 실행해요.

TransactionalEventListener까지 적용하기

@Component
public class AlarmEventHandler {
    private final AlarmRepository alarmRepository;

    public AlarmEventHandler(AlarmRepository alarmRepository) {
        this.alarmRepository = alarmRepository;
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    @Async
    public void createAlarm(AlarmEvent event) {
        alarmRepository.save(event.getAlarm());
    }
}

ApplicationEventPublisher의 한계

  • 스프링이 제공해주는 이벤트 기능으로, 스프링 Bean으로 동작해요.
    그렇다는 것은 외부 시스템과의 연동은 불가능해요.
  • 또한 다른 서버로 이벤트를 전달 할 수 없으니
    MSA 구조처럼 각 도메인 마다 서버가 분리되어 있는 경우는 사용이 불가능해요. 그래서 Kafka와 RabbitMQ와 같은 메세징 큐 소프트웨어를 사용하나 봐요.

참고자료

ApplicationEventPublisher (Spring Framework 5.3.22 API)
https://www.javacodegeeks.com/2012/08/observer-pattern-with-spring-events.html

profile
Don't ever say it's over if I'm breathing

0개의 댓글