소개: 모듈 간 통신
모듈 간의 통신은 모듈형 모놀리스의 심장 박동입니다. 와 달리 네트워크(HTTP, gRPC, 메시지 브로커)를 통해서만 통신이 이루어지는 마이크로서비스 모듈식 모노리스에서는 다음 중 하나를 선택할 수 있다는 장점이 있습니다. 동기식 통신 진행 중 e 이벤트 기반 비동기 통신, 각각 결합, 대기 시간 및 디버깅 가능성 측면에서 특정 절충안이 있습니다.
이 글에서는 세 가지 주요 의사소통 패턴인 직접 통화를 분석해 보겠습니다. 메서드, 진행 중인 이벤트 버스, 비동기 메시징 등이 있습니다. 우리는 이를 구현하는 방법을 살펴보겠습니다. 스프링부트 상황에 따라 둘 중 하나를 선택해야 하는 경우도 있습니다.
이 기사에서 배울 내용
- 동기식 통신: 인터페이스를 통한 메서드 호출, 장점 및 제한 사항
- In-process 이벤트 버스: 동일한 JVM 내의 pub-sub
- 비동기 메시징: 고급 분리를 위한 RabbitMQ 및 Kafka
- 중재자 패턴: 모듈 간 종속성 분리
- 전체 CQRS: 명령과 쿼리 분리
- 일관성: 강력한 일관성과 최종 일관성
- 실패 관리: 재시도, 데드 레터 큐, 서킷 브레이커
- 비동기 스트림의 디버깅 및 관찰 가능성
패턴 1: 동기 통신(직접 메서드 호출)
가장 간단한 패턴: 모듈이 공개 인터페이스 메소드를 직접 호출합니다. 다른 모듈. 통화가 이루어집니다 진행 중, 네트워크 오버헤드 없이 표준 Java 메서드 호출과 동일한 대기 시간(나노초)입니다.
장점
- 간단: 추가 인프라 없음, 메시지 브로커 없음
- 강력한 일관성: 통화가 동일한 거래의 일부입니다.
- 직접 디버깅: 전체 스택 추적, 중단점, 단계별 실행
- 유형 안전: 컴파일러는 모듈 간의 계약을 확인합니다.
단점
- 시간적 결합: 수신자가 응답할 때까지 발신자를 차단합니다.
- 계단식 오류: 호출된 모듈이 실패하면 호출자도 실패합니다.
- 직접적인 의존성: 호출 모듈은 수신자의 인터페이스를 알고 있습니다.
// Comunicazione sincrona: Order chiama Catalog via interfaccia
@Service
class OrderServiceImpl implements OrderModuleApi {
// Dipendenza esplicita verso l'API del modulo Catalog
private final CatalogModuleApi catalogModule;
private final UserModuleApi userModule;
@Override
@Transactional
public OrderDto createOrder(CreateOrderCommand cmd) {
// Chiamata sincrona 1: verifica utente
UserDto user = userModule.findById(cmd.userId())
.orElseThrow(() -> new UserNotFoundException(cmd.userId()));
// Chiamata sincrona 2: verifica prodotti
List<ProductDto> products = cmd.productIds().stream()
.map(id -> catalogModule.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id)))
.toList();
// Logica locale del modulo Order
Order order = Order.create(user.id(), products);
orderRepository.save(order);
return order.toDto();
// Tutto avviene nella stessa transazione ACID
}
}
패턴 2: 이벤트 버스 진행 중
L'진행 중인 이벤트 버스 패턴을 구현합니다 게시-구독 JVM 자체 내에서. 하나의 모듈이 이벤트를 게시하고 0개 이상의 모듈이 이벤트를 구독합니다. 그들은 사건에 반응합니다. 생산자는 소비자를 알지 못합니다. 디커플링이 완료되었습니다.
Spring Framework는 다음을 통해 네이티브 프로세스 내 이벤트 버스를 제공합니다.
ApplicationEventPublisher 및 주석 @EventListener
e @TransactionalEventListener.
장점
- 디커플링: 생산자는 소비자를 모른다
- 확장성: 생산자를 변경하지 않고도 새로운 Consumer를 추가할 수 있습니다.
- 인프라 없음: 외부 브로커 없이 JVM 자체 내에서 작동
단점
- 더 복잡한 디버깅: 흐름이 선형적이지 않으므로 이벤트를 추적해야 합니다.
- 실행 순서: 리스너 순서는 보장되지 않습니다.
- 이벤트 손실: 애플리케이션이 충돌하면 메모리에 있는 이벤트가 손실됩니다.
// Event Bus in-process con Spring Events
// 1. Definizione dell'evento (nel package api del modulo)
public record OrderCreatedEvent(
UUID orderId,
UUID userId,
Money total,
List<UUID> productIds,
Instant timestamp
) {}
// 2. Pubblicazione dell'evento (nel modulo Order)
@Service
class OrderServiceImpl {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public OrderDto createOrder(CreateOrderCommand cmd) {
Order order = Order.create(cmd);
orderRepository.save(order);
// Pubblica evento DOPO il commit della transazione
eventPublisher.publishEvent(new OrderCreatedEvent(
order.getId(),
order.getUserId(),
order.getTotal(),
order.getProductIds(),
Instant.now()
));
return order.toDto();
}
}
// 3. Consumatori in moduli diversi
// Modulo Payment: processa il pagamento
@Service
class PaymentEventHandler {
@TransactionalEventListener(phase = AFTER_COMMIT)
void handleOrderCreated(OrderCreatedEvent event) {
paymentService.initiatePayment(
event.userId(), event.total()
);
}
}
// Modulo Inventory: riserva lo stock
@Service
class InventoryEventHandler {
@TransactionalEventListener(phase = AFTER_COMMIT)
void handleOrderCreated(OrderCreatedEvent event) {
for (UUID productId : event.productIds()) {
inventoryService.reserveStock(productId);
}
}
}
// Modulo Notification: invia conferma
@Service
class NotificationEventHandler {
@TransactionalEventListener(phase = AFTER_COMMIT)
void handleOrderCreated(OrderCreatedEvent event) {
notificationService.sendOrderConfirmation(
event.userId(), event.orderId()
);
}
}
TransactionalEventListener 대 EventListener
@EventListener 리스너를 실행합니다 ~ 동안 거래
제조업체. 리스너가 실패하면 원래 트랜잭션이 롤백됩니다.
@TransactionalEventListener(phase = AFTER_COMMIT) 리스너를 실행합니다
후에 이벤트가 처리되었는지 확인하는 트랜잭션 커밋
원래 거래가 성공한 경우에만 가능합니다.
패턴 3: 중재자 패턴
Il 중재자 패턴 의사소통을 조정하는 중개자를 소개합니다. 모듈 사이. 대상 모듈 인터페이스를 직접 주입하는 대신 모듈 명령이나 쿼리를 중재자에게 보내고 중재자는 이를 적절한 처리기로 라우팅합니다.
// Mediator Pattern: disaccoppia completamente i moduli
// Interfaccia del Mediator
public interface Mediator {
<R> R send(Command<R> command);
<R> R query(Query<R> query);
}
// Comando: CreateOrder restituisce OrderDto
public record CreateOrderCommand(
UUID userId, List<UUID> productIds
) implements Command<OrderDto> {}
// Handler nel modulo Order
@Component
class CreateOrderHandler implements CommandHandler<CreateOrderCommand, OrderDto> {
@Override
public OrderDto handle(CreateOrderCommand cmd) {
Order order = Order.create(cmd.userId(), cmd.productIds());
orderRepository.save(order);
return order.toDto();
}
}
// Implementazione del Mediator
@Component
class MediatorImpl implements Mediator {
private final Map<Class<?>, CommandHandler<?, ?>> handlers;
@SuppressWarnings("unchecked")
public <R> R send(Command<R> command) {
CommandHandler<Command<R>, R> handler =
(CommandHandler<Command<R>, R>) handlers.get(command.getClass());
if (handler == null) {
throw new NoHandlerException(command.getClass());
}
return handler.handle(command);
}
}
// Utilizzo: il chiamante non conosce il modulo target
@RestController
class OrderController {
private final Mediator mediator;
@PostMapping("/orders")
OrderDto createOrder(@RequestBody CreateOrderRequest request) {
return mediator.send(new CreateOrderCommand(
request.userId(), request.productIds()
));
}
}
패턴 4: RabbitMQ를 사용한 비동기 메시징
필요한 시나리오의 경우 고급 디커플링, 실패에 대한 회복력 e 재시도 가능성, 다음과 같은 외부 메시지 브로커 RabbitMQ 보증을 제공합니다 진행 중인 이벤트 버스에 추가: 메시지 지속성, 배달 못한 편지 대기열, 자동 재시도 및 여러 인스턴스에 대한 배포.
// Messaging asincrono con RabbitMQ e Spring AMQP
// Configurazione
@Configuration
class RabbitConfig {
@Bean
public TopicExchange orderExchange() {
return new TopicExchange("order.events");
}
@Bean
public Queue paymentQueue() {
return QueueBuilder.durable("payment.order-created")
.withArgument("x-dead-letter-exchange", "order.events.dlx")
.build();
}
@Bean
public Binding paymentBinding() {
return BindingBuilder
.bind(paymentQueue())
.to(orderExchange())
.with("order.created");
}
}
// Produttore nel modulo Order
@Service
class OrderEventPublisher {
private final RabbitTemplate rabbitTemplate;
public void publishOrderCreated(OrderCreatedEvent event) {
rabbitTemplate.convertAndSend(
"order.events", // exchange
"order.created", // routing key
event // payload
);
}
}
// Consumatore nel modulo Payment
@Component
class PaymentOrderListener {
@RabbitListener(queues = "payment.order-created")
public void handleOrderCreated(OrderCreatedEvent event) {
try {
paymentService.processPayment(
event.userId(), event.total()
);
} catch (Exception e) {
// Il messaggio viene reinviato alla DLQ dopo 3 retry
throw new AmqpRejectAndDontRequeueException(e);
}
}
}
CQRS: 명령과 쿼리 분리
패턴 CQRS(명령 쿼리 책임 분리) 완전히 분리된다 쿼리 경로(읽기)에서 명령 경로(쓰기)를 가져옵니다. 명령이 통과됨 모듈 API를 사용하고 상태를 변경합니다. 쿼리는 최적화된 비정규화된 투영에 액세스합니다. 독서를 위해.
// CQRS: Command side (scrittura)
// Passa attraverso le API dei moduli con validazione
@Service
class OrderCommandService {
@Transactional
public UUID createOrder(CreateOrderCommand cmd) {
// Validazione, logica di dominio, persistenza
Order order = Order.create(cmd);
orderRepo.save(order);
events.publish(new OrderCreatedEvent(order));
return order.getId();
}
}
// CQRS: Query side (lettura)
// Accede a viste denormalizzate, nessuna logica di dominio
@Service
class OrderQueryService {
private final OrderReadModelRepository readModelRepo;
// Query veloce: la vista contiene già tutti i dati necessari
public OrderDetailsView getOrderDetails(UUID orderId) {
return readModelRepo.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
// Query di lista con filtri
public Page<OrderSummaryView> searchOrders(
OrderSearchCriteria criteria, Pageable pageable) {
return readModelRepo.search(criteria, pageable);
}
}
// Vista denormalizzata: contiene dati da più moduli
@Entity
public class OrderDetailsView {
private UUID orderId;
private String customerName; // dal modulo User
private String customerEmail; // dal modulo User
private List<OrderItemView> items;
private String productNames; // dal modulo Catalog
private BigDecimal total;
private String paymentMethod; // dal modulo Payment
private String paymentStatus; // dal modulo Payment
}
파산관리
비동기 흐름에서는 실패가 불가피합니다. 이를 관리하는 주요 패턴은 다음과 같습니다.
지수 백오프로 재시도
핸들러가 실패하면 1초, 2초, 4초, 8초 간격을 늘려 다시 시도하세요. 숫자 뒤에 최대 시도 횟수를 초과하면 메시지가 다음 위치로 이동됩니다. 배달 못한 편지 대기열(DLQ) 수동 분석용.
배달 못한 편지 대기열
모든 재시도 후 처리할 수 없는 메시지가 대기열로 이동되었습니다. 헌신. 운영자 또는 자동화된 프로세스는 실패한 메시지를 분석하고 재시도할 수 있습니다.
회로 차단기
대상 모듈에 반복적으로 오류가 발생하면 회로 차단기가 통화를 방해하다 일정 기간 동안 이미 어려움을 겪고 있는 모듈에 과부하가 걸리는 것을 방지합니다. 시간 초과 후에는 회로 차단기를 점차적으로 다시 시도하십시오.
디버깅 및 관찰 가능성
비동기 스트림은 동기 호출보다 디버그하기가 더 어렵습니다. 여기 있습니다 기본 관행:
- 상관관계 ID: 각 흐름에는 모든 이벤트와 호출을 통해 실행되는 고유 ID가 있습니다.
- 구조화된 로깅: 상관 ID, 소스 모듈, 대상 모듈이 포함된 구조화된 로그
- 이벤트 매장: 추적 및 재생을 위해 게시된 모든 이벤트를 저장합니다.
- 건강검진: 모든 모듈과 브로커가 작동하는지 확인
다음 기사
다음 글에서는 ''이라는 주제를 다루겠습니다. 배포 및 확장 모듈러의 모놀리스: 블루-그린 배포 전략, 점진적 릴리스를 위한 기능 플래그, 자동 크기 조정 지능형 메트릭 기반, 모듈을 마이크로서비스로 추출하는 시기 격리된 확장성.







