amqp-contract로 타입 안전한 AMQP 메시징 구축

발행: (2025년 12월 25일 오전 10:49 GMT+9)
10 min read
원문: Dev.to

Source: Dev.to

If you’ve worked with RabbitMQ or AMQP messaging in TypeScript, you’ve probably experienced the frustration of untyped messages, scattered validation logic, and the constant fear of runtime errors from mismatched data structures. What if there was a better way?

TypeScript에서 RabbitMQ 또는 AMQP 메시징을 다뤄본 적이 있다면, 타입이 지정되지 않은 메시지, 흩어져 있는 검증 로직, 그리고 데이터 구조가 맞지 않아 발생하는 런타임 오류에 대한 지속적인 두려움에 좌절감을 느꼈을 것입니다. 더 나은 방법이 있다면 어떨까요?

Today, I’m excited to introduce amqp‑contract — a TypeScript library that brings contract‑first development, end‑to‑end type safety, and automatic validation to AMQP messaging.

오늘은 amqp‑contract 를 소개하게 되어 기쁩니다 — 계약‑우선 개발, 엔드‑투‑엔드 타입 안전성, 그리고 자동 검증을 AMQP 메시징에 제공하는 TypeScript 라이브러리입니다.

전통적인 AMQP 개발의 문제

퍼블리셔 (타입 안전성 없음)

// ❌ 전통적인 접근 방식 – 타입 안전성 없음
import amqp from 'amqplib';

const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();

await channel.assertExchange('orders', 'topic', { durable: true });

channel.publish(
  'orders',
  'order.created',
  Buffer.from(
    JSON.stringify({
      orderId: 'ORD-123',
      amount: 99.99,
      // 필수 필드를 놓친 건가요?
    })
  )
);

컨슈머 (타입 정보 없음)

// ❌ 타입 정보 없음
channel.consume('order-processing', (msg) => {
  const data = JSON.parse(msg.content.toString()); // 알 수 없는 타입
  console.log(data.orderId); // 자동 완성도 없고, 검증도 안 됩니다
  // 이게 맞는 필드 이름일까요? 누가 알겠어요!
});

문제점

  • 타입 안전성 없음 – TypeScript의 이점이 메시징 경계에서 사라집니다.
  • 수동 검증 – 모든 메시지를 직접 검증해야 하며, 그렇지 않으면 런타임 오류가 발생할 수 있습니다.
  • 분산된 정의 – 메시지 구조가 암시적이거나 코드베이스 전역에 흩어져 있습니다.
  • 리팩토링 악몽 – 필드명을 변경하면 모든 사용처를 찾아야 합니다.
  • 문서와 코드 불일치 – 코드와 문서가 빠르게 동기화되지 않습니다.

amqp‑contract 시작하기

amqp‑contracttRPC, oRPC, ts‑rest와 같은 라이브러리에서 영감을 받아 계약‑우선 접근법으로 이러한 문제를 해결합니다. 엔드‑투‑엔드 타입‑안전 철학을 메시지 큐에 적용합니다.

1️⃣ 계약 정의

import {
  defineContract,
  defineExchange,
  defineQueue,
  definePublisher,
  defineConsumer,
  defineMessage,
  defineQueueBinding,
} from '@amqp-contract/contract';
import { z } from 'zod';

// AMQP resources
const ordersExchange = defineExchange('orders', 'topic', { durable: true });
const orderProcessingQueue = defineQueue('order-processing', { durable: true });

// Message schema
const orderMessage = defineMessage(
  z.object({
    orderId: z.string(),
    customerId: z.string(),
    items: z.array(
      z.object({
        productId: z.string(),
        quantity: z.number().int().positive(),
        price: z.number().positive(),
      })
    ),
    totalAmount: z.number().positive(),
    status: z.enum(['pending', 'processing', 'completed']),
  })
);

// Contract composition
export const contract = defineContract({
  exchanges: { orders: ordersExchange },
  queues: { orderProcessing: orderProcessingQueue },
  bindings: {
    orderBinding: defineQueueBinding(
      orderProcessingQueue,
      ordersExchange,
      { routingKey: 'order.created' }
    ),
  },
  publishers: {
    orderCreated: definePublisher(ordersExchange, orderMessage, {
      routingKey: 'order.created',
    }),
  },
  consumers: {
    processOrder: defineConsumer(orderProcessingQueue, orderMessage),
  },
});

2️⃣ 타입‑안전한 퍼블리싱

import { TypedAmqpClient } from '@amqp-contract/client';
import { contract } from './contract';

const clientResult = await TypedAmqpClient.create({
  contract,
  urls: ['amqp://localhost'],
});

if (clientResult.isError()) {
  throw clientResult.error;
}

const client = clientResult.value;

// ✅ 완전한 타입 지정 – TypeScript가 필요한 필드를 정확히 인식합니다
const result = await client.publish('orderCreated', {
  orderId: 'ORD-123',
  customerId: 'CUST-456',
  items: [{ productId: 'PROD-789', quantity: 2, price: 49.99 }],
  totalAmount: 99.98,
  status: 'pending',
});

result.match({
  Ok: () => console.log('✅ Published'),
  Error: (error) => console.error('❌ Failed:', error),
});

3️⃣ 타입‑안전한 컨슈밍

import { TypedAmqpWorker } from '@amqp-contract/worker';
import { contract } from './contract';

const workerResult = await TypedAmqpWorker.create({
  contract,
  handlers: {
    // ✅ `message`는 스키마를 기반으로 완전히 타입이 지정됩니다
    processOrder: async (message) => {
      console.log(`Processing order: ${message.orderId}`);
      console.log(`Customer: ${message.customerId}`);
      console.log(`Total: $${message.totalAmount}`);

      // ✅ 모든 필드에 대한 자동완성 지원
      message.items.forEach((item) => {
        console.log(`- ${item.quantity}x Product ${item.productId}`);
      });
    },
  },
  urls: ['amqp://localhost'],
});

workerResult.match({
  Ok: () => console.log('✅ Worker ready'),
  Error: (error) => { throw error; },
});

amqp‑contract 를 특별하게 만드는 주요 기능

🔒 엔드‑투‑엔드 타입 안전성

TypeScript 타입이 계약에서 퍼블리셔와 컨슈머로 자동으로 흐릅니다. 수동으로 타입 어노테이션을 달 필요가 없습니다. 스키마를 리팩터링하면 TypeScript가 즉시 업데이트가 필요한 모든 위치를 알려줍니다.

✅ 자동 검증

메시지는 Standard Schema v1 을 사용해 네트워크 경계에서 자동으로 검증됩니다. 이는 Zod, Valibot, ArkType 와 함께 동작하여 선호하는 검증 라이브러리를 자유롭게 선택할 수 있게 해줍니다.

🛠️ 컴파일‑타임 검사

TypeScript가 런타임 이전에 오류를 잡아냅니다:

// ❌ TypeScript error – "orderDeleted" doesn't exist
await client.publish('orderDeleted', { orderId: '123' });

// ❌ TypeScript error – missing handler
await TypedAmqpWorker.create({
  contract,
  handlers: {}, // forgot processOrder!
  urls: ['amqp://localhost'],
});

📄 AsyncAPI 3.0 생성

계약으로부터 AsyncAPI 사양을 자동으로 생성합니다:

import { AsyncAPIGenerator } from '@amqp-contract/asyncapi';
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4';

const generator = new AsyncAPIGenerator({
  schemaConverters: [new ZodToJsonSchemaConverter()],
});

const spec = await generator.generate(contract, {
  info: {
    title: 'Order Processing API',
    version: '1.0.0',
  },
  servers: {
    production: {
      host: 'rabbitmq.example.com:5672',
      protocol: 'amqp',
    },
  },
});

🎯 퍼스트‑클래스 NestJS 지원

NestJS 를 사용한다면 amqp-contract 가 전용 통합 패키지를 제공합니다:

import { Module } from '@nestjs/common';
import { AmqpWorkerModule } from '@amqp-contract/worker-nestjs';
import { AmqpClientModule } from '@amqp-contract/client-nestjs';

@Module({
  imports: [
    AmqpWorkerModule.forRoot({
      contract,
      handlers: {
        processOrder: async (message) => {
          console.log('Processing:', message.orderId);
        },
      },
      connection: process.env.RABBITMQ_URL,
    }),
    AmqpClientModule.forRoot({
      contract,
      connection: process.env.RABBITMQ_URL,
    }),
  ],
})
export class AppModule {}

amqp‑contract를 선택할까요?

Raw amqplib와 비교

기능amqp‑contractamqplib
✅ 타입 안전성
✅ 자동 검증
✅ 컴파일‑타임 검사
✅ 리팩토링 지원
✅ 코드 기반 문서화

다른 솔루션과 비교

다른 AMQP 라이브러리와 달리 amqp‑contract는:

  • 타입 안전성을 최우선으로 합니다 – 타입이 계약에서 파생됩니다.
  • Standard Schema v1을 사용합니다 – 여러 검증 라이브러리와 호환됩니다.
  • AsyncAPI 스펙을 생성합니다 – 자동 문서화.
  • 명시적 오류 처리를 제공합니다 – Result 타입을 사용합니다.
  • 프레임워크에 구애받지 않음 – 독립적으로 혹은 NestJS와 함께 사용할 수 있습니다.

시작하기

설치

# Core packages
pnpm add @amqp-contract/contract @amqp-contract/client @amqp-contract/worker

# Choose your schema library
pnpm add zod          # or valibot, or arktype

# AMQP client
pnpm add amqplib @types/amqplib

빠른 시작

  1. 계약 정의 스키마와 함께.
  2. 클라이언트 생성 메시지를 발행하기 위해.
  3. 워커 생성 메시지를 소비하기 위해.
  4. 타입 안전성 엔드‑투‑엔드로 즐기세요!

오늘 바로 사용해 보세요!

amqp‑contract는 오픈 소스(MIT 라이선스)이며 npm에서 사용할 수 있습니다:

자세한 가이드, API 레퍼런스 및 예제는 전체 문서 를 확인하세요.

결론

타입 안전성은 애플리케이션 경계에서 멈추지 않아야 합니다. amqp‑contract를 사용하면 TypeScript에서 누리는 동일한 수준의 타입 안전성과 개발자 경험을 AMQP 메시징 레이어에도 적용할 수 있습니다.

  • 런타임 오류와의 싸움을 멈추세요.
  • 메시지를 수동으로 검증하는 일을 멈추세요.
  • 리팩토링에 대한 걱정을 멈추세요.

오늘부터 타입‑안전하고 검증된, 유지보수가 쉬운 메시징 시스템을 구축하세요.

어떻게 생각하시나요? amqp‑contract를 사용해 보셨나요? 댓글에 타입‑안전 메시징 경험을 공유해 주세요!

Back to Blog

관련 글

더 보기 »

celery-plus 🥬 — Node.js용 현대적인 Celery

왜 확인해 보세요? - 🚀 기존 Python Celery 워커와 함께 작동합니다 - 📘 TypeScript로 작성되었으며 전체 타입을 제공합니다 - 🔄 RabbitMQ AMQP와 Redis를 지원합니다 - ⚡ Async/a...

프론트엔드의 혼돈에서 질서로

작동 방식 - 백엔드가 마이크로서비스용 GraphQL 스키마를 업데이트합니다. - 프론트엔드가 최신 스키마를 가져와 쿼리/뮤테이션을 생성하고 타입을 재생성합니다. - Any...