서론: 왜 이벤트 드리븐 아키텍처가 필요한가?
현대 소프트웨어 개발에서 마이크로서비스 아키텍처는 선택이 아닌 필수가 되었습니다.
하지만 서비스 간 통신과 데이터 일관성을 유지하는 것은 여전히 큰 도전입니다.
이때 이벤트 드리븐 아키텍처가 해답을 제시합니다.
스프링 클라우드 스트림은 이러한 복잡한 문제를 해결하기 위해 등장한 Spring 생태계의 핵심 프레임워크입니다.
메시지 브로커를 추상화하고, 개발자가 비즈니스 로직에만 집중할 수 있도록 도와줍니다.
이 글에서는 Spring Cloud Stream을 활용한 이벤트 드리븐 마이크로서비스 구축 방법을 실무 관점에서 상세히 알아보겠습니다.
Spring Cloud Stream 핵심 개념 이해
바인더(Binder)와 바인딩(Binding)
스프링 클라우드 스트림의 핵심은 바인더 개념입니다.
바인더는 애플리케이션과 메시지 브로커(Kafka, RabbitMQ, Azure Service Bus 등) 사이의 추상화 계층을 제공합니다.
바인딩은 애플리케이션 내부의 채널과 외부 메시지 브로커를 연결하는 역할을 담당합니다.
이를 통해 개발자는 특정 메시지 브로커에 종속되지 않고 비즈니스 로직을 구현할 수 있습니다.
함수형 프로그래밍 모델
Spring Cloud Stream 3.0부터 함수형 프로그래밍 모델이 도입되었습니다.
기존의 @EnableBinding
어노테이션 대신 java.util.function
패키지의 함수형 인터페이스를 활용합니다.
Supplier<T>
: 메시지 생성 (Producer)Consumer<T>
: 메시지 소비 (Consumer)Function<T, R>
: 메시지 변환 (Processor)
개발 환경 설정과 기본 구성
의존성 추가
Maven 프로젝트에서 스프링 클라우드 스트림을 사용하려면 다음 의존성을 추가해야 합니다:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
기본 설정 구성
application.yml
파일에서 이벤트 드리븐 통신을 위한 기본 설정을 구성합니다:
spring:
cloud:
stream:
kafka:
binder:
brokers: localhost:9092
auto-create-topics: true
bindings:
orderCreated-out-0:
destination: order-events
content-type: application/json
orderProcessed-in-0:
destination: order-events
group: order-service
이 설정을 통해 마이크로서비스 간 안정적인 메시지 전달이 가능해집니다.
이벤트 생성자(Producer) 구현
주문 생성 이벤트 발행
이벤트 드리븐 시스템에서 핵심은 도메인 이벤트의 정확한 모델링입니다.
주문 생성 시나리오를 예로 들어보겠습니다:
@RestController
@RequiredArgsConstructor
public class OrderController {
private final OrderEventPublisher orderEventPublisher;
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
// 주문 생성 로직
Order order = orderService.createOrder(request);
// 이벤트 발행
OrderCreatedEvent event = new OrderCreatedEvent(
order.getId(),
order.getCustomerId(),
order.getTotalAmount(),
Instant.now()
);
orderEventPublisher.publishOrderCreated(event);
return ResponseEntity.ok(OrderResponse.from(order));
}
}
함수형 방식으로 이벤트 발행
스프링 클라우드 스트림의 함수형 모델을 활용한 이벤트 발행:
@Component
public class OrderEventPublisher {
private final StreamBridge streamBridge;
public OrderEventPublisher(StreamBridge streamBridge) {
this.streamBridge = streamBridge;
}
public void publishOrderCreated(OrderCreatedEvent event) {
streamBridge.send("orderCreated-out-0", event);
}
}
StreamBridge를 사용하면 동적으로 메시지를 발행할 수 있어 더욱 유연한 이벤트 드리븐 시스템을 구축할 수 있습니다.
이벤트 소비자(Consumer) 구현
주문 처리 이벤트 소비
마이크로서비스 환경에서 이벤트 소비는 시스템의 반응성과 확장성을 결정하는 중요한 요소입니다:
@Component
public class OrderEventHandler {
private final PaymentService paymentService;
private final InventoryService inventoryService;
@Bean
public Consumer<OrderCreatedEvent> orderProcessor() {
return event -> {
try {
// 결제 처리
paymentService.processPayment(event.getOrderId(), event.getTotalAmount());
// 재고 차감
inventoryService.decreaseStock(event.getOrderId());
// 처리 완료 이벤트 발행
publishOrderProcessed(event.getOrderId());
} catch (Exception e) {
// 에러 처리 및 보상 트랜잭션
handleOrderProcessingError(event, e);
}
};
}
}
에러 처리와 재시도 전략
이벤트 드리븐 시스템에서 장애 복구는 필수적입니다.
Spring Cloud Stream은 다양한 에러 처리 전략을 제공합니다:
spring:
cloud:
stream:
bindings:
orderProcessor-in-0:
destination: order-events
group: payment-service
consumer:
max-attempts: 3
back-off-initial-interval: 1000
back-off-multiplier: 2
default-retryable: true
메시지 변환과 직렬화
JSON 직렬화 설정
스프링 클라우드 스트림에서 메시지 직렬화는 서비스 간 호환성을 보장하는 핵심 요소입니다:
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "eventType"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = OrderCreatedEvent.class, name = "ORDER_CREATED"),
@JsonSubTypes.Type(value = OrderProcessedEvent.class, name = "ORDER_PROCESSED")
})
public abstract class DomainEvent {
private String eventId;
private Instant eventTime;
private String eventType;
// getters, setters
}
스키마 레지스트리 활용
대규모 마이크로서비스 환경에서는 스키마 레지스트리를 통한 스키마 진화 관리가 중요합니다:
spring:
cloud:
stream:
schema-registry-client:
endpoint: http://localhost:8081
bindings:
orderCreated-out-0:
destination: order-events
producer:
use-native-encoding: true
orderProcessor-in-0:
destination: order-events
consumer:
use-native-decoding: true
고급 기능 활용
메시지 파티셔닝
이벤트 드리븐 시스템의 성능 최적화를 위해 메시지 파티셔닝을 활용할 수 있습니다:
spring:
cloud:
stream:
bindings:
orderCreated-out-0:
destination: order-events
producer:
partition-key-expression: payload.customerId
partition-count: 3
orderProcessor-in-0:
destination: order-events
consumer:
partitioned: true
instance-count: 3
instance-index: 0
메시지 필터링
조건부 메시지 처리를 위한 필터링 기능:
@Bean
public Consumer<OrderCreatedEvent> highValueOrderProcessor() {
return event -> {
if (event.getTotalAmount().compareTo(BigDecimal.valueOf(1000)) > 0) {
// 고액 주문 특별 처리
specialOrderProcessingService.processHighValueOrder(event);
}
};
}
모니터링과 디버깅
액추에이터를 통한 메트릭 수집
스프링 클라우드 스트림 애플리케이션의 상태를 모니터링하기 위한 설정:
management:
endpoints:
web:
exposure:
include: health,info,metrics,bindings
endpoint:
health:
show-details: always
분산 추적
마이크로서비스 환경에서 메시지 흐름을 추적하기 위해 Spring Cloud Sleuth를 활용:
@Component
public class OrderEventHandler {
@NewSpan
@Bean
public Consumer<OrderCreatedEvent> orderProcessor() {
return event -> {
// 트레이스 정보가 자동으로 전파됨
Span span = tracer.nextSpan().name("order-processing");
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
span.tag("order.id", event.getOrderId());
span.tag("customer.id", event.getCustomerId());
processOrder(event);
} finally {
span.end();
}
};
}
}
테스트 전략
통합 테스트
이벤트 드리븐 시스템의 통합 테스트를 위한 TestBinder 활용:
@SpringBootTest
@TestPropertySource(properties = {
"spring.cloud.stream.bindings.orderCreated-out-0.destination=test-orders",
"spring.cloud.stream.bindings.orderProcessor-in-0.destination=test-orders"
})
class OrderEventIntegrationTest {
@Autowired
private TestBinder testBinder;
@Test
void shouldProcessOrderCreatedEvent() {
// Given
OrderCreatedEvent event = new OrderCreatedEvent("order-1", "customer-1",
new BigDecimal("100.00"), Instant.now());
// When
testBinder.send("orderCreated-out-0", event);
// Then
Message<OrderProcessedEvent> received = testBinder.receive("orderProcessor-in-0");
assertThat(received.getPayload().getOrderId()).isEqualTo("order-1");
}
}
단위 테스트
함수형 모델의 장점을 활용한 단위 테스트:
@ExtendWith(MockitoExtension.class)
class OrderEventHandlerTest {
@Mock
private PaymentService paymentService;
@Mock
private InventoryService inventoryService;
@Test
void shouldProcessOrderSuccessfully() {
// Given
OrderEventHandler handler = new OrderEventHandler(paymentService, inventoryService);
OrderCreatedEvent event = createTestEvent();
// When
handler.orderProcessor().accept(event);
// Then
verify(paymentService).processPayment(event.getOrderId(), event.getTotalAmount());
verify(inventoryService).decreaseStock(event.getOrderId());
}
}
실무 적용 시 고려사항
메시지 순서 보장
이벤트 드리븐 시스템에서 메시지 순서가 중요한 경우:
spring:
cloud:
stream:
kafka:
bindings:
orderCreated-out-0:
producer:
configuration:
enable.idempotence: true
max.in.flight.requests.per.connection: 1
중복 메시지 처리
마이크로서비스 환경에서 중복 메시지 처리를 위한 멱등성 보장:
@Component
public class IdempotentOrderProcessor {
private final RedisTemplate<String, String> redisTemplate;
@Bean
public Consumer<OrderCreatedEvent> orderProcessor() {
return event -> {
String idempotencyKey = "order:" + event.getOrderId();
// 중복 체크
if (redisTemplate.hasKey(idempotencyKey)) {
return; // 이미 처리된 메시지
}
try {
processOrder(event);
redisTemplate.opsForValue().set(idempotencyKey, "processed",
Duration.ofHours(24));
} catch (Exception e) {
// 에러 처리
handleProcessingError(event, e);
}
};
}
}
백프레셔(Backpressure) 처리
시스템 부하 상황에서의 백프레셔 처리:
spring:
cloud:
stream:
bindings:
orderProcessor-in-0:
consumer:
concurrency: 3
max-concurrency: 10
use-native-decoding: true
kafka:
bindings:
orderProcessor-in-0:
consumer:
configuration:
max.poll.records: 100
fetch.min.bytes: 1024
성능 최적화 전략
배치 처리
대량 메시지 처리를 위한 배치 모드:
@Component
public class BatchOrderProcessor {
@Bean
public Consumer<List<OrderCreatedEvent>> batchOrderProcessor() {
return events -> {
// 배치 단위로 처리
List<String> orderIds = events.stream()
.map(OrderCreatedEvent::getOrderId)
.collect(Collectors.toList());
paymentService.processBatchPayments(orderIds);
inventoryService.decreaseBatchStock(orderIds);
};
}
}
메모리 최적화
메모리 효율적인 메시지 처리:
spring:
cloud:
stream:
kafka:
binder:
configuration:
receive.buffer.bytes: 65536
send.buffer.bytes: 131072
bindings:
orderProcessor-in-0:
consumer:
configuration:
fetch.max.bytes: 52428800
max.partition.fetch.bytes: 1048576
마무리: 이벤트 드리븐 아키텍처의 미래
스프링 클라우드 스트림을 활용한 이벤트 드리븐 마이크로서비스 아키텍처는 현대 소프트웨어 개발의 핵심 패러다임입니다.
서비스 간 느슨한 결합을 통해 시스템의 확장성과 유지보수성을 크게 향상시킬 수 있습니다.
본 글에서 다룬 내용들을 실무에 적용할 때는 다음 사항들을 고려해야 합니다:
- 메시지 스키마의 진화 전략 수립
- 장애 복구 및 보상 트랜잭션 설계
- 모니터링 및 알림 체계 구축
- 성능 테스트 및 용량 계획
이벤트 드리븐 아키텍처는 단순히 기술적 선택이 아닌, 비즈니스 요구사항에 빠르게 대응할 수 있는 조직의 역량을 의미합니다.
Spring Cloud Stream의 지속적인 발전과 함께 더욱 효율적이고 안정적인 마이크로서비스 생태계를 구축해 나가시기 바랍니다.
참고 자료
'Spring & Spring Boot 실무 가이드' 카테고리의 다른 글
Cloud Run + AI 에이전트 자동 배포 파이프라인 구축 가이드 (0) | 2025.06.26 |
---|---|
Apache Camel로 엔터프라이즈 통합 패턴 구현하기: Spring Boot와 함께하는 실무 가이드 (0) | 2025.06.22 |
Spring Modulith로 모놀리식을 모듈화하기: 스프링 모듈리스 아키텍처 완벽 가이드 (0) | 2025.06.20 |
Spring Boot 테스트 컨테이너 실전 가이드 - Docker 없이 통합 테스트 자동화 (0) | 2025.06.18 |
Spring WebFlux 완벽 가이드: 리액티브 프로그래밍으로 대용량 트래픽 처리하기 (1) | 2025.06.10 |