MQ? Kafka?
회사 프로젝트를 설계하면서 동료분들이 자주 하는 말이 있습니다.
“비동기로 구현하는 게 좋을 것 같은데, MQ는 Kafka를 쓰면 되지 않을까요?”
이 말을 들을 때마다 한 가지 궁금증이 생겼습니다. MQ(Message Queue)라고 부르는데, Kafka는 메시지 전달 플랫폼만 지원하는 걸까?
Apache Kafka 공식 사이트를 들어가보니 첫 문장부터 눈에 띄었습니다.
Apache Kafka is an open-source distributed event streaming platform used by thousands of companies for high-performance data pipelines, streaming analytics, data integration, and mission-critical applications.
“event streaming platform” 이라고 명확하게 나와 있었습니다.
그렇다면 당연히 이런 질문이 생깁니다. 이벤트와 메시지는 뭐가 다를까? 이 의문을 풀기 위해 둘의 특징을 비교해보기로 했습니다.
메세지
현실에서의 메세지
시스템의 메세지를 이해하기 전에, 현실에서 편지가 전달되는 과정을 생각해봅시다.
- 편지를 쓸 때 가장 먼저 하는 일은 봉투에 “To. 누구”라고 명확히 수신인을 적는 것입니다.
- 작성한 편지를 우체국에 맡깁니다.
- 우체국은 봉투에 적힌 수신인을 확인하고 정확하게 전달합니다.
- 수신인이 편지를 받아 읽고 나면, 대부분의 편지는 버려지거나 보관합니다.
이 과정에서 메세지의 특징들을 찾을 수 있었습니다.
- 수신인이 명확하다: 편지는 특정 수신인을 위해 작성되고 전달됩니다.
- 송신인의 기대가 있다: 편지를 보내는 사람은 누군가 이 편지를 받고 처리할 것을 기대합니다.
- 읽고 나면 역할이 끝난다: 편지의 목적은 수신인이 읽는 것이고, 읽은 후에는 사라져도 괜찮습니다.
시스템에서의 메세지
시스템에서의 메세지도 비슷한 특성이 있습니다.
예를 들어, 마이크로소프트의 Azure Service Bus 문서에서는 아래와 같이 설명하고 있습니다.

메시지는 큐 간에서 전송 및 수신됩니다. 큐는 수신 애플리케이션이 수신 및 처리할 수 있을 때까지 메시지를 저장합니다.
큐의 메시지는 도착 시 순서가 지정되고 타임스탬프가 지정됩니다.
broker가 메시지를 승인하면 메시지는 항상 삼중 중복 스토리지에 안전하게 보관되며, 네임스페이스가 영역을 사용하도록 설정되면 가용성 영역으로 확장됩니다.
Service Bus는 클라이언트가 메시지를 수락했다고 보고할 때까지 메시지를 메모리나 휘발성 리포지토리에 보관합니다.
이미지에서 “보낸 사람”과 “받는 사람”이 명확하게 구분되어 있습니다.
시스템의 메세지도 특정 수신자를 위한 것입니다.
클라이언트가 메시지를 수락했다고 보고할 때까지만 보관한다는 것은, 메시지의 목적(수신자가 처리하는 것)이 달성되면 더 이상 필요 없다는 의미입니다. 현실의 편지를 받은 후 버리는 것과 같은 맥락입니다.
이벤트
현실에서의 이벤트
이벤트(Event)는 “사건” 그 자체입니다. 편지처럼 누군가를 목표로 보내는 게 아니라, 이미 발생한 사실입니다.
예를 들어, 제가 자전거를 타다가 넘어졌다고 생각해봅시다.
이 순간, “자전거를 타다가 넘어졌다”는 사건이 발생했습니다.
이를 본 사람들은 각자 다르게 반응합니다:
- 누군가는 심각하게 넘어졌다고 판단해 119에 신고합니다.
- 누군가는 놀라서 응급처치를 하려고 달려옵니다.
- 누군가는 봐도 그냥 지나갑니다.
메세지와는 아래와 같은 차이점이 있었습니다.
- 송신자의 기대가 없다: 넘어진 사람은 누군가가 도와줄 거라고 “기대하고” 넘어진 게 아닙니다.
- 사건은 사라지지 않는다: “자전거를 타다가 넘어졌다”는 사건은 이미 일어났고, 그 사실은 변하지 않습니다.
- 수신자가 반응을 결정한다: 누군가 이 사건을 “처리”했다고 해서 사건 자체가 없어지지는 않습니다. 모두가 각자의 방식으로 반응할 수 있습니다.
시스템에서의 이벤트
메세지와 마찬가지로 시스템에서의 이벤트도 크게 현실의 이벤트와 다르지 않다고 생각했습니다.
이커머스 결제 플로우를 예로 들겠습니다.
결제 완료 플로우:
- 사용자가 결제를 요청
- PG사(결제게이트웨이)에 결제 요청
- PG사가 결제 완료 여부를 서비스로 알림
- 결제 서비스에서 결제 완료 처리
여기까지는 결제 서비스가 책임져야 할 작업입니다. 하지만 결제 완료 후:
- 데이터 플랫폼에 결제 정보를 저장해야 하고
- 배송 서비스에서 배송을 준비해야 하고
- 사용자에게 알림을 보내야 하고
- …더 많은 작업들이 있을 수 있습니다.
이때 이벤트를 활용하면 어떻게 될까요?
@Service
@RequiredArgsConstructor
public class PgCallbackService {
private final ApplicationEventPublisher eventPublisher;
// 다른 의존성들...
@Transactional
public void callback(PgCallbackCommand command) {
// PG 결제 결과 저장
PgPayment pgPayment = pgPaymentRepository.getByWithLock(transactionKey);
pgPaymentRepository.save(pgPayment.with(status, failedReason));
// 결제 완료 처리
PaymentCallbackStrategy strategy = strategySelector.select(orderKey, status);
Payment payment = strategy.pay(pgPayment, failedReason);
// 결제 완료 이벤트 발행
eventPublisher.publishEvent(new PaymentCompletedEvent(payment.getId()));
}
}
결제 서비스는 단순히 “나 결제가 완료됐어”라는 PaymentCompletedEvent를 발행할 뿐, 누가 이 이벤트를 처리할지 신경 쓰지 않습니다.
이벤트는 사라지지 않는다
이미 일어난 사건은 되돌릴 수 없습니다. “자전거를 타다가 넘어졌다”는 사실은 누군가가 응급처치를 했다고 해서 사라지지 않습니다.
시스템의 이벤트도 같은 원리입니다. 중요한 건 누군가 이벤트를 처리했다고 해서 그 이벤트가 사라지는 게 아니라는 점입니다.
메세지와의 차이를 다시 생각해보면:
- 메세지: 수신자가 처리하면 → 더 이상 필요 없음 → 삭제 가능
- 이벤트: 수신자가 처리했어도 → 사건은 여전히 존재 → 다른 수신자가 나중에 이용 가능
이는 Kafka 같은 이벤트 스트리밍 플랫폼의 오프셋(Offset) 개념으로 잘 설명됩니다.
Kafka에서 오프셋은 토픽 파티션 내 메시지의 위치를 나타냅니다. 각 메시지에 할당된 순차적인 ID 번호입니다. 컨슈머가 메시지를 읽으면 다음 오프셋을 커밋하여 어디까지 처리했는지 추적합니다. 하지만 이벤트 자체는 계속 저장되어 있으므로, 다른 컨슈머가 다시 읽을 수 있습니다.
즉, 사용자 별로 어느 이벤트까지 처리됐는지 위치 기반으로 처리할 뿐입니다.
수신자가 특정되지 않는다
이벤트는 *누가 이 이벤트를 처리할지 발행자가 알 필요가 없습니다
결제 완료 이벤트(PaymentCompletedEvent)가 발행되면, 여러 서비스가 각자의 방식으로 독립적으로 반응할 수 있습니다.
데이터 플랫폼에 저장하기:
@Component
@RequiredArgsConstructor
public class PaymentDataPlatformEventHandler {
private final DataPlatformClient dataPlatformClient;
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = AFTER_COMMIT)
void handle(PaymentCompletedEvent event) {
// 데이터 플랫폼에 결제 정보 저장
dataPlatformClient.send(event.getPaymentId());
}
}
배송 서비스 시작하기:
@Component
@RequiredArgsConstructor
public class DeliveryEventHandler {
private final DeliveryService deliveryService;
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = AFTER_COMMIT)
void handle(PaymentCompletedEvent event) {
// 배송 준비 시작
deliveryService.prepareDelivery(event.getOrderId());
}
}
사용자에게 알림 발송하기:
@Component
@RequiredArgsConstructor
public class NotificationEventHandler {
private final NotificationService notificationService;
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = AFTER_COMMIT)
void handle(PaymentCompletedEvent event) {
// 결제 완료 알림 발송
notificationService.sendPaymentSuccessNotification(event.getUserId());
}
}
결제 서비스의 관점에서 보면,
- 데이터 플랫폼이 무엇인지 알 필요가 없습니다
- 배송 서비스가 어떻게 동작하는지 신경 쓸 필요가 없습니다
- 알림 시스템의 존재도 몰라도 됩니다
단지 “결제가 완료됐다”는 사실을 알릴 뿐, 누가 그 소식을 받고 어떻게 반응할지는 각 서비스가 독립적으로 결정합니다.
그래서 뭘 사용할까?
처음에 말했던 동료 개발자와 관련된 부분에서 Kafka를 MQ라고 부르는 동료를 지적 하고자 하는 목적이 아닙니다.
이벤트 기반의 시스템을 구축해온 기간보다 메세지를 사용하여 구현한 기간이 훨씬 더 길기 때문에 이벤트와 메세지가 다르다고 하더라도 개발자들끼리는 편의를 위해 MQ라고 부를 수 있습니다.
제가 생각했던 부분은 이벤트와 메세지가 어떻게 다른지 이해하고 우리의 서비스에 맞게 적절하게 사용하는게 좋겠다는 것이였습니다.
그럼 어떤 서비스가 이벤트에 적절하고, 메세지에 적절한지 생각해봤습니다.
메세지, 이벤트 모두 단순히 시스템의 결합력을 낮춘다(decoupling)라는 목적은 달성할 수 있습니다.
하지만 제가 생각한 가장 큰 차이점은 “송신자가 수신자를 기대하고 있느냐” 였습니다.
즉, 이벤트를 사용한 구현은 송신자가 수신자를 신경쓰지않고 본인의 비즈니스에만 집중할 수 있다는 것이 였습니다.
아마, 이 부분으로 인해 물론 MSA와 같이 많은 서비스가 서로 통신하고 있는 상황에서는
결제가 완료됐을때
- 배송도 해야되고
- 결제 이력도 남겨야되고
- 사용자에게 알람도 보내야하고….
이 모든 것을 결제 서비스에서 신경을 쓰지 않고 비즈니스에 집중할 수 있기 때문에 이벤트 사용이 많이 적절하다고 생각이 됐습니다.
하지만 이 정도 복잡성 없는 서비스에서는 과연 유의미할까? 라는 생각이 들었습니다.
단순히 결제가 완료가 됐으면 사용자에게 알림을 보내는 것외에는 없다라고 한다면 이벤트보다는 MQ를 사용하는게 더 나은 선택지가 될 수 있지 않을까? 라는 고민이 들었습니다.
