BTP

Event Mesh — Durable vs Direct 구독 #shorts #SAP #BTP

▶ YouTube에서 보기

이 글에서 다룰 것

BTP에서 마이크로서비스를 REST 호출 체인으로만 엮으면 한 서비스의 장애가 전체로 번집니다. SAP Event Mesh는 이 결합을 끊는 Pub/Sub 계층입니다. 이 글은 토픽(topic) 구조, 큐(queue) 설정, 구독(subscription) 패턴 세 축을 실전 관점에서 다룹니다.

  • point-to-point 대신 Pub/Sub을 선택해야 하는 아키텍처 기준
  • 네임스페이스 기반 토픽 네이밍과 wildcard 구독 설계
  • durable 큐와 non-durable 소비의 차이, dead letter 큐 구성
  • Node.js + REST/AMQP로 발행-구독 파이프라인 직접 구성

사전 가정

BTP 서비스 인스턴스·서비스 키 생성 경험, OAuth2 client credentials 이해, Node.js 비동기 코드 경험을 전제로 합니다. AMQP/MQTT를 몰라도 따라올 수 있지만 Kafka·RabbitMQ 경험이 있으면 개념 대비가 빠릅니다.

테스트 환경

  • SAP BTP, Cloud Foundry 환경 (Event Mesh entitlement 필요)
  • SAP Event Mesh 서비스 플랜 default — 트라이얼은 dev 플랜(기능 제한)
  • Node.js 20 LTS, @sap/xb-msg-amqp-v100, axios
  • 프로토콜: 관리용 REST API, 메시징 REST API, AMQP 1.0 over WebSocket

SAP는 대규모 스트리밍용 advanced event mesh(AEM)도 별도 제공하지만, 이 글의 대상은 BTP 서비스형 Event Mesh입니다. default 플랜은 메시지 1건 최대 1MB 제약이 있어 대용량 페이로드는 참조 URL 전달을 권장합니다.

왜 Pub/Sub인가 — 토픽, 큐, 구독의 삼각관계

point-to-point는 발신자가 수신자의 큐 주소를 직접 알아야 해 수신 서비스가 늘 때마다 발신 코드가 바뀝니다. Pub/Sub은 발행자가 이벤트를 토픽에 던질 뿐 누가 듣는지 모릅니다. 영업 서비스가 SalesOrderCreated를 발행하면 청구·재고·알림 서비스가 각자 구독으로 받아가고, 소비자가 늘어도 발행 코드는 그대로입니다.

우편 시스템에 비유하면:

  • 토픽 = 주소 체계. 저장소가 아닌 라우팅 규칙. 발행 순간 매칭 구독이 없으면 메시지는 사라집니다.
  • = 우편함. 유일하게 메시지를 보관(durable)하는 곳. 소비자가 오프라인이어도 쌓입니다.
  • 큐 구독 = 전입신고. "이 토픽 패턴을 이 큐로 배달하라"는 바인딩입니다.

소비 방식은 두 갈래입니다. durable 소비는 큐에 붙어 at-least-once 특성으로 받고, non-durable(다이렉트) 소비는 큐 없이 토픽에 직접 붙어(QoS 0) 오프라인 구간의 메시지가 유실됩니다. 유실 허용 시나리오가 아니라면 durable 큐 소비를 권장합니다.

토픽은 인스턴스 생성 시 지정한 네임스페이스(3세그먼트)가 접두어가 되고 뒤에 도메인/이벤트/버전을 붙이는 것이 일반적입니다.

{namespace}/{바운디드컨텍스트}/{이벤트명}/{버전}
stackline/sales/kr01/salesorder/SalesOrderCreated/v1
stackline/sales/kr01/salesorder/SalesOrderCancelled/v1
stackline/sales/kr01/delivery/DeliveryBlocked/v1

wildcard 문자는 프로토콜별로 다릅니다. 큐 구독(브로커 측)은 *가 한 세그먼트, 맨 끝 >가 나머지 전체 레벨을 매칭하고, MQTT 구독은 +#을 씁니다. 예로 stackline/sales/kr01/salesorder/>는 salesorder 도메인의 모든 이벤트·버전을 한 큐로 모읍니다. 정확한 토픽 1:1 구독, 도메인 단위 wildcard 구독, 다이렉트 토픽 소비 — 이 셋이 실무의 대표 구독 패턴입니다.

실전 예제 3단계

1단계 — 큐 생성, 구독 바인딩, 첫 발행 (REST)

서비스 키의 management URI와 httprest URI, 토큰 엔드포인트는 각각 분리되어 있습니다. 먼저 관리 API로 큐와 구독을 만듭니다.

const axios = require('axios');

async function fetchToken(tokenUrl, clientId, clientSecret) {
  const res = await axios.post(`${tokenUrl}/oauth/token`,
    new URLSearchParams({ grant_type: 'client_credentials' }),
    { auth: { username: clientId, password: clientSecret } });
  return res.data.access_token;
}

const MGMT = process.env.EM_MGMT_URI; // 서비스 키 management[0].uri
const queue = encodeURIComponent('stackline/sales/kr01/q/billing-inbox');
const pattern = encodeURIComponent('stackline/sales/kr01/salesorder/>');

async function provision(token) {
  const h = { Authorization: `Bearer ${token}` };
  // 큐 생성 (idempotent PUT)
  await axios.put(`${MGMT}/hub/rest/api/v1/management/messaging/queues/${queue}`,
    { accessType: 'NON_EXCLUSIVE', maxQueueMessageCount: 100000 }, { headers: h });
  // 토픽 패턴 → 큐 바인딩
  await axios.put(
    `${MGMT}/hub/rest/api/v1/management/messaging/queues/${queue}/subscriptions/${pattern}`,
    {}, { headers: h });
}

발행은 메시징 REST API로 확인합니다. x-qos: 1이 durable 전달입니다.

const topic = encodeURIComponent('stackline/sales/kr01/salesorder/SalesOrderCreated/v1');
await axios.post(`${REST_URI}/messagingrest/v1/topics/${topic}/messages`,
  { orderId: 'SO-2026-08123', amount: 1490000, currency: 'KRW' },
  { headers: { Authorization: `Bearer ${msgToken}`, 'x-qos': 1 } });

순서 규칙 하나 — 구독이 발행보다 먼저여야 합니다. 토픽은 저장하지 않으므로 바인딩 전 발행분은 복구할 수 없습니다.

2단계 — AMQP 소비자에 에러 처리와 로깅 붙이기

운영 소비자는 AMQP 스트리밍 수신이 일반적입니다. 성공 시에만 ack하고 실패 시 failed 처리로 재전달을 유도합니다.

const msg = require('@sap/xb-msg-amqp-v100');

const client = new msg.Client({
  amqp: {
    uri: process.env.EM_AMQP_URI, // 서비스 키 messaging[].uri (amqp10ws)
    oa2: { endpoint: `${TOKEN_URL}/oauth/token`,
           client: CLIENT_ID, secret: CLIENT_SECRET, grant: 'client_credentials' }
  }
});

const stream = client.receiver('billing')
  .attach('queue:stackline/sales/kr01/q/billing-inbox');

stream.on('data', (message) => {
  const raw = message.payload.toString('utf8');
  try {
    const event = JSON.parse(raw);
    console.log(JSON.stringify({ level: 'info', at: 'billing-consumer',
      orderId: event.orderId, ts: Date.now() }));
    processInvoice(event);          // 비즈니스 로직
    message.done();                 // 성공 시에만 ack
  } catch (err) {
    console.error(JSON.stringify({ level: 'error', reason: err.message,
      preview: raw.slice(0, 200) }));
    message.failed();               // 재전달 대상 표시
  }
});

client.on('reconnecting', () => console.warn('EM reconnecting...'));
client.connect();

wildcard 구독 큐에는 여러 이벤트가 섞여 들어오므로 이벤트 타입 분기 라우터를 두는 패턴이 자주 쓰입니다. 페이로드를 CloudEvents 포맷으로 통일하면 팀 간 인터페이스 협상이 수월해집니다.

3단계 — dead letter 큐, 재전달 한도, 운영 안전장치

poison message가 무한 재전달되면 큐가 막힙니다. 재전달 한도와 DLQ로 격리합니다.

// DLQ 자체도 큐이므로 먼저 생성
const dlq = 'stackline/sales/kr01/q/billing-inbox.dlq';
await axios.put(`${MGMT}/hub/rest/api/v1/management/messaging/queues/${encodeURIComponent(dlq)}`,
  { maxQueueMessageCount: 10000 }, { headers: h });

// 본 큐에 DLQ 연결 + 운영 속성
await axios.put(`${MGMT}/hub/rest/api/v1/management/messaging/queues/${queue}`, {
  accessType: 'EXCLUSIVE',            // 순서 민감 시 단일 소비자 강제
  maxRedeliveryCount: 5,              // 5회 실패 시 DLQ로 이동
  deadMsgQueue: dlq,
  respectTtl: true,                   // 발행 시 지정한 TTL 준수
  maxDeliveredUnackedMsgsPerFlow: 50  // 소비자당 미확인 메시지 상한
}, { headers: h });

운영 체크 세 가지. 첫째, DLQ 적재 건수를 관리 API 큐 상태 조회(GET .../queues/{name})로 주기 점검하고 임계치 초과 시 알림. 둘째, at-least-once 특성상 중복 전달이 가능하므로 orderId 같은 비즈니스 키로 멱등 처리(이력 테이블 upsert가 가장 단순). 셋째, 발행 전용 앱에는 발행 권한만 가진 인스턴스를 바인딩하는 최소 권한 구성을 적용합니다.

자주 만나는 함정

Q1. 발행은 성공(2xx)인데 큐에 메시지가 없습니다.

발행 시점에 매칭 구독이 없었던 경우가 대부분입니다. 토픽은 저장소가 아니라 브로커가 조용히 버립니다. 프로비저닝에서 큐·구독 생성을 발행보다 항상 앞에 두세요.

Q2. 관리 API는 되는데 발행에서 401/403이 납니다.

관리 API와 메시징 API는 서비스 키 내 토큰 엔드포인트와 자격증명이 다릅니다. 각 프로토콜 블록(oa2)의 것을 정확히 매칭했는지 확인하세요.

Q3. wildcard 구독이 동작하지 않습니다.

큐 구독에 MQTT식 #을 넣으면 리터럴 문자로 취급됩니다. REST 경로의 패턴은 /까지 URL 인코딩(%2F)해야 합니다.

Q4. 같은 이벤트가 두 번 처리됐습니다.

at-least-once의 정상 동작 범위입니다. 소비자 멱등성을 반드시 갖추세요.

핵심 한 줄

토픽은 라우팅, 큐는 저장, 구독은 그 둘의 바인딩 — 발행자는 이벤트만 던지고, 내구성과 재처리(DLQ)는 큐 설정으로 해결하는 것이 SAP Event Mesh Pub/Sub 설계의 전부다.

이어서 보면 좋은 글

CAP(CDS)에서 @sap/cds 메시징 플러그인으로 Event Mesh를 선언적으로 붙이는 방법, S/4HANA 비즈니스 이벤트 수신 구성, 대규모 처리량에서 advanced event mesh로 넘어가는 판단 기준이 자연스러운 확장 경로입니다.

댓글 0

아직 댓글이 없습니다.