주문에서 쿠폰 등록 커멘드일까? 이벤트일까?
- 루퍼스/7주차
- 2025. 8. 29. 02:46
배경
주문과 결제에는 굉장히 많은 이벤트들이 존재한다.
재고, 포인트, 쿠폰등등의 이벤트들이 존재한다.
현재는 이것들이 하나의 트랜잭션에서 동작을 하고 있다.
각각의 책임을 분리하기 위해서는 어떻게 해야할까?
커멘드와 이벤트
커멘드와 이벤트는 뭐가 다를까.. 이것들은 각각 커멘드는 명령이고, 이벤트는 사건이라는 뜻을 가지고 있다.
예를 들어, 주문, 결제, 쿠폰, 재고, 포인트라는 등장인물이 존재한다고 생각해보자.
내가 선택한 부분은
주문 -> 쿠폰 등록
결제 -> 포인트 사용
결제 -> 재고 차감
결제 -> 주문 완료
요렇게 작성이 되어진다. 가장 맨위로 예시를 들어보면 주문이 쿠폰은 명령일까 사건일까? 주문 입장에서 쿠폰 도메인한테 명령을 보내는 것이 맞을까? 아니면 주문 입장에서 쿠폰 생성은 하나의 사건일 뿐인걸까?
명령과 사건에 대한 뜻을 찾아봤다.
'명령'은 다른 사람에게 어떠한 행동을 하도록 지시하고 강요하는 행위
'사건'은 사회적으로 관심을 받거나 주목할 만한 뜻밖의 일을 의미
이걸 토대로 생각을 해보면 쿠폰은 주문에게서 행위를 강요 받는걸까? 아니면 뜻밖의 일인걸까?
만약에 쿠폰이 주문에게서 행위를 강요 받는다고 생각한다면, 명령 일거고 뜻밖의 일이라면 사건 일거다.
내가 생각할때, 쿠폰 등록은 주문 입장에서 뜻밖의 일이라고 생각한다. 왜냐하면 반드시 일어나야 하는 일이 아니라고 생각하기 때문이다.
주문 입장에서 쿠폰 등록이 되든 안되든 그게 중요할까?
결국 주문 -> 쿠폰 등록은 이벤트라고 생각이 든다.
내 기준에서의 커멘드와 이벤트에 대해 작성해봤다. 그렇다면 스프링에서는 어떻게 표현할 수 있을까?
In Spring
우리가 이렇게 커멘드와 이벤트로 서로 분리를 했지만 스프링 입장에서는 사실 이벤트다.
이걸 ApplicationEventPublisher
와 @EventListener
을 통해 정리할 수 있다.
아쉽게도 커멘드를 위한 클래스, 어노테이션은 존재하지 않는다. 커멘드와 이벤트는 얘네를 어떻게 사용하냐에 따라
커멘드와 이벤트로 구분을 지을수가 있다.
ApplicationEventPublisher
명령을 하든, 사건이 발생하든 어찌되었든 발행이 되어야 한다. 위에서 말했듯이 스프링입장에서는 명령이든 사건이든, event(사건)에 불가하다. 이를 메시지가 발행이 되었다고 말할 수 있다.
코드로 작성해본다면 다음과 같이 작성할 수 있다.
public class EventPublisher {
private final ApplicationEventPublisher publisher;
public void handle() {
publisher.publishEvent("메시지");
}
}
메시지를 작성되어진 "메시지" 부분에 정보를 담아서 전송하면 된다. 그러면 어떤 데이터를 전달을 해야할까?
여기서 커멘드인지 이벤트인지 정해진다고 생각이 들어진다.
command
명령은 다른 사람에게 메시지를 전달 한다. 메시지는 어떠한 정보를 담아야 할까?
뛰어난 명령은 간단한 단어로, 말할 내용을 모두 함축해서 논리적으로 상대방을 납득시키는 것을 말한다.
즉, 명령의 메시지는 상대방(명령을 받는 도메인)에게 맞춰서 작성이 되어야 한다.
위에서 주문과 쿠폰을 대상으로 command로 할지 event로 할지 정했었다.
만약, command로 작성이 되어진다면, 메시지는 어떻게 작성이 되어야 할까?
이걸 다시 작성해보면 주문이 쿠폰에게 명령을 내려서 쿠폰이 등록이 되게 만들려면 어떻게 해야 할까?
어떤 메시지를 전달해야 할까?
주문은 쿠폰에게 이런 정보가 있으니, 한번 쿠폰을 등록해봐라... 하지 않을까?
즉, 어떻게 보면 주문은 쿠폰 등록을 유도했다고 볼수 있다고 생각한다.
쿠폰이 등록이 되려면, 어디에서 (주문) 누가(사용자)가 반드시 필요하다.
최소한으로 이 정보들이 있어야 쿠폰이 등록을 할 수 가 있다.
그렇다면, Event는 어떠한 점이 다를까?
Event
사건은 명령과 무엇이 다를까? 우리가 사건을 바라볼때, 그것이 반드시 발생할거라고 생각하지는 않는다.
그렇다면 사건은 메시지를 어떻게 작성해야 할까? 어떻게 보면 메시지를 발행하는것이 기때문에 command라 생각할 수 있다.
왜냐하면 사건을 발행한다니.. 뭔가 이상하다. ..
일반적으로 사건이 발생이 되면 그것을 제 3자로써 바라보게 된다.
이에토대로 생각해보면, 이벤트라는건 잘 기록해놓은것이 아닌가 생각이 든다.
즉, 이미 발생된 내용들을 기록해 놓은거라고 생각하면 된다. 아이러니하게도 이벤트도 커멘드와 마찬가지로 메시지를 발행한다.
그렇다는건 이미 발생된 사실을 전달해주는거이지 않을까?
이벤트를 “잘 기록한다”는 건 결국 시스템이 기억할 가치가 있는 사실만 남긴다는 것이다.
따라서 이벤트 발행은 더 정확히 말하면 이벤트 기록을 발행해서 확인하는 절차를 가지는거라 할 수 있다.
그러면 위 예시로 계속 얘기를 해보면, 주문이 쿠폰 등록을 이벤트로 바라본다면,
주문은 쿠폰 등록이 되었다는 사실을 기록한것을 쿠폰에게 전달하여 그것을 확인하는 절차라고 생각이 되어진다.
이걸 이벤트로 바라본다면, 쿠폰 등록은 주문과 별개로 발생이 되어진다.
이제 이걸 메시지로 작성해보면, 어떻게 작성할 수 있을까? 커멘드와 했더와 달리 어떤 쿠폰으로 등록이 되는지 알 필요가 있다.
즉, 쿠폰 등록 이벤트가 발생이 되었다는 사실을 알려면 어디에서(주문 ID) 누가(계정 ID 무엇으로 (쿠폰ID) 기록이 되어야 한다 생각한다
그래야 주문쪽에서 쿠폰 등록 이벤트가 발생이 되는것을 알기 때문이다. 이렇게 메시지를 발행하는 방법을 알아보았다.
이제 이 메시지를 수신하는 방법도 알아보자.
리스너
이렇게 발행이 되어진 메시지는 각 도메인에서 수신을 할 수 있다.
(도메인 리스너를 말하는게 아님) 아쉽게도 리스너를 통해 커멘드인지 이벤트인지는 알 수 없었다.
@EventListener
이 방식은 메시지가 발행이 되어지면, 즉시 실행이 되어지는 특징을 가지고 있다. 처음 생각할때 이 어노테이션은 이벤트를 사용할때 사용이 되어진다고 생각하였다. 하지만 곰곰히 생각해보니 커멘드에서도 사용할 수 있지 않을까 생각이 든다.
이 또한 커멘드와 이벤트를 분리해서 어떤 느낌인지 한번 비교해보자.
command
즉시, 실행이 되어지는데 명령이라.. 주문이 쿠폰에게 쿠폰 등록이라는 명령을 내렸는데 명령이 하달 되는 즉시 처리를 하라는 뜻이다.그렇담 쿠폰은 이를 즉시 실행을 해야 하는데 여러 사유로 인해 '쿠폰 등록' 명령을 어긴다면? 왜 그 명령을 어겼는지 보고를 해야 한다.그래야 명령을 내린 주문도 그에 따른 조치를 할 수 있기 때문이다.
Event
이 또한 즉시 이벤트가 발생이 되어진다고 생각하면 된다. 주문에서 메시지가 발행이 되어질때, 쿠폰 등록 이벤트가 발생이 되어진다고 해석 할 수 있다. 이벤트이기때문에 주문(주체)입장에서는 쿠폰 등록이 되었는지 안 되었는지 크게 관심이 없다.
기록관이 잘 기록만 해주면 된다고 생각한다.
그래서 만약 실패를 하게 된다면, 또 다른 command혹은 Event를 통해 해결해야 된다. 이를 보상 트랜잭션이라고 부르기도 한다.
@TransactionEventListner
위에서 메시지가 발행이 되는 시점에 즉시 실행을 시켰다면, 이 방식은 트랜잭션의 여부에 따라 실행 시점을 다르게 할 수 있다.
(DB트랜잭션이 아닌 스프링 트랜잭션임에 주의하자.)
방식은 다음과 같다.
BEFORE_COMMIT
→ 트랜잭션이 커밋 직전에 실행됨
→ 아직 DB에 flush가 반영되기 전이므로, 예외 발생 시 트랜잭션 전체가 롤백됨
AFTER_COMMIT (기본값)
→ 트랜잭션이 성공적으로 커밋된 이후 실행됨
→ DB 반영이 확정된 상태이므로, 여기서 발생하는 예외는 원 트랜잭션에 영향을 주지 않음
AFTER_ROLLBACK
→ 트랜잭션이 롤백된 이후 실행됨
→ 실패 상황에 대한 보상 처리, 정리 작업, 알림 등에 활용
AFTER_COMPLETION
→ 트랜잭션이 끝난 직후 (성공/실패와 무관하게) 실행됨
커멘드와 이벤트에 관련된얘기는 중복되므로 생략합니다.
비동기
지금까지 동기방식인 경우 커멘드와 이벤트에 대해 알아봤다. @Async
리스너 쪽(수신 받는 쪽)에 위 어노테이션만 추가하면
비동기 방식으로 동작이 되어진다. 그러면 이는 어떤 의미를 내재하고 있을까?
커멘드와 이벤트 모두 동일하므로 따로 분리하지는 않고 설명할 예정이다.
위에서 말했듯이 커멘드와 이벤트는 메시지가 발행된다고 하였다.
그리고 비동기는 이전 메시지의 결과가 마무리 되는거와 상관없이 실행이 되어진다. 퍼블리셔를 통해 메시지가 발행이 되면 그 즉시 리스너가 실행이 되고 리스너의 완료와 상관없이 퍼블리셔의 다음코드로 넘어가게 된다.
비동기로 동작은 다음과 같은 의미를 내재하고 있다고 한다.
1. 발행자-수신자의 독립성 강화
- 동기에서는 발행자가 수신자의 실행 완료에 묶여 있음.
- 비동기로 바꾸면 발행자는 수신자의 생존/실패 여부와 무관하게 자기 로직을 계속 진행 가능.
- 즉, 결합도가 낮아지고 확장성이 높아짐.
2. 에러 전파 불가능
- 동기에서는 수신자가 실패하면 예외가 퍼블리셔에게 전달됨.
- 비동기에서는 예외가 별도 쓰레드에서 터지므로 퍼블리셔는 모름.
- 따라서 “보상 트랜잭션”이나 “재시도 메커니즘”을 별도로 설계해야 함.
3.성능과 처리량 증가
- 요청/응답 구조가 아니라, 이벤트 드리븐 구조로 가면서 발행자는 블로킹되지 않음.
- 따라서 TPS가 올라가고, 리스너가 느려도 발행자는 영향이 적음.
4. 일관성(Consistency) 문제 발생
- 동기 방식에서는 발행자와 수신자가 같은 트랜잭션처럼 느껴질 수 있음.
- 비동기에서는 발행 시점과 실제 수신 처리 시점 사이에 시간 차가 생김
→ 최종적 일관성(Eventual Consistency) 모델로 바뀜.
- 따라서, 즉시 정합성이 필요한 비즈니스 로직에서는 위험할 수 있음.
상황에 따라 다르게 해석은 할 수 는 있겠지만 대체적으로 커멘드는 sync 방식이 이벤트는 Async 방식이 본질적으로 맞다고 한다.
왜냐하면 명령을 내리게 되면 추후에 이 동작을 하여라라는 뜻이기 때문에 조금더 동기방식에 어울리고
이벤트는 '~이 일어났다'라는 사실이 더 중요하기 때문에 비동기 방식에 더 맞다고 생각한다.
결론
내가 생각할때, 커멘드와 이벤트는 뉘양스의 차이라고 생각한다. 즉, 어떤것들이어도 잘만 포장만 한다면, 둘다 정답이 될 수 도 있다고 생각한다. 왜냐하면, 커멘드도 누군가에게는 이벤트에 불가하기 때문이라 생각이 든다. 조선 시대 왕이 신하들에게 명령을 내리는 것을 '왕'입장에서 볼때는 커멘드지만 그걸 지켜보는 신하들 혹은 이걸 보고있는 우리들은 그 상황을 하나의 이벤트로 여길 수 있기 때문이다. 결국, 어떤 입장에서 바라보느냐야 따라 커멘드와 이벤트로 구분을 지을수 있다. 그래서 spring이 바라볼때는 이벤트인거구, 코드를 작성하고 있는 우리는 커멘드(명령)으로 바라볼 수 밖에 없다. 하지만 주체는 스프링도 아니고 우리도 아니다. 각 도메인에 빙의하여 선택해야 한다고 생각한다. 그렇다면 주문에서 쿠폰 등록은 커멘드일까? 이벤트일까?