마이크로서비스 개념과 NestJS 적용
모놀리식과 마이크로서비스의 경계를 비교하고 TCP 기반 사용자·주문 서비스를 분리해 요청 통신을 검증합니다.
6장에서는 오래 걸리는 작업을 BullMQ와 Redis로 분리하고 실행 상태와 복구 기준을 관리했습니다.
7장에서는 이 비동기 경계를 서비스 사이로 확장하여 마이크로서비스(Microservices) 아키텍처의 기본 개념과 NestJS 적용 방법을 다룹니다.
단일 거대 애플리케이션(Monolithic Application)은 개발 초기에는 빠르고 간단하지만, 규모가 커지고 복잡해질수록 유지보수, 확장, 배포에 어려움을 겪게 됩니다.
마이크로서비스는 이러한 문제를 해결하기 위해 애플리케이션을 작고 독립적인 서비스들로 분해하는 아키텍처 접근 방식입니다.
각 서비스는 특정 비즈니스 기능에 집중합니다. 다만 독립 개발·배포·확장은 서비스별 소유권, 실행 환경, 배포 파이프라인, 데이터 경계를 실제로 분리했을 때 얻을 수 있는 운영 특성이지, 코드를 나누는 것만으로 자동 보장되지는 않습니다.
마이크로서비스 아키텍처란?
마이크로서비스 아키텍처는 단일 애플리케이션을 작고 독립적인 서비스의 모음으로 개발하는 접근 방식입니다.
각 서비스는 자체 프로세스에서 실행되며, 잘 정의된 경량 통신 메커니즘(예: HTTP REST API, gRPC, 메시지 큐)을 사용하여 통신합니다.
마이크로서비스의 주요 특징- 독립적인 배포: 서비스별 릴리스 파이프라인과 호환 가능한 계약을 갖추면 다른 서비스와 분리해 배포할 수 있습니다.
- 독립적인 개발: 비즈니스 소유권과 변경 책임이 분명하면 별도 팀이 독립적으로 개발할 수 있습니다.
- 독립적인 확장: 실행 환경과 용량 설정을 분리하면 부하가 높은 서비스만 확장할 수 있습니다.
- 기술 스택 다양성: 각 서비스는 특정 요구사항에 가장 적합한 기술 스택(언어, 프레임워크, 데이터베이스)을 선택할 수 있습니다 (Polyglot Persistence/Programming).
- 높은 응집도, 낮은 결합도: 각 서비스는 특정 비즈니스 도메인에 대한 높은 응집도를 가지며, 다른 서비스와의 결합도는 최소화됩니다.
- 장애 격리: 타임아웃, 격리된 자원, 재시도·서킷 브레이커 같은 실패 정책을 함께 설계하면 한 서비스의 장애 전파를 제한할 수 있습니다.
- 확장성: 특정 서비스만 유연하게 확장 가능
- 유지보수성: 서비스 단위가 작아 이해하고 수정하기 용이
- 배포 용이성: 독립적인 배포로 인한 빠른 릴리스 주기
- 기술 유연성: 서비스별 최적의 기술 스택 선택 가능
- 팀 자율성: 소규모 팀이 독립적으로 작업 가능
- 복잡성 증가: 분산 시스템 관리의 복잡성 (네트워크 지연, 장애 처리, 데이터 일관성)
- 운영 오버헤드: 더 많은 서비스, 배포 파이프라인, 모니터링 시스템 필요
- 데이터 일관성: 분산 트랜잭션 관리의 어려움
- 서비스 간 통신: 통신 메커니즘 선택 및 관리
- 디버깅 어려움: 여러 서비스에 걸친 문제 추적의 복잡성
마이크로서비스는 무조건 쪼개는 기술이 아니라, 독립 배포와 운영 비용 사이의 균형을 판단하는 설계입니다.
아래 다이어그램은 서비스를 분리할 때 확인해야 할 기준을 한 화면에 정리합니다.
Nest · Service Boundary Decision
“따로 운영할 이유”가 분산 비용보다 클 때 분리한다
코드를 서비스로 나눴다는 사실만으로 독립 배포나 장애 격리가 생기지는 않습니다. 소유권·런타임·데이터·배포 경계를 실제로 분리하고 그 대가를 운영할 수 있어야 합니다.
| 판단 축 | 얻을 수 있는 이득 | 성립 조건 | 반드시 감당할 비용 |
|---|---|---|---|
| 소유권 | 도메인 규칙과 변경 책임이 선명해진다 | 한 팀이 계약과 데이터를 끝까지 소유한다 | 팀 사이 계약 조율과 중복 운영 |
| 독립 배포 | 서비스별 속도로 릴리스하고 롤백한다 | 별도 파이프라인과 하위 호환 계약을 갖춘다 | 버전 호환, 배포 순서, 더 많은 파이프라인 |
| 부하 격리 | 트래픽이 몰린 기능만 별도로 확장한다 | 런타임·용량·자원 풀을 실제로 분리한다 | 네트워크 지연·부분 실패와 용량 조정 |
| 장애 격리 | 한 서비스의 실패 전파 범위를 줄인다 | 타임아웃·격리·재시도·저하 모드를 설계한다 | 분산 로그·메트릭·트레이스로 원인을 추적 |
| 데이터 경계 | 상태의 주인과 변경 규칙이 분명해진다 | 서비스별 쓰기 권한과 계약을 제한한다 | 단일 트랜잭션 대신 일관성·보상 정책 필요 |
팀과 데이터의 주인이 분명한가
자율성이 생기는 대신 서비스 사이 계약 조율과 중복 운영을 감당합니다.
릴리스 주기를 정말 분리하는가
별도 파이프라인과 호환 계약이 있어야 독립 배포가 실제 이득이 됩니다.
런타임과 용량도 따로 두는가
선택 확장 대신 네트워크 지연·부분 실패와 서비스별 용량 조정이 생깁니다.
실패 전파를 끊는 정책이 있는가
자원 격리와 타임아웃·저하 모드가 있어야 하며 분산 추적 비용이 따라옵니다.
서비스별 데이터 경계를 지키는가
상태 주인은 선명해지지만 분산 변경에는 일관성·보상 정책이 필요합니다.
한 프로세스·한 릴리스 주기
Orders, Users, Payments가 같은 배포와 장애 경계를 공유합니다. 초기에는 모듈 경계만 선명하게 해도 충분할 수 있습니다.
여러 운영 경계·네트워크 계약
각 서비스가 별도 프로세스와 데이터 책임을 갖고 pattern·payload 같은 원격 계약으로 연결될 때 독립 운영의 이득이 생깁니다.
Users
계정·인증·프로필 소유권이 분명하면 후보입니다. Orders와 늘 같은 트랜잭션만 쓴다면 보류합니다.
Orders
주문·결제 흐름의 변경 주기가 독립적이면 후보입니다. 초기 규모라면 한 HTTP 모듈로 충분할 수 있습니다.
Notifications
실패가 핵심 주문을 막지 않아야 하면 후보입니다. 전송량이 작고 격리가 불필요하면 보류합니다.
판정: 독립 운영의 구체적 이유가 있고 네트워크·관측성·일관성 비용을 감당할 때 분리합니다. 그렇지 않으면 모놀리스 안의 모듈 경계를 먼저 강화합니다.
NestJS가 마이크로서비스에 적합한 이유
NestJS는 마이크로서비스 아키텍처를 구축할 때 전송 계층, 메시지 패턴, DI 구조를 함께 제공하는 프레임워크입니다.
-
모듈 기반 아키텍처: NestJS의 모듈 시스템은 애플리케이션을 기능별로 분리하고 구성하기 용이하게 하여, 자연스럽게 마이크로서비스의 경계를 정의하는 데 도움을 줍니다.
-
엔터프라이즈급 패턴 지원: NestJS는 컨트롤러, 프로바이더, 모듈 등 잘 알려진 엔터프라이즈 디자인 패턴을 적용하여, 복잡한 시스템의 구조를 명확하게 유지할 수 있습니다.
-
다양한 통신 방식 지원: NestJS는 마이크로서비스 간 통신을 위한 다양한 전송 계층(Transport Layer)을 내장하고 있습니다.
- TCP: 기본 제공, 가벼운 통신
- Redis: 메시지 브로커로 사용, Pub/Sub 지원
- Kafka: 고처리량 분산 메시징 시스템
- RabbitMQ: 메시지 큐 기능
- NATS: 고성능 메시징 시스템
- gRPC: 고성능, 언어 중립적인 RPC 프레임워크 (HTTP/2 기반)
- MQTT: IoT 환경에 최적화된 경량 메시징 프로토콜
-
DI(Dependency Injection): 의존성 주입 시스템은 서비스 간의 결합도를 낮추고 테스트 용이성을 높입니다.
-
TypeScript 기반: 타입 안전성을 제공하여 대규모 프로젝트에서 코드 품질과 유지보수성을 향상시킵니다.
-
통일된 개발 경험: 각 마이크로서비스가 NestJS로 구현될 경우, 개발자들은 일관된 아키텍처 패턴과 코딩 스타일을 유지할 수 있어 학습 곡선을 줄이고 생산성을 높일 수 있습니다.
NestJS 마이크로서비스 기본 구조와 통신 방식
마이크로서비스 개념과 NestJS 적용에서는 요청이 들어오는 위치, 책임을 맡는 계층, 실패 응답으로 나가는 지점을 분리합니다.
Nest · TCP Request Flow
HTTP 계약을 내부 메시지 계약으로 바꿔 왕복한다
이 예제의 Orders는 HTTP 진입점일 뿐 자동으로 API Gateway가 되지는 않습니다. Controller가 원격 호출을 명시적으로 시작하고 실패를 HTTP 의미로 다시 매핑합니다.
GET 요청에서 응답까지
-
HTTP client
GET /orders/1/user로 외부 HTTP 계약을 호출합니다. -
OrdersController
orderId를userId로 해석하고 결과·오류를 HTTP 응답으로 바꿀 준비를 합니다. -
ClientProxy.send()
ClientProxy.send()가 pattern과 payload로 cold Observable을 만들고, 구독 또는firstValueFrom()때 요청을 보냅니다. -
Nest TCP transport
pattern과 payload를 직렬화해 Users 프로세스로 보내고 요청과 응답의 상관관계를 관리합니다.
-
Users @MessagePattern()
@MessagePattern('get_user_by_id')가 같은 pattern을 받아 payload로 조회한 뒤 User | null을 반환합니다. -
응답과 HTTP 변환
응답 Observable이 값을 내보내면 Orders가 성공,
null, timeout, transport 오류를 각각 HTTP 결과로 매핑합니다.
| 계약 요소 | Orders가 보내는 값 | Users가 받거나 반환하는 값 | 불일치하면 |
|---|---|---|---|
| pattern | 'get_user_by_id' |
@MessagePattern('get_user_by_id') |
대상 handler가 실행되지 않는다 |
| payload | userId: number |
@Payload() id: number |
검증 실패 또는 잘못된 조회가 난다 |
| response | Observable<User | null> |
User | null |
HTTP 상태·본문 변환이 어긋난다 |
라우팅 키를 똑같이 맞춘다
send()와 @MessagePattern()의 문자열 또는 객체 구조가 같아야 합니다.
입력 구조를 검증한다
여기서는 userId: number를 보내고 @Payload()로 받습니다.
없음을 타입에 포함한다
양쪽 계약을 User | null로 맞추고 Orders가 HTTP 결과를 결정합니다.
질문하고 응답을 기다린다
request–response용 cold Observable입니다. 구독해야 전송되며 timeout()과 transport·원격 오류 처리를 호출자가 명시해야 합니다.
이벤트를 발행하고 응답 본문은 받지 않는다
@EventPattern()용 hot Observable로, 구독하지 않아도 전달을 즉시 시도합니다. 그렇다고 전달 오류 관측 정책까지 사라지는 것은 아닙니다.
① HTTP 표면
상태 코드, 응답 본문, 전체 지연을 확인합니다.
② Orders + transport
pattern, 마스킹한 payload, timeout·오류와 TCP status를 기록합니다.
③ Users handler
handler 도착, 조회 시간, User 또는 null 결과를 확인합니다.
전송의 공통 틀
transport 연결, packet 직렬화, pattern 라우팅, 요청·응답 상관관계와 ClientProxy 추상화를 제공합니다.
업무 계약과 실패 정책
pattern·DTO, 입력 검증, timeout·재시도·멱등성, HTTP 오류 변환, 추적 컨텍스트와 데이터 일관성을 정합니다.
장애를 좁히는 순서: HTTP 응답 → Orders의 send와 구독 → TCP 상태 → Users handler → 도메인 조회. 세 구간에 같은 correlation ID를 남기면 분산 추적이 쉬워집니다.
NestJS에서 마이크로서비스를 구성하는 가장 기본적인 방법은 클라이언트(Consumer) 서비스와 서버(Provider) 서비스로 나누는 것입니다.
여기서는 가장 간단한 TCP 기반의 통신 예시를 통해 구조를 이해합니다.
시나리오: Users 서비스와 Orders 서비스가 있습니다.
Orders 서비스가 주문을 처리하는 과정에서 Users 서비스에 사용자 정보를 요청해야 하는 경우.
TCP 사용자 서비스 구축
사용자 정보를 제공하는 독립적인 마이크로서비스를 생성합니다.
단계 1: 새 NestJS 프로젝트 생성 (또는 기존 프로젝트 내에 모듈로 분리)마이크로서비스는 독립적인 프로젝트로 관리하는 것이 일반적입니다.
# 새로운 사용자 서비스 프로젝트 생성
nest new users-service --skip-install
cd users-service
npm install @nestjs/microservices
npm installmain.ts 파일 수정 (마이크로서비스 서버 설정)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MicroserviceOptions, Transport } from '@nestjs/microservices'; // MicroserviceOptions, Transport 임포트
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.TCP, // TCP 전송 방식 사용
options: {
host: 'localhost', // 서비스가 바인딩될 호스트
port: 3001, // 서비스가 리스닝할 포트
},
});
await app.listen();
console.log('Users Microservice is listening on port 3001');
}
bootstrap();NestFactory.createMicroservice(): 일반적인 HTTP 서버 대신 마이크로서비스 애플리케이션을 생성합니다.transport: Transport.TCP: TCP 프로토콜을 사용하여 메시지를 주고받도록 설정합니다. NestJS는 다양한Transport옵션을 제공합니다.options: 특정 전송 방식에 대한 추가 옵션을 설정합니다 (호스트, 포트 등).
마이크로서비스에서는 @MessagePattern() 데코레이터를 사용하여 메시지를 처리하는 핸들러를 정의합니다.
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices'; // MessagePattern, Payload 임포트
import { UsersService } from './users.service';
interface User {
id: number;
name: string;
email: string;
}
@Controller() // 마이크로서비스 컨트롤러는 일반적으로 경로가 없습니다.
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// 'get_user_by_id' 메시지 패턴에 대한 핸들러
@MessagePattern('get_user_by_id')
getUserById(@Payload() id: number): User | null {
console.log(`Users Service: Received request for user ID: ${id}`);
// 실제로는 DB에서 조회
return this.usersService.findOne(id); // 사용자 없으면 null 반환
}
// 'create_user' 메시지 패턴에 대한 핸들러
@MessagePattern('create_user')
createUser(@Payload() userDto: { name: string; email: string }): User {
console.log(`Users Service: Received request to create user: ${JSON.stringify(userDto)}`);
const newUser = this.usersService.create(userDto);
return newUser;
}
}@MessagePattern('pattern_name'): 이 핸들러가 처리할 메시지의 패턴(이름)을 정의합니다. 클라이언트는 이 패턴을 사용하여 특정 핸들러를 호출합니다.@Payload(): 메시지에서 전송된 데이터를 추출하여 메서드 인자로 주입합니다.
import { Injectable } from '@nestjs/common';
interface User {
id: number;
name: string;
email: string;
}
@Injectable()
export class UsersService {
private users: User[] = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
];
private nextId = 3;
findOne(id: number): User | null {
return this.users.find(user => user.id === id) ?? null;
}
create(user: { name: string; email: string }): User {
const newUser = { id: this.nextId++, ...user };
this.users.push(newUser);
return newUser;
}
}UsersModule 구성
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}AppModule에 UsersModule 임포트
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
controllers: [],
providers: [],
})
export class AppModule {}주문 서비스(TCP 클라이언트) 구축
사용자 서비스의 기능을 호출하는 클라이언트(또는 다른 마이크로서비스)를 구축합니다.
단계 1: 새 NestJS 프로젝트 생성 (또는 기존 프로젝트 내에 모듈로 분리)# 새로운 주문 서비스 프로젝트 생성
nest new orders-service --skip-install
cd orders-service
npm install @nestjs/microservices
npm installmain.ts 파일 수정 (일반적인 HTTP 서버 유지)
주문 서비스는 클라이언트로부터 HTTP 요청을 받고, 내부적으로 사용자 서비스와 통신할 수 있습니다. 이 예제의 Orders 서비스는 HTTP 진입점이지만, 라우팅·인증·집계 같은 게이트웨이 책임을 별도로 설계하지 않는 한 자동으로 API Gateway가 되는 것은 아닙니다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000); // HTTP 요청을 받을 포트
console.log('Orders Service is listening on port 3000 (HTTP)');
}
bootstrap();ClientProxy를 사용하여 다른 마이크로서비스에 메시지를 보냅니다.
NestJS는 이를 위한 @Client() 데코레이터를 제공합니다.
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices'; // ClientsModule, Transport 임포트
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
@Module({
imports: [
ClientsModule.register([
{
name: 'USERS_SERVICE', // 클라이언트 프록시의 토큰 (주입 시 사용)
transport: Transport.TCP, // Users Service와 동일한 전송 방식
options: {
host: 'localhost',
port: 3001, // Users Service가 리스닝하는 포트
},
},
]),
],
controllers: [OrdersController],
providers: [OrdersService],
})
export class OrdersModule {}ClientsModule.register(): 하나 이상의 마이크로서비스 클라이언트를 등록합니다.name: 이 클라이언트 프록시를 의존성 주입을 통해 참조할 때 사용할 토큰입니다.transport&options: 연결할 마이크로서비스 서버의 전송 방식과 옵션을 지정합니다.
클라이언트로부터 HTTP 요청을 받고, USERS_SERVICE 프록시를 통해 사용자 서비스로 메시지를 보냅니다.
import {
Body,
Controller,
Get,
Inject,
NotFoundException,
Param,
Post,
RequestTimeoutException,
ServiceUnavailableException,
} from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices'; // ClientProxy 임포트
import { OrdersService } from './orders.service';
import { firstValueFrom, timeout, TimeoutError } from 'rxjs';
interface User {
id: number;
name: string;
email: string;
}
@Controller('orders') // HTTP 엔드포인트
export class OrdersController {
constructor(
private readonly ordersService: OrdersService,
@Inject('USERS_SERVICE') private readonly usersClient: ClientProxy, // 'USERS_SERVICE' 클라이언트 주입
) {}
@Get(':orderId/user')
async getOrderUser(@Param('orderId') orderId: string): Promise<User> {
// 임시로 orderId를 사용자 ID로 사용한다고 가정
const userId = parseInt(orderId, 10);
const user = await this.requestUser(userId);
if (!user) {
throw new NotFoundException(`User for order ${orderId} not found.`);
}
return user;
}
@Post('/create-order-with-user')
async createOrderWithUser(@Body() orderDto: { userId: number; item: string }): Promise<any> {
const { userId, item } = orderDto;
const user = await this.requestUser(userId);
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found.`);
}
const order = this.ordersService.createOrder(userId, item);
return { order, user };
}
private async requestUser(userId: number): Promise<User | null> {
try {
// send()는 cold Observable이므로 firstValueFrom()의 구독 때 전송됩니다.
return await firstValueFrom(
this.usersClient
.send<User | null, number>('get_user_by_id', userId)
.pipe(timeout(3000)),
);
} catch (error) {
if (error instanceof TimeoutError) {
throw new RequestTimeoutException('Users Service response timed out.');
}
throw new ServiceUnavailableException('Users Service request failed.');
}
}
}@Inject('USERS_SERVICE'):OrdersModule에서 정의한USERS_SERVICE클라이언트 프록시를 주입받습니다.this.usersClient.send('pattern', data): 요청-응답용 coldObservable을 반환합니다. 구독하거나firstValueFrom()으로 소비해야 실제 전송이 시작되며, 분산 호출이 무한히 기다리지 않도록timeout()과 오류 매핑을 애플리케이션에서 정합니다.this.usersClient.emit('pattern', data):@EventPattern()으로 받을 이벤트를 발행합니다. 반환값은 hotObservable이라 명시적으로 구독하지 않아도 전달을 즉시 시도하지만, 요청-응답 결과는 없습니다. 전달 실패를 관측·처리해야 한다면 반환된 스트림의 오류도 운영 정책에 맞게 다룹니다.
import { Injectable } from '@nestjs/common';
interface Order {
id: number;
userId: number;
item: string;
createdAt: Date;
}
@Injectable()
export class OrdersService {
private orders: Order[] = [];
private nextId = 1;
createOrder(userId: number, item: string): Order {
const newOrder = {
id: this.nextId++,
userId,
item,
createdAt: new Date(),
};
this.orders.push(newOrder);
return newOrder;
}
}AppModule에 OrdersModule 임포트
import { Module } from '@nestjs/common';
import { OrdersModule } from './orders/orders.module';
@Module({
imports: [OrdersModule],
controllers: [],
providers: [],
})
export class AppModule {}서비스 기동 및 호출 검증
cd users-servicenpm run start:dev- 콘솔에
Users Microservice is listening on port 3001메시지 확인
cd orders-servicenpm run start:dev- 콘솔에
Orders Service is listening on port 3000 (HTTP)메시지 확인
-
사용자 정보 가져오기
GET http://localhost:3000/orders/1/user- 응답:
{"id":1,"name":"Alice","email":"alice@example.com"}(Users Service 콘솔에 요청 메시지 확인)
-
존재하지 않는 사용자 정보 가져오기
GET http://localhost:3000/orders/999/user- 응답: HTTP
404 Not Found와 NestJS 오류 본문
-
주문 생성과 사용자 정보 확인
POST http://localhost:3000/orders/create-order-with-user- Headers:
Content-Type: application/json - Body:
{"userId": 2, "item": "Laptop"} - 응답:
{"order":{"id":1,"userId":2,"item":"Laptop","createdAt":"2023-06-23T...Z"},"user":{"id":2,"name":"Bob","email":"bob@example.com"}}
TCP 예제의 검증은 두 프로세스가 모두 떠 있고 send() 요청이 Users Service 로그까지 도달하는지 확인하는 과정입니다.
호출을 검증할 때는 HTTP 응답만 보지 말고 Orders 로그의 pattern·payload, TCP 연결 상태, Users 로그의 handler 도착 여부를 함께 봅니다. 응답 지연은 포트·서비스 기동·타임아웃을, handler 미실행은 send()와 @MessagePattern()의 pattern 불일치를, null 응답은 payload와 조회 결과를 우선 확인합니다.
이 예시는 NestJS가 마이크로서비스 아키텍처를 얼마나 쉽게 구축할 수 있는지 보여줍니다.
TCP 외에도 Redis, Kafka, gRPC 등 다양한 전송 계층을 사용하여 특정 요구사항에 맞는 통신 방식을 선택할 수 있습니다.
마이크로서비스는 복잡도를 증가시키므로 서비스 경계, 통신 방식, 배포 단위, 관측성 기준을 명확히 관리해야 합니다.
NestJS의 모듈 구조와 전송 계층 추상화는 이 기준을 코드 구조로 옮기는 데 사용됩니다. 프레임워크는 transport 연결, pattern 라우팅, 직렬화와 응답 상관관계를 제공하지만, pattern·DTO 계약, 입력 검증, 타임아웃·재시도·멱등성, HTTP 오류 변환, 추적 컨텍스트와 데이터 일관성 정책은 애플리케이션이 정해야 합니다.