[Spring Boot] 비동기 처리와 메시징이란
KUKJIN LEE • 3일 전 작성
애플리케이션 규모가 커지고 서비스 요청량이 증가함에 따라 동기적 요청/응답 방식은 시스템 부하, 응답 지연, 처리량 한계 등의 문제를 야기할 수 있습니다. 이때 비동기 처리(Asynchronous Processing)와 메시징(Messaging) 아키텍처는 이러한 한계를 극복하고, 유연하고 확장성 있는 시스템을 구축할 수 있습니다.
비동기 처리(Asynchronous Processing)란?
비동기 처리는 클라이언트 요청에 대해 서버가 즉시 처리 결과를 반환하는 대신, 백그라운드에서 처리가 완료될 때까지 기다리지 않고 바로 응답을 종료하거나, 이후에 결과를 전달받는 방식입니다.
-
성능 및 확장성 향상: 요청을 동기적으로 처리할 때 발생하는 스레드 블로킹을 줄여, 더 많은 요청을 동시에 처리할 수 있습니다.
-
자원 효율성 극대화: 스레드나 CPU와 같은 시스템 자원을 효율적으로 활용하여 병렬 처리 성능을 개선합니다.
-
유연한 시스템 구성: 프런트엔드, 백엔드, 데이터 처리 파이프라인 등 다양한 계층 간의 결합도를 낮춰, 각 서비스나 컴포넌트별로 독립적으로 개발, 배포, 확장이 가능합니다.
메시징(Messaging)이란?
메시징은 시스템 컴포넌트 간에 데이터를 교환하기 위해 메시지 브로커(Message Broker)나 큐(Queue)와 같은 중간 매개체를 사용하는 통신 패턴입니다. 전통적인 동기 HTTP 호출과 달리, 메시징 기반 통신은 비동기적으로 동작합니다.
-
비동기 통신: 발신 측(Producer)이 메시지를 큐나 토픽(Topic)에 넣으면, 수신 측(Consumer)은 필요할 때 메시지를 가져가 처리합니다. 이 과정은 즉각적인 응답이 필요하지 않으므로 서비스 간 느슨한 결합을 실현합니다.
-
내장된 버퍼링: 메시지 브로커나 큐는 자연스럽게 트래픽 스파이크를 흡수하는 버퍼 역할을 합니다. 갑작스러운 요청 급증에도 사용자가 안정적으로 메시지를 처리할 수 있습니다.
-
장애 내성(Fault Tolerance): 한 서비스가 일시적으로 다운되더라도, 메시지는 브로커에 쌓여 유지되기 때문에 해당 서비스가 복구된 후 다시 메시지를 처리할 수 있습니다.
이벤트 기반 아키텍처(Event-Driven Architecture)에서의 역할
이벤트 기반 아키텍처는 특정 이벤트(데이터 변화나 상태 변경 등)가 발생했을 때 이를 트리거로 하여 다른 서비스나 컴포넌트가 동작하는 구조를 의미합니다.
-
이벤트 발행/구독(Publish/Subscribe) 모델: 특정 이벤트가 발생하면 Producer 서비스는 해당 이벤트를 토픽에 발행(Publish)하고, 구독(Subscribe) 중인 서비스들은 이 이벤트를 비동기적으로 받아 처리합니다.
-
데이터 흐름의 명확한 분리: 각 서비스는 이벤트를 소비(Consume)하는 비즈니스 로직에만 집중하면 되며, 이벤트 전파나 전달은 메시징 인프라에 맡기므로 구현 복잡도를 낮출 수 있습니다.
-
확장 용이성: 새로운 서비스가 이벤트를 구독하고자 할 때, 기존 서비스 코드를 수정할 필요 없이 메시지 브로커 설정만으로 연동을 수행할 수 있어 확장이 쉽습니다.
예시 코드
package com.example.messaging;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
@Bean
public DirectExchange orderExchange() {
return new DirectExchange("order.exchange");
}
@Bean
public Queue orderQueue() {
return new Queue("order.created.queue", true);
}
@Bean
public Binding orderBinding(Queue orderQueue, DirectExchange orderExchange) {
return BindingBuilder.bind(orderQueue).to(orderExchange).with("order.created");
}
}
package com.example.messaging;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class OrderService {
private final RabbitTemplate rabbitTemplate;
public OrderService(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
public void createOrder(String productId, int quantity) {
Order order = new Order();
order.setId(UUID.randomUUID().toString());
order.setProductId(productId);
order.setQuantity(quantity);
rabbitTemplate.convertAndSend("order.exchange", "order.created", order);
}
}
코드를 살펴보면, bind()를 통해서 작동합니다. 주문 생성 후 바로 응답을 반환하지만, 실제 업데이트, 후속 처리 로직은 메시지 사용자 측에서 비동기적으로 처리됩니다. 서비스와 재고 관리 서비스 간 결합도를 낮추고, 스케일 아웃 시에도 유연하게 대응할 수 있습니다.