API 게이트웨이 패턴 구현
API 게이트웨이를 단일 진입점으로 두고 요청 라우팅·응답 합성·공통 인증을 NestJS 서비스에 적용합니다.
지난 절에서는 NestJS와 Kafka를 활용하여 마이크로서비스 간 비동기 통신 및 이벤트 기반 아키텍처를 구현하는 방법을 살펴보았습니다.
이제 7장의 마지막으로, 마이크로서비스 아키텍처에서 중요한 역할을 하는 API 게이트웨이(API Gateway) 패턴에 대해 알아보고, NestJS를 사용하여 이를 구현하는 방법에 대해 자세히 설명하겠습니다.
마이크로서비스 아키텍처는 백엔드를 여러 개의 작은 서비스로 분해하여 독립적인 개발, 배포, 확장을 가능하게 합니다.
그러나 클라이언트(웹 애플리케이션, 모바일 앱 등)의 관점에서는 수많은 마이크로서비스와 직접 통신하는 것이 복잡하고 비효율적일 수 있습니다.
이럴 때 필요한 것이 바로 API 게이트웨이입니다.
Nest · API Gateway Boundary
클라이언트는 하나의 HTTP 계약만 봅니다. Gateway는 외부 요청의 공통 정책과 내부 route를 책임지고, 각 서비스는 자기 데이터와 업무 규칙을 책임집니다.
HTTP 진입부터 내부 호출까지 경계를 따라간다
-
클라이언트는 외부 API만 호출한다
웹·모바일은
http://localhost:3000/api아래의 route를 사용하며 Users gRPC 주소나 Orders 저장 방식을 알지 않습니다. -
Gateway가 edge 공통 정책을 적용한다
신원 확인, route·scope 권한, CORS, 요청 제한, 외부 DTO와 응답 모양, request ID처럼 여러 route에 공통인 edge 정책을 진입 경계에서 일관되게 적용합니다.
-
route가 도메인 처리 대상을 고른다
/api/users는UserServicegRPC로 위임하고,/api/orders는 이 학습용 앱의 Orders 로직을 호출합니다. details route는 두 결과를 외부 응답으로 조합합니다. -
내부 결과를 외부 계약으로 번역한다
Gateway는 deadline과 gRPC status를 관측해 route별 HTTP 상태와 부분 응답 모양을 선택합니다. 내부 protocol 오류를 그대로 노출하지 않습니다.
외부 계약과 공통 edge 정책
Route 선택, 신원 확인과 공통 scope 권한, rate limit, protocol 변환, 응답 조합, 실패 매핑과 요청 관측을 맡습니다. 리소스 소유자처럼 데이터가 필요한 도메인 권한은 각 서비스가 판단합니다.
도메인 규칙과 데이터 소유권
Users는 사용자 조회·생성 규칙과 데이터를, Orders는 주문 생성·조회 규칙과 데이터를 소유합니다. Gateway가 응답을 조합해도 이 업무 판단을 가져오지 않습니다.
orders-service와 Gateway의 동거는 학습용이다
예제는 한 프로세스에서 HTTP route, Orders 로컬 처리, Users gRPC 호출과 composition을 모두 실행해 흐름을 작게 보여 줍니다. 운영 배치에서 둘을 같은 서비스로 묶으라는 권장이 아니며, 변경 주기·확장·권한·팀 소유권이 다르면 Gateway를 별도 애플리케이션으로 분리합니다.
각 route의 실제 처리 경로
| 외부 route | Gateway 동작 | 도메인 소유자 |
|---|---|---|
GET /api/users/:userId |
getUserById unary gRPC 호출 |
Users Service |
POST /api/users |
createUser unary gRPC 호출 |
Users Service |
POST /api/orders |
이 학습용 앱의 Orders provider 호출 | Orders 도메인 |
GET …/:orderId/details |
Order 조회 후 Users gRPC 결과를 조합 | 각 서비스가 원본을 소유하고 Gateway가 외부 모양만 구성 |
/api/users/:userId
getUserById unary gRPC 호출 · Users Service 소유
/api/users
createUser unary gRPC 호출 · Users Service 소유
/api/orders
이 학습용 앱의 Orders provider 호출 · Orders 도메인 소유
/api/orders/:orderId/details
Order 조회 후 Users 결과를 조합합니다. 원본 데이터와 업무 규칙은 각 서비스에 남습니다.
판정 기준: Gateway는 외부 계약과 공통 edge 권한을 안정화하지만, 도메인 서비스의 리소스 권한·업무 규칙·데이터 경계를 대신하지 않습니다.
게이트웨이 패턴이란?
API 게이트웨이는 클라이언트의 모든 API 요청을 단일 진입점으로 받아들이고, 이 요청을 적절한 마이크로서비스로 라우팅하는 서비스입니다.
즉, 클라이언트와 백엔드 마이크로서비스 간의 중개자 역할을 수행합니다.
API 게이트웨이의 주요 역할- 요청 라우팅(Request Routing): 클라이언트의 요청 URL이나 헤더 등을 분석하여 해당 요청을 처리할 적절한 마이크로서비스로 전달합니다.
- 요청 합성(Request Composition): 여러 마이크로서비스의 응답을 받아 클라이언트에 필요한 형태로 조합하여 단일 응답으로 제공합니다. (Backend For Frontend, BFF 패턴과도 관련)
- 인증 및 권한 부여(Authentication & Authorization): 신원 확인과 route·scope 같은 edge 권한을 일관되게 적용합니다. 주문 소유자 확인처럼 도메인 데이터가 필요한 세부 권한 판단은 해당 서비스에 남깁니다.
- 속도 제한(Rate Limiting): 특정 클라이언트나 사용자로부터의 요청 속도를 제한하여 서비스 과부하를 방지합니다.
- 캐싱(Caching): 자주 요청되는 데이터를 캐싱하여 백엔드 서비스의 부하를 줄이고 응답 속도를 높입니다.
- 로깅 및 모니터링(Logging & Monitoring): 모든 API 요청에 대한 중앙 집중식 로깅 및 모니터링 기능을 제공합니다.
- 프로토콜 변환(Protocol Translation): 클라이언트는 HTTP/REST로 요청하고, 게이트웨이가 이를 gRPC나 Kafka 이벤트 등으로 변환하여 백엔드 서비스와 통신할 수 있습니다.
- 장애 처리(Fault Tolerance): 백엔드 서비스의 장애를 감지하고, 서킷 브레이커(Circuit Breaker)나 재시도(Retry)와 같은 패턴을 적용하여 클라이언트에 안정적인 응답을 제공합니다.
- 클라이언트 복잡도 감소: 클라이언트가 여러 서비스 엔드포인트를 알 필요 없이 단일 엔트리포인트만 호출하면 됩니다.
- 보안 강화: 신원 확인과 공통 edge 접근 정책을 중앙에서 처리하되, 서비스가 자기 리소스의 도메인 권한을 다시 검증하도록 경계를 나눕니다.
- 유연한 마이크로서비스 변경: 백엔드 서비스 변경이 클라이언트에 미치는 영향을 최소화합니다.
- 성능 최적화: 캐싱, 압축 등으로 응답 속도를 개선할 수 있습니다.
- 단일 실패 지점(Single Point of Failure): 게이트웨이가 다운되면 전체 시스템이 마비될 수 있습니다 (고가용성 확보 필요).
- 성능 병목(Performance Bottleneck): 모든 요청이 게이트웨이를 통과하므로, 게이트웨이가 성능 병목이 될 수 있습니다 (확장성 고려).
- 복잡성 증가: 게이트웨이 자체의 개발, 배포, 유지보수 복잡성이 추가됩니다.
게이트웨이에 모을 공통 정책과 각 마이크로서비스에 남겨야 할 도메인 책임을 분리하면, 인증·속도 제한·응답 합성의 경계가 더 선명해집니다. 위 다이어그램은 단일 HTTP 진입점, Gateway의 edge 정책, HTTP→gRPC 라우팅과 도메인 서비스의 데이터·업무 규칙 소유권을 한 경계로 정리합니다.
NestJS에서 API 게이트웨이 구현하기
API 게이트웨이는 인증, 라우팅, 프로토콜 변환, 장애 응답 경계를 나눠 읽습니다.
NestJS에서는 HTTP 라우팅과 마이크로서비스 클라이언트를 한 애플리케이션 안에서 함께 구성할 수 있습니다.
여기서는 orders-service가 API 게이트웨이 역할도 겸하도록 확장하여 이전의 users-service와 통신하는 예시를 봅니다. 이는 한 장 안에서 HTTP 라우팅, 로컬 주문 처리, gRPC 호출과 응답 합성을 실행해 보는 학습용 배치입니다. 실제 시스템에서 Gateway와 Orders 도메인의 배포·확장·권한 경계를 합치라는 권장 구조가 아니며, 운영에서는 변경 주기와 소유권에 따라 별도 애플리케이션으로 분리할 수 있습니다.
시나리오: 웹 클라이언트가 orders-service (API 게이트웨이)에 HTTP 요청을 보내면, orders-service는 내부적으로 users-service (gRPC)를 호출하여 데이터를 조합하고 클라이언트에 응답합니다.
외부 클라이언트는 HTTP 계약만 알고, Gateway는 route에 따라 Orders 로컬 로직 또는 Users gRPC 호출을 선택합니다. 신원 확인, route·scope 권한, 요청 제한은 edge 경계에 두되, 주문 소유자처럼 데이터가 필요한 세부 권한과 실제 업무 규칙은 해당 도메인 서비스에 남깁니다.
사전 준비- 7장 2절에서 구축한
users-service(gRPC 서버)가localhost:50051에서 실행 중이어야 합니다. - 새로운
orders-service(API 게이트웨이 겸) 프로젝트를 시작합니다.
Orders Service (API Gateway) 설정
단계 1: 새 NestJS 프로젝트 생성 (기존orders-service를 재사용하거나 새로 생성)
# orders-service 프로젝트가 없다면 새로 생성 (있다면 스킵)
nest new orders-service --skip-install
cd orders-service
npm install @nestjs/microservices @grpc/grpc-js @grpc/proto-loader rxjsmain.ts 파일 수정 (HTTP 서버 역할)
orders-service는 클라이언트로부터 HTTP 요청을 받아야 하므로, 일반적인 NestJS HTTP 서버로 구성됩니다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// CORS 활성화 (옵션, 클라이언트 웹 개발 시 필요)
app.enableCors();
await app.listen(3000); // 클라이언트가 접근할 API 게이트웨이 포트
console.log('API Gateway (Orders Service) is listening on port 3000');
}
bootstrap();이 최소 코드에서 실제로 켠 공통 정책은 CORS뿐입니다. 인증·권한, rate limit, request ID와 접근 로그는 Gateway라는 이름만으로 자동 제공되지 않으며 Guard, middleware, interceptor 또는 앞단 proxy에서 별도로 구성하고 테스트해야 합니다.
단계 3:.proto 파일 복사 또는 공유 설정
users-service와 동일한 users.proto 파일을 orders-service 프로젝트의 proto 폴더에 복사하거나, 별도의 공유 모듈/패키지를 통해 관리하는 것이 좋습니다.
여기서는 orders-service/proto/users.proto로 복사했다고 가정합니다.
// orders-service/proto/users.proto (users-service의 proto/users.proto와 동일)
syntax = "proto3";
package users;
service UserService {
rpc GetUserById (UserByIdRequest) returns (User);
rpc CreateUser (CreateUserRequest) returns (User);
}
message UserByIdRequest {
int32 id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
}OrdersModule에 gRPC 클라이언트 등록
orders-service가 users-service (gRPC)와 통신해야 하므로, gRPC 클라이언트를 등록합니다.
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { join } from 'path';
@Module({
imports: [
ClientsModule.register([
{
name: 'USERS_SERVICE', // gRPC 클라이언트 토큰
transport: Transport.GRPC,
options: {
package: 'users',
protoPath: join(__dirname, '../../proto/users.proto'), // .proto 파일 경로
url: 'localhost:50051', // Users gRPC 서비스 주소
loader: {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
},
},
},
]),
],
controllers: [OrdersController],
providers: [OrdersService],
})
export class OrdersModule {}이 컨트롤러는 클라이언트의 HTTP 요청을 받아, 내부적으로 gRPC USERS_SERVICE를 호출하고, 그 결과를 HTTP 응답으로 변환합니다.
여기서는 7장 2절의 orders.controller.ts와 유사하지만, API Gateway의 라우팅과 조합 기능을 명확히 보여줍니다.
import {
BadGatewayException,
BadRequestException,
Body,
Controller,
GatewayTimeoutException,
Get,
HttpException,
Inject,
Logger,
NotFoundException,
OnModuleInit,
Param,
ParseIntPipe,
Post,
ServiceUnavailableException,
} from '@nestjs/common';
import { status, type CallOptions } from '@grpc/grpc-js';
import { ClientGrpc } from '@nestjs/microservices';
import { OrdersService } from './orders.service';
import { lastValueFrom, Observable } from 'rxjs';
// 동적 proto client가 노출하는 lower camel-case 메서드 계약
interface UserService {
getUserById(data: { id: number }, options?: CallOptions): Observable<User>;
createUser(
data: { name: string; email: string },
options?: CallOptions,
): Observable<User>;
}
interface User {
id: number;
name: string;
email: string;
}
interface Order {
id: number;
userId: number;
item: string;
createdAt: Date;
}
type DependencyStatus = 'ok' | 'not-found' | 'timeout' | 'unavailable';
interface OrderDetails {
order: Order;
user: User | null;
dependencyStatus: DependencyStatus;
}
interface GrpcErrorShape {
code?: number;
}
function readGrpcError(error: unknown): GrpcErrorShape {
return typeof error === 'object' && error !== null
? (error as GrpcErrorShape)
: {};
}
// 이 예제 Gateway가 선택한 외부 HTTP 계약입니다. 보편적인 표준표가 아닙니다.
function mapGrpcError(error: unknown): HttpException {
switch (readGrpcError(error).code) {
case status.INVALID_ARGUMENT:
return new BadRequestException('Invalid Users Service request');
case status.NOT_FOUND:
return new NotFoundException('User not found');
case status.DEADLINE_EXCEEDED:
return new GatewayTimeoutException('Users Service timed out');
case status.UNAVAILABLE:
return new ServiceUnavailableException(
'Users Service is temporarily unavailable',
);
default:
return new BadGatewayException('Users Service request failed');
}
}
@Controller('api')
export class OrdersController implements OnModuleInit {
private userService!: UserService;
private readonly logger = new Logger(OrdersController.name);
constructor(
private readonly ordersService: OrdersService,
@Inject('USERS_SERVICE') private readonly client: ClientGrpc,
) {}
onModuleInit(): void {
this.userService = this.client.getService<UserService>('UserService');
}
private logUpstreamFailure(route: string, error: unknown): void {
const { code } = readGrpcError(error);
this.logger.warn(
`route=${route} upstream=users-service grpcCode=${code ?? 'UNKNOWN'}`,
);
}
@Get('users/:userId')
async getUser(
@Param('userId', ParseIntPipe) userId: number,
): Promise<User> {
try {
return await lastValueFrom(
this.userService.getUserById(
{ id: userId },
{ deadline: Date.now() + 1_500 },
),
);
} catch (error) {
this.logUpstreamFailure('GET /api/users/:userId', error);
throw new BadGatewayException('Users Service request failed');
}
}
@Post('users')
async createUser(@Body() userDto: { name: string; email: string }): Promise<User> {
try {
return await lastValueFrom(
this.userService.createUser(
userDto,
{ deadline: Date.now() + 1_500 },
),
);
} catch (error) {
this.logUpstreamFailure('POST /api/users', error);
throw mapGrpcError(error);
}
}
@Post('orders')
createOrder(@Body() orderDto: { userId: number; item: string }): Order {
return this.ordersService.createOrder(orderDto.userId, orderDto.item);
}
@Get('orders/:orderId/details')
async getOrderDetails(
@Param('orderId', ParseIntPipe) orderId: number,
): Promise<OrderDetails> {
const order = this.ordersService.findOne(orderId);
if (!order) {
throw new NotFoundException('Order not found');
}
try {
const user = await lastValueFrom(
this.userService.getUserById(
{ id: order.userId },
{ deadline: Date.now() + 1_500 },
),
);
return { order, user, dependencyStatus: 'ok' };
} catch (error) {
const { code } = readGrpcError(error);
this.logUpstreamFailure('GET /api/orders/:orderId/details', error);
if (code === status.NOT_FOUND) {
return { order, user: null, dependencyStatus: 'not-found' };
}
if (code === status.DEADLINE_EXCEEDED) {
return { order, user: null, dependencyStatus: 'timeout' };
}
if (code === status.UNAVAILABLE) {
return { order, user: null, dependencyStatus: 'unavailable' };
}
throw mapGrpcError(error);
}
}
}@Controller('api'): 모든 엔드포인트에/api접두사를 붙여 외부 HTTP 표면을 한곳에 둡니다.- 라우팅:
/api/users경로는users-service로 위임하고,/api/orders는 이 학습용 앱의 Orders 로직에서 처리합니다. - 클라이언트 계약: proto의
GetUserById,CreateUser는 Nest gRPC client proxy에서getUserById,createUser처럼 lower camel-case 메서드로 노출됩니다. Unary 응답은Observable이므로lastValueFrom()으로 완료된 한 응답을 기다립니다. - 시간 예산: gRPC에는 기본 deadline이 없으므로 예제는 각 RPC에 유한한 deadline을 전달합니다.
1_500ms는 학습용 값이며 실제 값은 상위 HTTP 예산, 네트워크와 부하 측정으로 정합니다. - 오류 경계:
ParseIntPipe의 400과 로컬 주문 404는 gRPCtry밖에서 결정됩니다. 따라서 이미 만든HttpException이 같은catch에 잡혀 500 또는 502로 다시 포장되지 않습니다. - 실패 매핑: 이 예제의 직접 Users route는
INVALID_ARGUMENT→400,NOT_FOUND→404,DEADLINE_EXCEEDED→504,UNAVAILABLE→503, 그 밖의 예상하지 못한 상류 실패를502로 정합니다. 이는 이 Gateway의 외부 계약 예시이지 gRPC→HTTP 보편 표준표가 아닙니다. 다른 API 계약은UNAVAILABLE을 502로 정할 수도 있으므로 문서·테스트와 일치시키는 것이 핵심입니다. - 부분 실패: 조합 route는 주문이 있으면 Users의 not found, timeout, unavailable을
user: null과 명시적dependencyStatus로 반환합니다. 저장된 주문에서 만든 상류 요청의INVALID_ARGUMENT을 현재 HTTP 요청의 400으로 돌리지 않고, 허용하지 않은 다른 상류 실패와 함께 502로 처리합니다. 모든 endpoint가 부분 성공을 써야 한다는 뜻은 아니며, 소비자가 이 응답 모양을 계약으로 이해할 때만 사용합니다. - 재시도:
createUser처럼 비멱등인 호출은 deadline 뒤 서버에서 이미 완료되었을 수도 있습니다. idempotency key와 중복 방지 계약 없이 애플리케이션이나 service config의 재시도를 추가하지 않습니다. gRPC transport는 애플리케이션이 call을 보기 전의 제한된 상황에서 transparent retry를 수행할 수 있으므로, 이것도 정확히 한 번 실행을 뜻하지 않습니다. - 관측 범위: 위 최소 코드는 실패 route와 gRPC code만 남깁니다. request/trace ID, duration, 최종 HTTP status, partial 여부를 연결하려면 middleware나 interceptor와 telemetry 계층을 별도로 구성하고 성공·실패 경로에서 테스트해야 합니다.
게이트웨이의 품질은 성공 응답보다 실패 응답에서 더 잘 드러납니다. 내부 오류 문자열은 외부 응답에 그대로 노출하지 않고, route별로 문서화한 상태 코드·응답 모양으로 바꿉니다.
단계 6:OrdersService 구현 (주문 데이터 관리)
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;
}
findOne(id: number): Order | undefined {
return this.orders.find(order => order.id === id);
}
}AppModule에 OrdersModule 임포트 (동일)
import { Module } from '@nestjs/common';
import { OrdersModule } from './orders/orders.module';
@Module({
imports: [OrdersModule],
controllers: [],
providers: [],
})
export class AppModule {}게이트웨이 동작 검증
성공 응답 하나만 확인하지 말고 route 선택, 실제 상류 호출, deadline, 외부 실패 매핑과 부분 응답을 함께 검증합니다. 아래 다이어그램은 요청 경로와 route별 실패 계약, 로그·metric·trace 관측 지점을 한 검증표로 정리합니다.
Nest · Route & Failure Contract
같은 gRPC 실패도 외부 route의 약속에 따라 전체 실패 또는 명시적 부분 응답이 됩니다. 내부 status와 HTTP status 사이에는 자동 보편 매핑이 없습니다.
요청 한 건의 routing·composition·failure 경계
-
HTTP 입력과 공통 정책을 먼저 확정한다
Route parameter는
ParseIntPipe같은 HTTP 경계에서 검증합니다. 인증·권한·rate limit과 request ID도 내부 호출 전에 적용해 잘못된 요청이 상류 서비스로 번지지 않게 합니다. -
route가 실제 처리 경로를 선택한다
Users route는 gRPC unary RPC로 위임하고 Orders 생성은 로컬 provider를 호출합니다. Details route는 먼저 Order를 찾고, 존재할 때만 Users 조회를 이어 갑니다.
-
상류 호출에 유한한 deadline을 건다
예제는 Users RPC에
Date.now() + 1_500을 전달합니다. 조합 route는 남은 HTTP 시간 예산 안에서 필요한 호출을 끝내야 하며, 운영 값은 tail latency와 부하 측정으로 정합니다. -
gRPC status를 route 계약으로 분류한다
직접 Users route는 실패를 HTTP 예외로 번역합니다. Details route는 계약이 허용한
NOT_FOUND, timeout, unavailable만 user: null과 상태 필드로 표현하고 예상 밖 오류는 502로 실패합니다. -
외부 응답과 관측 신호를 함께 남긴다
운영 telemetry에서는 최종 HTTP status, partial 여부, route template, upstream RPC, gRPC code와 duration을 같은 request/trace ID로 연결합니다. 최소 예제의 실패 로그만으로 이 완료 조건이 충족되지는 않습니다.
같은 상류 결과도 route 계약에 따라 다르게 보인다
| 내부 결과 | 직접 Users route | Order details 조합 route | 판정 |
|---|---|---|---|
OK |
GET 200 · POST 201 · User | 200 · dependencyStatus = "ok" |
요청한 전체 데이터가 준비됨 |
NOT_FOUND |
404 · User not found | 200 · user: null, not found | Order가 있을 때만 조합 route의 부분 응답 허용 |
| deadline 초과 | 504 · upstream timeout | 200 · user: null, timeout |
시간 예산 초과를 명시 |
UNAVAILABLE |
503 · 이 예제의 계약 | 200 · user: null, unavailable |
다른 외부 계약은 502 또는 503을 선택할 수 있음 |
| INVALID_ARGUMENT | 400 · 잘못된 직접 Users 입력 | 502 · 저장된 Order에서 만든 상류 요청 실패 | 같은 gRPC code도 어느 경계의 입력인지에 따라 외부 의미가 다름 |
| 그 밖의 예상하지 못한 상류 실패 | 502 · upstream failure | 502 · partial로 숨기지 않음 | 알 수 없는 실패를 성공처럼 반환하지 않음 |
직접 route · GET 200, POST 201
조합 route · 200, dependencyStatus = "ok"
직접 route · 404
Order가 있으면 조합 route는 200, user: null, not-found를 반환합니다.
직접 route · 504
조합 route · 200, user: null, timeout
직접 route · 이 예제는 503
조합 route · 200, user: null, unavailable. 다른 외부 계약은 502를 고를 수도 있습니다.
직접 입력은 400 · 조합 상류 실패는 502
같은 gRPC code도 어느 경계의 입력인지에 따라 외부 의미가 달라집니다.
직접·조합 route 모두 502
알 수 없는 실패를 성공이나 partial 응답처럼 반환하지 않습니다.
직접 위임을 검증한다
GET /api/users/:userId와 POST /api/users가 각각 올바른 gRPC 메서드를 호출하는지 확인합니다. 성공 200·201과 실패 status별 응답, deadline을 검증하고, 비멱등 생성 요청은 idempotency 계약 없이 명시적·정책 재시도를 추가하지 않습니다.
로컬 처리는 불필요한 상류 호출이 없어야 한다
POST /api/orders가 Orders provider만 호출하고 Users gRPC를 호출하지 않는지 확인합니다. 이 route의 도메인 검증과 데이터 소유권은 Gateway 공통 정책이 아니라 Orders 책임입니다.
조합 순서와 부분 실패를 함께 검증한다
Details route는 Order가 없으면 404로 끝나고 Users를 호출하지 않아야 합니다. Order가 있으면 Users 결과를 합치며, 허용한 실패만 안정된 dependencyStatus 값으로 반환합니다.
응답과 내부 경로를 한 trace로 맞춘다
운영 관측 계층을 구성한 뒤 gRPC 호출 유무, code, duration, 부분 응답 flag와 최종 status를 비교합니다. Route template을 metric label로 쓰고 높은 cardinality의 ID와 payload는 trace·보호된 로그에서 다룹니다.
운영 완료 조건: route가 의도한 내부 경로만 타고, deadline과 실패 분류가 문서화한 외부 응답으로 나타나며, 추가한 telemetry의 같은 request/trace ID로 원인을 추적할 수 있어야 합니다.
- 7장 2절에서 만든
users-service프로젝트로 이동합니다. cd users-servicenpm run start:dev- 콘솔에
Users gRPC Microservice is listening on localhost:50051메시지 확인.
cd orders-servicenpm run start:dev- 콘솔에
API Gateway (Orders Service) is listening on port 3000메시지 확인.
- 새로운 사용자 생성 (API Gateway -> Users Service gRPC)
POST http://localhost:3000/api/users- Headers:
Content-Type: application/json - Body:
{"name": "Evan", "email": "evan@example.com"} - 응답:
{"id":4,"name":"Evan","email":"evan@example.com"} - Users Service 콘솔:
Users gRPC Service: Received request to create user: {"name":"Evan","email":"evan@example.com"}확인. - 사용자 정보 조회 (API Gateway -> Users Service gRPC)
GET http://localhost:3000/api/users/1- 응답:
{"id":1,"name":"Alice","email":"alice@example.com"} - Users Service 콘솔:
Users gRPC Service: Received request for user ID: 1확인. - 새로운 주문 생성 (API Gateway 자체 처리)
POST http://localhost:3000/api/orders- Headers:
Content-Type: application/json - Body:
{"userId": 1, "item": "Monitor"} - 응답:
{"id":1,"userId":1,"item":"Monitor","createdAt":"2023-06-23T..."} - 주문 및 사용자 정보 조회 (API Gateway 조합)
GET http://localhost:3000/api/orders/1/details- 응답:
{ "order": { "id": 1, "userId": 1, "item": "Monitor", "createdAt": "2023-06-23T..." }, "user": { "id": 1, "name": "Alice", "email": "alice@example.com" }, "dependencyStatus": "ok" } - Users Service 콘솔:
Users gRPC Service: Received request for user ID: 1확인.
- Users Service를 중단하거나 test double로 gRPC status를 주입합니다.
GET /api/users/1은 성공 시 200,POST /api/users는 Nest 기본 성공 상태인 201인지 확인합니다. 실패는 이 예제 계약에서INVALID_ARGUMENT→400,UNAVAILABLE→503,DEADLINE_EXCEEDED→504,NOT_FOUND→404이며 내부 gRPC 오류 문자열을 응답에 그대로 노출하지 않습니다.GET /api/orders/1/details는 주문이 존재할 때user: null과dependencyStatus: "unavailable" | "timeout" | "not-found"를 반환하는지 확인합니다. 예상하지 못한 상류 실패는 부분 성공으로 숨기지 않고 502로 실패합니다.GET /api/users/not-a-number는 400, 존재하지 않는 주문의 details route는 404이며 두 경우 Users gRPC 호출이 발생하지 않아야 합니다.- 운영 관측 계층을 추가했다면 route, upstream RPC, gRPC status, 최종 HTTP status, duration, partial 여부를 같은 request/trace ID로 연결합니다. 원문 payload와 개인 정보는 관측 label이나 로그에 넣지 않습니다.
API 게이트웨이 패턴은 마이크로서비스 아키텍처의 복잡성을 줄이고, 클라이언트에 일관된 인터페이스를 제공하기 위한 구조입니다.
NestJS에서는 HTTP 서버 기능과 마이크로서비스 클라이언트 기능을 한 애플리케이션 안에서 함께 구성할 수 있습니다.
이것으로 7장 마이크로서비스 아키텍처를 마칩니다.
NestJS 마이크로서비스 구성, 통신 방식, API 게이트웨이 패턴을 한 흐름으로 정리했습니다. Gateway는 내부 복잡도를 숨기되 도메인 소유권을 가져오지 않고, 외부 실패 계약과 관측 가능성까지 명시할 때 안정적인 경계가 됩니다.