0. 들어가며
앞선 글에서 우리는 REST, gRPC, GraphQL과 같은 동기 통신 방식과 API Gateway 패턴에 대해 살펴보았다. 동기 통신은 구현이 직관적이고 이해하기 쉽다는 장점이 있지만, 서비스 간 결합도를 높이고 장애가 연쇄적으로 전파될 수 있는 위험이 있다.
이번 글에서는 이러한 동기 통신의 한계를 극복하기 위한 비동기(Asynchronous) 통신과 이벤트 기반 아키텍처(Event-Driven Architecture)에 대해 알아본다. 메시지 브로커를 활용한 Pub/Sub 패턴, Kafka와 RabbitMQ의 차이점, 그리고 실제 이커머스 시스템에서 어떻게 활용할 수 있는지 살펴볼 것이다.
1. 비동기 통신 개요
1.1. 비동기 통신이란?
비동기 통신은 요청을 보내고 응답을 기다리지 않고 바로 다음 작업을 수행하는 통신 방식이다. 요청과 응답이 분리되어 있어, 서비스 간 결합도를 낮추고 시스템의 탄력성을 높일 수 있다.
동기 vs 비동기 통신 비교:
// 동기 통신 - 응답을 기다림
public void processOrder(Order order) {
// 결제 서비스 호출 - 응답 올 때까지 블로킹
PaymentResult result = paymentService.processPayment(order);
if (result.isSuccess()) {
// 재고 서비스 호출 - 응답 올 때까지 블로킹
inventoryService.deductStock(order);
// 배송 서비스 호출 - 응답 올 때까지 블로킹
shippingService.createShipment(order);
}
}
// 비동기 통신 - 이벤트 발행 후 즉시 반환
public void processOrder(Order order) {
// 주문 생성 이벤트 발행 (Kafka, RabbitMQ)
orderCreatedEventPublisher.publish(new OrderCreatedEvent(order));
// 즉시 반환 - 실제 처리는 컨슈머가 비동기로 처리
}
1.2. 비동기 통신의 장단점
장점:
- 느슨한 결합(Loose Coupling): 서비스가 서로를 직접 알 필요 없이 메시지 브로커를 통해 통신
- 탄력성(Resilience): 일부 서비스 장애가 전체 시스템으로 전파되지 않음
- 확장성(Scalability): 컨슈머를 여러 개 두어 병렬 처리 가능
- 부하 분산(Load Balancing): 피크 타임에도 메시지 큐가 버퍼 역할을 함
- 탄력적 처리(Elastic Processing): 처리 능력에 따라 메시지 소비 속도 조절
단점:
- 복잡성 증가: 메시지 브로커 도입으로 인프라 복잡도 상승
- 추적 어려움: 분산된 시스템에서 메시지 흐름 추적이 어려움
- 최종 일관성: 강한 일관성 대신 최종 일관성만 보장
- 메시지 순서 보장 어려움: 분산 환경에서 순서 보장이 복잡
- 디버깅 어려움: 비동기 흐름 디버깅이 동기 방식보다 어려움
1.3. 비동기 통신이 필요한 상황
| 상황 | 동기 통신 | 비동기 통신 |
| 즉시 응답이 필요한 경우 (조회 API) | ✅ | ❌ |
| 오래 걸리는 작업 (배치, 리포트 생성) | ❌ | ✅ |
| 여러 서비스에 동일한 데이터 전파 | ❌ | ✅ (Pub/Sub) |
| 트래픽 급증이 예상되는 경우 | ❌ | ✅ (큐 버퍼링) |
| 강한 일관성이 필요한 경우 | ✅ | ❌ |
2. 비동기 통신에서의 메시지 처리 방법
2.1. 메시지 큐(Message Queue) 패턴
메시지 큐는 프로듀서가 보낸 메시지를 임시 저장했다가 컨슈머가 가져가는 방식이다. 일반적으로 점대점(Point-to-Point) 통신에 사용된다.
[프로듀서] → [메시지 큐] → [컨슈머 1] (메시지 소비)
↘ [컨슈머 2] (메시지 소비 안 함 - 이미 소비됨)
특징:
- 하나의 메시지는 하나의 컨슈머만 소비
- 작업 분산(Work Queue) 패턴에 적합
- 로드 밸런싱 효과
2.2. Publish/Subscribe 패턴
Pub/Sub 패턴은 프로듀서가 발행한 메시지를 구독 중인 모든 컨슈머가 받아가는 방식이다.
[프로듀서] → [토픽] → [컨슈머 1] (메시지 수신)
↘ [컨슈머 2] (메시지 수신)
↘ [컨슈머 3] (메시지 수신)
특징:
- 하나의 메시지를 여러 컨슈머가 수신
- 이벤트 전파(Event Broadcasting)에 적합
- 느슨한 결합 극대화
2.3. 메시지 처리 패턴 비교
| 패턴 | 메시지 전달 | 사용 사례 |
| Point-to-Point | 1:1 | 작업 큐, 이메일 발송 |
| Pub/Sub | 1:N | 이벤트 알림, 데이터 동기화 |
| Request-Reply | 1:1 (응답 별도 큐) | 비동기 요청-응답 |
| Dead Letter Queue | 실패 메시지 저장 | 에러 처리, 재시도 관리 |
3. Kafka vs RabbitMQ 비교
3.1. RabbitMQ 개요
RabbitMQ는 Erlang으로 작성된 오픈소스 메시지 브로커로, AMQP(Advanced Message Queuing Protocol)를 구현한다.
특징:
- 다양한 메시징 패턴 지원 (큐, 토픽, 라우팅)
- 스마트 브로커, 덤 컨슈머 모델
- 메시지 확인(ACK)과 재전송 메커니즘
- 복잡한 라우팅 가능 (Exchange 타입 다양)
적합한 사용 사례:
- 복잡한 라우팅이 필요한 경우
- 작업 큐(Work Queue) 패턴
- RPC 스타일 통신
- 트랜잭셔널 메시징
3.2. Kafka 개요
Apache Kafka는 링크드인에서 개발된 분산 이벤트 스트리밍 플랫폼으로, 높은 처리량과 내구성이 특징이다.
특징:
- 로그 기반 메시지 저장 (디스크에 저장)
- 덤 브로커, 스마트 컨슈머 모델
- 높은 처리량 (초당 수백만 메시지)
- 메시지 순서 보장 (파티션 내에서)
- 긴 메시지 보존 기간 설정 가능
적합한 사용 사례:
- 대용량 이벤트 스트리밍
- 로그 수집 및 집계
- CDC(Change Data Capture)
- 이벤트 소싱(Event Sourcing)
3.3. 상세 비교
| 특성 | RabbitMQ | Apache Kafka |
| 모델 | 큐 기반 | 로그 기반 |
| 메시지 저장 | 소비 후 삭제 (기본) | 설정된 보존 기간 동안 유지 |
| 처리량 | 초당 수천~수만 | 초당 수백만 |
| 순서 보장 | 큐 내에서 보장 | 파티션 내에서 보장 |
| 라우팅 | 복잡한 Exchange 가능 | 토픽 기반 단순 라우팅 |
| 메시지 재처리 | 어려움 (ACK 후 삭제) | 가능 (오프셋 리셋) |
| 운영 복잡도 | 중간 | 높음 (ZooKeeper 필요) |
| 클라이언트 언어 | 대부분 지원 | 대부분 지원 |
| 사용 사례 | 작업 큐, RPC | 스트리밍, 이벤트 저장 |
3.4. 선택 가이드
RabbitMQ를 선택해야 하는 경우:
- 복잡한 라우팅 로직이 필요한 경우
- 작업 큐 패턴으로 작업을 분산해야 하는 경우
- 트랜잭셔널 메시징이 필요한 경우
- 비교적 단순한 메시징 인프라가 필요한 경우
Kafka를 선택해야 하는 경우:
- 초당 수십만 이상의 높은 처리량이 필요한 경우
- 이벤트 소싱(Event Sourcing)을 구현해야 하는 경우
- 긴 기간 메시지를 보존해야 하는 경우
- 스트림 프로세싱이 필요한 경우
- 여러 컨슈머가 동일한 이벤트를 다시 읽어야 하는 경우
4. Event-Driven Architecture 패턴
4.1. 이벤트 기반 아키텍처란?
이벤트 기반 아키텍처(EDA)는 시스템의 상태 변화를 이벤트로 표현하고, 이러한 이벤트의 생성, 감지, 소비를 중심으로 구축된 아키텍처 스타일이다.
핵심 구성 요소:
- 이벤트(Event): 과거에 발생한 사실 (예: "주문이 생성되었다")
- 이벤트 프로듀서: 이벤트를 생성하는 서비스
- 이벤트 컨슈머: 이벤트를 소비하고 반응하는 서비스
- 이벤트 채널: 이벤트가 전달되는 경로 (메시지 브로커)
4.2. 이벤트 패턴의 유형
1. 이벤트 알림(Event Notification) 패턴
가장 기본적인 패턴으로, 이벤트 발생을 알리는 최소한의 정보만 전달한다.
// 이벤트 알림 - 최소 정보만 포함
public class OrderCreatedEvent {
private String orderId;
private String userId;
private LocalDateTime occurredAt;
// 상세 정보는 API로 조회해야 함
}
// 컨슈머는 이벤트를 받으면 API로 상세 정보 조회
@KafkaListener(topics = "order-events")
public void handleOrderCreated(OrderCreatedEvent event) {
// 이벤트만으로는 부족하므로 API 호출
Order order = orderServiceClient.getOrder(event.getOrderId());
// 처리 로직...
}
2. 이벤트 전달 상태 저장(Event-Carried State Transfer) 패턴
이벤트 자체에 모든 필요한 데이터를 포함시켜 전달한다. 컨슈머는 별도 API 호출 없이 이벤트만으로 처리 가능하다.
// 이벤트에 모든 상태 포함
public class OrderCreatedEvent {
private String orderId;
private String userId;
private String userName; // 사용자 정보 포함
private String userEmail; // 사용자 정보 포함
private List<OrderItem> items; // 상품 정보 포함
private Money totalAmount;
private LocalDateTime occurredAt;
// 필요한 모든 데이터 포함
}
// 컨슈머는 이벤트만으로 처리 가능
@KafkaListener(topics = "order-events")
public void handleOrderCreated(OrderCreatedEvent event) {
// API 호출 없이 이벤트 데이터만으로 처리
analyticsService.recordOrder(
event.getUserId(),
event.getUserName(),
event.getItems(),
event.getTotalAmount()
);
}
3. 이벤트 소싱(Event Sourcing) 패턴
애플리케이션의 상태를 저장하는 대신 상태를 변경시킨 모든 이벤트를 저장하는 패턴이다.
// 이벤트 저장소
@Entity
public class EventStore {
@Id
private String eventId;
private String aggregateId;
private String eventType;
private String eventData; // JSON 형태
private int version;
private LocalDateTime timestamp;
}
// 상태는 이벤트 재생으로 복원
public class OrderAggregate {
private List<DomainEvent> changes = new ArrayList<>();
public static OrderAggregate recreateFrom(List<DomainEvent> events) {
OrderAggregate aggregate = new OrderAggregate();
events.forEach(aggregate::applyEvent);
return aggregate;
}
private void applyEvent(DomainEvent event) {
if (event instanceof OrderCreatedEvent) {
// 상태 변경
} else if (event instanceof OrderShippedEvent) {
// 상태 변경
}
}
}
4.3. 이커머스에서의 EDA 활용 예시
주문 처리 프로세스:
[주문 서비스] OrderPlaced 이벤트 발행
↓ (Kafka/RabbitMQ)
├─→ [재고 서비스] 재고 차감
├─→ [결제 서비스] 결제 처리
├─→ [분석 서비스] 주문 데이터 기록
├─→ [알림 서비스] 고객에게 이메일 발송
└─→ [추천 서비스] 사용자 구매 이력 업데이트
코드 구현 예시:
// 1. 이벤트 정의
@Value
public class OrderPlacedEvent {
String eventId = UUID.randomUUID().toString();
String orderId;
String userId;
List<OrderItemEvent> items;
Money totalAmount;
LocalDateTime occurredAt = LocalDateTime.now();
}
@Value
public class OrderItemEvent {
String productId;
String productName;
int quantity;
Money price;
}
// 2. 이벤트 발행 (주문 서비스)
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final KafkaTemplate<String, Object> kafkaTemplate;
@Transactional
public Order createOrder(CreateOrderRequest request) {
// 1. 주문 저장
Order order = Order.create(request);
orderRepository.save(order);
// 2. 이벤트 생성
OrderPlacedEvent event = new OrderPlacedEvent(
order.getId(),
order.getUserId(),
order.getItems().stream()
.map(item -> new OrderItemEvent(
item.getProductId(),
item.getProductName(),
item.getQuantity(),
item.getPrice()))
.collect(Collectors.toList()),
order.getTotalAmount()
);
// 3. 이벤트 발행 (트랜잭션 후)
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
kafkaTemplate.send("order-events", order.getId(), event);
}
}
);
return order;
}
}
// 3. 이벤트 소비 (재고 서비스)
@Service
@Slf4j
@RequiredArgsConstructor
public class InventoryService {
private final InventoryRepository inventoryRepository;
@KafkaListener(topics = "order-events")
public void handleOrderPlaced(OrderPlacedEvent event) {
log.info("Processing order placed event: {}", event.getOrderId());
event.getItems().forEach(item -> {
try {
// 재고 차감
inventoryRepository.deductStock(
item.getProductId(),
item.getQuantity()
);
log.info("Stock deducted for product: {}", item.getProductId());
} catch (Exception e) {
log.error("Failed to deduct stock for product: {}", item.getProductId(), e);
// 실패 처리 - 보상 트랜잭션이나 DLQ 전송
}
});
}
}
// 4. 이벤트 소비 (알림 서비스)
@Service
@Slf4j
@RequiredArgsConstructor
public class NotificationService {
private final EmailSender emailSender;
private final SmsSender smsSender;
@KafkaListener(topics = "order-events")
public void handleOrderPlaced(OrderPlacedEvent event) {
log.info("Sending notifications for order: {}", event.getOrderId());
// 이메일 발송
emailSender.send(
event.getUserId(),
"주문이 완료되었습니다",
"주문번호: " + event.getOrderId()
);
// SMS 발송 (고액 결제 시)
if (event.getTotalAmount().isGreaterThan(new Money(100_000))) {
smsSender.send(
event.getUserId(),
"고액 결제가 완료되었습니다"
);
}
}
}
5. [실습 요약] 비동기 통신 구현
5.1. 실습 9: Message Broker 실행 및 메시지 발행
RabbitMQ 실행:
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management
Kafka 실행 (Docker Compose):
version: '3'
services:
zookeeper:
image: confluentinc/cp-zookeeper:latest
environment:
ZOOKEEPER_CLIENT_PORT: 2181
kafka:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
Spring Boot 설정:
spring:
kafka:
bootstrap-servers: localhost:9092
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
consumer:
group-id: my-group
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.JsonDeserializer
properties:
spring.json.trusted.packages: "*"
5.2. 실습 10: Kafka 기반 서비스 간 비동기 처리
멀티 모듈 프로젝트 구성:
ecommerce-eda/
├── common-event/ # 공통 이벤트 정의
├── order-service/ # 주문 서비스 (프로듀서)
├── inventory-service/ # 재고 서비스 (컨슈머)
├── notification-service/ # 알림 서비스 (컨슈머)
└── analytics-service/ # 분석 서비스 (컨슈머)
주요 학습 포인트:
- 트랜잭셔널 아웃박스(Transactional Outbox) 패턴
- 멱등성(Idempotency) 처리
- 데드 레터 큐(Dead Letter Queue) 구성
- 컨슈머 그룹과 파티션 할당
6. 정리
6.1. 동기 vs 비동기 통신 선택 요약
| 기준 | 동기 통신 | 비동기 통신 |
| 응답 시간 | 즉시 응답 필요 | 지연 허용 |
| 결합도 | 높음 | 낮음 |
| 장애 격리 | 어려움 | 쉬움 |
| 확장성 | 제한적 | 높음 |
| 구현 복잡도 | 낮음 | 높음 |
| 추적/디버깅 | 쉬움 | 어려움 |
6.2. Kafka vs RabbitMQ 선택 요약
| 상황 | 추천 |
| 작업 큐, 복잡한 라우팅 | RabbitMQ |
| 대용량 이벤트 스트리밍 | Kafka |
| 이벤트 소싱 | Kafka |
| RPC 스타일 통신 | RabbitMQ |
| 로그 수집 | Kafka |
| 트랜잭셔널 메시징 | RabbitMQ |
6.3. 다음 글 예고
이번 글에서는 비동기 통신과 이벤트 기반 아키텍처의 기본 개념을 살펴보았다. 다음 글부터는 ADVANCED 시리즈로 들어가 더 깊이 있는 주제들을 다룰 예정이다. 첫 번째 ADVANCED 글에서는 데이터 관리 전략으로 Database per Service 패턴, CQRS, 샤딩(Sharding)에 대해 알아보겠다.
'MSA > MSA 아키텍처' 카테고리의 다른 글
| [ADVANCED #2] 분산 트랜잭션과 SAGA 패턴 완전 정리 (0) | 2026.03.02 |
|---|---|
| [ADVANCED #1] MSA 데이터 관리 전략 (DB per Service, CQRS, Sharding) (0) | 2026.03.02 |
| [BASIC #3] MSA 동기 통신 전략과 API Gateway 패턴 (0) | 2026.03.02 |
| [BASIC #2] Microservice Architecture 개요와 서비스 분해 전략 (0) | 2026.03.02 |
| [BASIC #1] 마이크로서비스 아키텍처의 이해: Monolithic에서 MSA까지 (0) | 2026.03.02 |
