
다른 도메인에 특정 작업의 후속 작업을 인계할 때 Event를 활용한다면 결합도를 낮출 수 있다.
Spring에서는 이런 Event 기반으로 동작하기 위해 ApplicationEventPublisher 를 사용하여 event를 publish하고 @EventListner를 통해 이벤트를 Subscribe 할 수 있다.
물론 ApplicationEventPublisher를 통해 ApplicationContext로 이벤트를 발행하는 것 말고도 RabbitMQ, Kafka, EventBus 등을 사용하여 이벤트를 발행할 수 있지만 나는 ApplicationEventPublisher를 사용했다.
이때, 이벤트를 수신받는 클래스에서는 이벤트를 수신하여 이후 동작을 시행하게 되는데, 여기서 Event를 그대로 주는 것보다는 Command 객체로 Event를 변환하여 하위 레이어에게 정보를 넘긴다.
Event를 그대로 주면 안되는걸까? 여기서 나온 Command는 무엇일까?
자세한 이해를 위해 코드 예시로 확인해보자.
@Getter
@EqualsAndHashCode
@AllArgsConstructor
public class UserWithdrawnEvent {
private final Long userId;
}
....
//UserCommandService.java
eventPublisher.publishEvent(new UserWithdrawnEvent(user.getId()));
이렇게 User의 Id를 담은 이벤트가 회원 탈퇴 시 User 도메인에서 발행되면
@Component
@RequiredArgsConstructor
public class GoalEventListener {
private final GoalCommandService goalCommandService;
@Async
@EventListener
public void handleUserWithdrawnEvent(UserWithdrawnEvent event) {
goalCommandService.deleteAll(new DeleteAllGoalCommand(event.getUserId()));
}
}
Goal 도메인에 있는 GoalEventListner는 사용자가 탈퇴했다는 이벤트를 수신하고 사용자가 작성했던 목표들을 삭제한다.
여기서 CommandService의 Command는 CQRS에서 사용하는 Command, Query의 Command이다.
아니 Event를 수신받았으면 바로 Event를 넘기면 될 것 같은데 왜 Command로 변환해서 넘기지? 대체 Command는 뭐고 Event와 Command의 차이는 뭘까?

Command와 Event는 모두 메시지라는 점에서 공통점이 있다. 다른 서비스가 알아야 할 정보를 직렬화한 페이로드라는 의미에서 동일하다. 그러나 그 목적과 시점이 다르기 때문에 구분해 사용하는 것이 중요하다.
즉, Command는 어떤 동작이 일어나기 전에 발행되고, Event는 어떤 동작이 끝난 후에 발행된다. 따라서 Command는 거부될 수 있으나 Event는 이미 발생한 사실이므로 거부가 불가능하다는 특성이 시스템 설계에서 큰 차이를 만든다.
Command는 “지금 이 동작을 수행하라”라는 의미를 갖는다.
Event는 "이미 발생한 사실”을 의미한다. 이러한 이유로 Event에 이름을 붙일 때는 과거시제로 작명하는 것이 일반적이다.
앞선 코드를 다시 확인해보면
@Component
@RequiredArgsConstructor
public class GoalEventListener {
private final GoalCommandService goalCommandService;
@Async
@EventListener
public void handleUserWithdrawnEvent(UserWithdrawnEvent event) {
goalCommandService.deleteAll(
new DeleteAllGoalCommand(event.getUserId())
);
}
}
goalCommandService는 Goal 엔티티를 활용하여 작업을 처리하는 서비스로 어떤 행동을 "실행"하는 것에 목적을 두는 반면, 이벤트는 어떤 일이 발생했다는 "사실" 자체를 단순히 알리는 것이 목적이다.
따라서, EventListner 클래스는 이 Event가 알리는 "사실"을 받아 goalCommandService로 Goal을 삭제하라는 DeleteAllGoalCommand로 Event를 가공하여 전달하는 것이다. 이러면 EventListner가 goalCommandService로 행동을 "지시"하는 것이 된다.
이는 곧 다른 도메인 간의 서비스 동작을 제어할 때 이벤트와 커멘드가 사용한다.
또한, Command는 어떤 작업이 실행되기 전에 A 작업을 시행해달라! 라고 지시하는 것이기 때문에 유효성 검증이나 권한 검사 등에 용이하다.
반면, Event는 작업이 이루어진 뒤의 결과를 전달하는 것이기 때문에 이미 해당 작업은 이루어졌다는 것을 의미한다. 때문에 이를 기반으로 특정 작업 이후의 후속 처리나 알림 발신 등의 작업을 진행하는 데 유용하다.
일반적으로 Spring에서 ApplicationEventPublisher를 사용하면 이벤트에 비동기처리를 하지 않는 한, 한 쓰레드에서 동작이 진행되기 때문에 트랜잭션을 보장할 수 있다. 그러나, 이벤트 기반 동작이 한 쓰레드에서 이루어진다는 것 자체부터가 이벤트를 사용하는 의미가 없어지고 비효율적이게 된다.
이벤트는 사실을 전달하고 이 이벤트를 받아 작업하는 다른 서비스에 대한 관심을 끄고 신경쓰지 않기 위해 사용하는 것인데, 한 쓰레드에서 여러 서비스의 작업이 실행된다는 것 자체가 불쾌한 일이기 때문에 보통의 이벤트는 비동기 처리를 진행한다.
이렇게 되면 이벤트에 대해서는 여러 쓰레드에서 작업이 이루어지기 때문에 트랜잭션 보장이 되지 않는다.
이를 해결하기 위해 이벤트 기반으로 설계했을 때에는 실패 시 재시도 및 재요청 로직을 포함하여 일시적인 장애로 인한 실패를 대비한다. 그럼에도 불구하고 여러 번 실패한다면 DLQ(Dead Letter Queue)를 배치하여 재실패한 이벤트들을 DLQ에 넣고 이후 별도의 수동 작업, 모니터링을 통해 DLQ에 들어간 이벤트들을 재발행하거나 따로 처리하여 해결할 수 있다.
혹은 보상 트랜잭션 패턴을 적용시켜 오류가 발생했을 때 해당 작업을 보상하거나 롤백시킬 수 있는 별도의 로직을 두어 시스템의 일관성을 유지할 수 있게 한다.
물론 이런 처리들은 기존보다 추가적인 작업이 들어가야 하기 때문에 이런 점들을 신중히 고려하여 도입해야 한다.
이런 비슷한 내용들을 인지한다면 SAGA 패턴을 이해하기 쉬워진다.
Command와 Event의 차이를 느끼니 점점 소프트웨어 개발도 처음 접했을 때와는 느낌이 달라지는 것 같다.
처음 개발을 접했을 때는 여러 방법론들, 구현 패턴, 생소한 용어들로 인해 정형화되고 전문적인 영역이라고만 생각을 했었는데, 점차 파면 팔수록 전체 프로세스를 추상화해서 바라보니 일상생활과 비슷하다는 생각이 들었다.
처음보다는 점차 프로젝트에 대한 이해와 도메인 분리를 먼저 고민하게 되고, 이 도메인에 대해 객체의 역할과 특징에 대해 고민하다보니 객체가 더 이상 코드가 아닌 하나의 물체로 보이기 시작했고, 이 물체들 간의 소통을 위해 Command, Event와 같은 개념들이 생기는 것을 체감할 수 있는 것 같다.
단순히 서버를 구축하고 코드를 짜는 것이 아니라 서비스에 대해 깊이 이해하고 추상화하는 단계를 고민할 수 있게 되니 완전히 새로운 즐거움으로 개발자에 대해 흥미가 생긴 것 같다.
확실히 이러한 것들은 정답이 존재하는 것이 아니고 인간이 요구사항과 개인의 바램에 맞게 커스텀하고 고민하는 과정이기 때문에 AI가 작성하는 단순 코드 짜기와는 지향하는 방향이 다른 것 같다. 이런 점들을 갈고 닦는다면 대체되지 않는 개발자가 될 수 있을까...?
참고 : Events vs Commands: What's the difference? - Drawing Boxes