[BASIC #4] 비동기 통신과 Event-Driven Architecture

2026. 3. 2. 14:12·MSA/MSA 아키텍처

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. 비동기 통신의 장단점

장점:

  1. 느슨한 결합(Loose Coupling): 서비스가 서로를 직접 알 필요 없이 메시지 브로커를 통해 통신
  2. 탄력성(Resilience): 일부 서비스 장애가 전체 시스템으로 전파되지 않음
  3. 확장성(Scalability): 컨슈머를 여러 개 두어 병렬 처리 가능
  4. 부하 분산(Load Balancing): 피크 타임에도 메시지 큐가 버퍼 역할을 함
  5. 탄력적 처리(Elastic Processing): 처리 능력에 따라 메시지 소비 속도 조절

단점:

  1. 복잡성 증가: 메시지 브로커 도입으로 인프라 복잡도 상승
  2. 추적 어려움: 분산된 시스템에서 메시지 흐름 추적이 어려움
  3. 최종 일관성: 강한 일관성 대신 최종 일관성만 보장
  4. 메시지 순서 보장 어려움: 분산 환경에서 순서 보장이 복잡
  5. 디버깅 어려움: 비동기 흐름 디버깅이 동기 방식보다 어려움

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
'MSA/MSA 아키텍처' 카테고리의 다른 글
  • [ADVANCED #2] 분산 트랜잭션과 SAGA 패턴 완전 정리
  • [ADVANCED #1] MSA 데이터 관리 전략 (DB per Service, CQRS, Sharding)
  • [BASIC #3] MSA 동기 통신 전략과 API Gateway 패턴
  • [BASIC #2] Microservice Architecture 개요와 서비스 분해 전략
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[BASIC #4] 비동기 통신과 Event-Driven Architecture
상단으로

티스토리툴바