본문 바로가기

API 게이트웨이 패턴 구현

API 게이트웨이를 단일 진입점으로 두고 요청 라우팅·응답 합성·공통 인증을 NestJS 서비스에 적용합니다.

지난 절에서는 NestJS와 Kafka를 활용하여 마이크로서비스 간 비동기 통신 및 이벤트 기반 아키텍처를 구현하는 방법을 살펴보았습니다.

이제 7장의 마지막으로, 마이크로서비스 아키텍처에서 중요한 역할을 하는 API 게이트웨이(API Gateway) 패턴에 대해 알아보고, NestJS를 사용하여 이를 구현하는 방법에 대해 자세히 설명하겠습니다.

마이크로서비스 아키텍처는 백엔드를 여러 개의 작은 서비스로 분해하여 독립적인 개발, 배포, 확장을 가능하게 합니다.

그러나 클라이언트(웹 애플리케이션, 모바일 앱 등)의 관점에서는 수많은 마이크로서비스와 직접 통신하는 것이 복잡하고 비효율적일 수 있습니다.

이럴 때 필요한 것이 바로 API 게이트웨이입니다.

Nest Gateway · 책임과 배치

외부 HTTP 계약을 모으고 도메인 데이터 책임을 남깁니다

학습 예제는 Orders와 Gateway를 함께 실행해 Users gRPC와 결과를 조합합니다. CORS 외의 인증·권한·rate limit·request ID는 별도로 구성할 공통 정책입니다.

Gateway 외부 계약과 서비스 데이터 책임 학습용 Orders 앱 안에 Gateway HTTP route와 Orders provider가 함께 있고 Users는 별도 gRPC 프로세스다. Users 직접 route는 원격 호출, Orders 생성 route는 로컬 처리이다. details는 저장된 Order를 먼저 찾은 뒤 Users를 조회해 조합하므로 단순병렬호출이 아니다. 점선은 details가 필요한 원격조회로 이어가는 관계다. 도메인 데이터 책임은 각서비스에 남고 이 코드의 공통edge구현은CORS뿐이다. Orders · 학습용 HTTP 앱 Users route Orders route Users 결과 로컬 결과 details 조회 외부 HTTP client /api 아래 route Gateway route 예제: CORS 인증 · 제한 별도 Users · 별도 프로세스 gRPC 조회 · 생성 Orders provider 같은 학습 앱 주문 데이터 책임 외부 응답 구성 직접 route / details 실패 계약은 route별
route가 로컬 주문과 원격 Users를 선택한다 학습용 Orders 앱 안에 Gateway HTTP route와 Orders provider가 함께 있고 Users는 별도 gRPC 프로세스다. Users 직접 route는 원격 호출, Orders 생성 route는 로컬 처리이다. details는 저장된 Order를 먼저 찾은 뒤 Users를 조회해 조합하므로 단순병렬호출이 아니다. 점선은 details가 필요한 원격조회로 이어가는 관계다. 도메인 데이터 책임은 각서비스에 남고 이 코드의 공통edge구현은CORS뿐이다. 외부 HTTP client /api 아래 경로 Gateway route 예제 공통 정책: CORS 인증 · 제한은 별도 Users route gRPC 호출 원격 Users Orders route 로컬 provider 주문 책임 외부 응답 구성 직접 route / details 실패 계약은 route별
single entry

HTTP 진입부터 내부 호출까지 경계를 따라간다

  1. 클라이언트는 외부 API만 호출한다

    웹·모바일은 http://localhost:3000/api 아래의 route를 사용하며 Users gRPC 주소나 Orders 저장 방식을 알지 않습니다.

  2. Gateway가 edge 공통 정책을 적용한다

    신원 확인, route·scope 권한, CORS, 요청 제한, 외부 DTO와 응답 모양, request ID처럼 여러 route에 공통인 edge 정책을 진입 경계에서 일관되게 적용합니다.

  3. route가 도메인 처리 대상을 고른다

    /api/users는 UserService gRPC로 위임하고, /api/orders는 이 학습용 앱의 Orders 로직을 호출합니다. details route는 두 결과를 외부 응답으로 조합합니다.

  4. 내부 결과를 외부 계약으로 번역한다

    Gateway는 deadline과 gRPC status를 관측해 route별 HTTP 상태와 부분 응답 모양을 선택합니다. 내부 protocol 오류를 그대로 노출하지 않습니다.

Gateway owns

외부 계약과 공통 edge 정책

Route 선택, 신원 확인과 공통 scope 권한, rate limit, protocol 변환, 응답 조합, 실패 매핑과 요청 관측을 맡습니다. 리소스 소유자처럼 데이터가 필요한 도메인 권한은 각 서비스가 판단합니다.

Service owns

도메인 규칙과 데이터 소유권

Users는 사용자 조회·생성 규칙과 데이터를, Orders는 주문 생성·조회 규칙과 데이터를 소유합니다. Gateway가 응답을 조합해도 이 업무 판단을 가져오지 않습니다.

teaching topology

orders-service와 Gateway의 동거는 학습용이다

예제는 한 프로세스에서 HTTP route, Orders 로컬 처리, Users gRPC 호출과 composition을 모두 실행해 흐름을 작게 보여 줍니다. 운영 배치에서 둘을 같은 서비스로 묶으라는 권장이 아니며, 변경 주기·확장·권한·팀 소유권이 다르면 Gateway를 별도 애플리케이션으로 분리합니다.

route contract

각 route의 실제 처리 경로

GET /api/users/:userId

Gateway 동작: getUserById unary gRPC 호출
도메인 소유자: Users Service

POST /api/users

Gateway 동작: createUser unary gRPC 호출
도메인 소유자: Users Service

POST /api/orders

Gateway 동작: 이 학습용 앱의 Orders provider 호출
도메인 소유자: Orders 도메인

GET …/:orderId/details

Gateway 동작: Order 조회 후 Users gRPC 결과를 조합
도메인 소유자: 각 서비스가 원본을 소유하고 Gateway가 외부 모양만 구성

판정 기준: 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)와 같은 패턴을 적용하여 클라이언트에 안정적인 응답을 제공합니다.
API 게이트웨이의 장점
  • 클라이언트 복잡도 감소: 클라이언트가 여러 서비스 엔드포인트를 알 필요 없이 단일 엔트리포인트만 호출하면 됩니다.
  • 보안 강화: 신원 확인과 공통 edge 접근 정책을 중앙에서 처리하되, 서비스가 자기 리소스의 도메인 권한을 다시 검증하도록 경계를 나눕니다.
  • 유연한 마이크로서비스 변경: 백엔드 서비스 변경이 클라이언트에 미치는 영향을 최소화합니다.
  • 성능 최적화: 캐싱, 압축 등으로 응답 속도를 개선할 수 있습니다.
API 게이트웨이의 단점
  • 단일 실패 지점(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에서 실행 중이어야 합니다. 7장 3절에서 같은 프로젝트를 HTTP 서버로 바꾸었다면 main.ts, UsersController, UsersModule을 7장 2절의 gRPC 구성으로 되돌린 뒤 시작합니다. 7장 3절의 Users HTTP 프로세스는 종료해 새 API 게이트웨이의 3000번 포트와 충돌하지 않게 합니다.
  • 새로운 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 rxjs
단계 2: main.ts 파일 수정 (HTTP 서버 역할)

orders-service는 클라이언트로부터 HTTP 요청을 받아야 하므로, 일반적인 NestJS HTTP 서버로 구성됩니다.

orders-service/src/main.ts
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 폴더에 복사하거나, 별도의 공유 모듈/패키지를 통해 관리하는 것이 좋습니다.

7장 2절의 경로 표에서 선택한 배치를 유지합니다. 서버 원래 경로를 그대로 두었다면 서버는 공통 상위 폴더의 proto/users.proto를 읽습니다. 서버 프로젝트 내부 배치로 바꾸었다면 서버의 options.protoPath에 그 절의 ../proto/users.proto 변형을 적용합니다. 이 절의 Orders 모듈은 기본 dist/orders 위치에서 프로젝트 내부 proto를 읽으므로, 파일 이름뿐 아니라 package, service, RPC와 message tag도 같은지 확인합니다.

여기서는 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;
}
단계 4: OrdersModule에 gRPC 클라이언트 등록

orders-service가 users-service (gRPC)와 통신해야 하므로, gRPC 클라이언트를 등록합니다.

orders-service/src/orders/orders.module.ts (수정)
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 {}
단계 5: 주문 서비스 컨트롤러 (API 게이트웨이 로직 구현)

이 컨트롤러는 클라이언트의 HTTP 요청을 받아, 내부적으로 gRPC USERS_SERVICE를 호출하고, 그 결과를 HTTP 응답으로 변환합니다.

여기서는 7장 2절의 orders.controller.ts와 유사하지만, API Gateway의 라우팅과 조합 기능을 명확히 보여줍니다.

orders-service/src/orders/orders.controller.ts (수정)
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 mapGrpcError(error);
    }
  }

  @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 new BadGatewayException('Users Service request failed');
    }
  }
}
  • @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는 gRPC try 밖에서 결정됩니다. 따라서 이미 만든 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 구현 (주문 데이터 관리)
orders-service/src/orders/orders.service.ts (수정)
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);
  }
}
단계 7: AppModule에 OrdersModule 임포트 (동일)
orders-service/src/app.module.ts
import { Module } from '@nestjs/common';
import { OrdersModule } from './orders/orders.module';

@Module({
  imports: [OrdersModule],
  controllers: [],
  providers: [],
})
export class AppModule {}

게이트웨이 동작 검증

성공 응답 하나만 확인하지 말고 route 선택, 실제 상류 호출, deadline, 외부 실패 매핑과 부분 응답을 함께 검증합니다. 아래 다이어그램은 요청 경로와 route별 실패 계약, 로그·metric·trace 관측 지점을 한 검증표로 정리합니다.

Gateway 라우팅·부분 실패·외부 응답 검증

입력 검사·로컬 Order 확인·유한한 deadline RPC·응답 검증·완전/선택 부분/오류 응답으로 경계가 바뀐다.

Nest Gateway · route별 실패 계약

같은 상류 실패를 직접 route와 조합 route의 약속으로 번역합니다

직접 Users route는 HTTP 오류를 반환합니다. Order가 있는 details route만 허용한 세 실패를 user:null·dependencyStatus로 나타냅니다. 나머지 상류 실패는 502이며 telemetry는 추가 구성합니다.

같은 상류 결과를 route 약속에 따라 번역한다 입력정수검증실패는RPC전에400,details의로컬Order부재는RPC전에404이다. 직접Users와Order가있는details만유한deadline의RPC로간다. 직접route는OKGET200/POST201,정한상류실패는HTTP오류이다. details는OK200또는NOT_FOUND/DEADLINE_EXCEEDED/UNAVAILABLE만user:null과dependencyStatus를담은200으로반환한다. 둘의기타상류실패는502이며partial로숨기지않는다. 유효Order를읽기전원격조회가먼저시작되거나잘못된입력이상류에가는흐름이아니다. 통과 details Order 있음 직접 직접 details 기타 실패 HTTP 경로 검증 실패는 400 · RPC 없음 route 선택 직접 Users / details details Order 조회 없음 → 404 · RPC 없음 Users RPC 직접 또는 Order 있음 유한 deadline 상류 결과 분류 OK / 허용 실패 / 기타 직접 결과 OK → 200 / 201 실패 → HTTP 오류 details 결과 OK200 · 허용실패200 user:null + 상태 예상 밖 상류 실패 둘 다 502
먼저 종료할 조건과 route별 실패 계약 입력정수검증실패는RPC전에400,details의로컬Order부재는RPC전에404이다. 직접Users와Order가있는details만유한deadline의RPC로간다. 직접route는OKGET200/POST201,정한상류실패는HTTP오류이다. details는OK200또는NOT_FOUND/DEADLINE_EXCEEDED/UNAVAILABLE만user:null과dependencyStatus를담은200으로반환한다. 둘의기타상류실패는502이며partial로숨기지않는다. 유효Order를읽기전원격조회가먼저시작되거나잘못된입력이상류에가는흐름이아니다. 있음 HTTP 경로 검증 ParseIntPipe 실패 → 400 통과 route 선택 직접 Users / details 직접 route Users RPC 상류 결과 details Order 조회 없으면 404 Users RPC Order가 있을 때 조회 유한한 deadline 상류 결과 분류 OK / 허용 실패 / 기타 직접 결과 OK:200/201 실패 → HTTP 오류 details 결과 OK → 200 허용실패 → 200 + 상태 예상 밖 상류 실패 둘 다 502 partial로 숨기지 않음
request budget

요청 한 건의 routing·composition·failure 경계

  1. HTTP 입력과 공통 정책을 먼저 확정한다

    Route parameter는 ParseIntPipe 같은 HTTP 경계에서 검증합니다. 인증·권한·rate limit과 request ID도 내부 호출 전에 적용해 잘못된 요청이 상류 서비스로 번지지 않게 합니다.

  2. route가 실제 처리 경로를 선택한다

    Users route는 gRPC unary RPC로 위임하고 Orders 생성은 로컬 provider를 호출합니다. Details route는 먼저 Order를 찾고, 존재할 때만 Users 조회를 이어 갑니다.

  3. 상류 호출에 유한한 deadline을 건다

    예제는 Users RPC에 Date.now() + 1_500을 전달합니다. 조합 route는 남은 HTTP 시간 예산 안에서 필요한 호출을 끝내야 하며, 운영 값은 tail latency와 부하 측정으로 정합니다.

  4. gRPC status를 route 계약으로 분류한다

    직접 Users route는 실패를 HTTP 예외로 번역합니다. Details route는 계약이 허용한 NOT_FOUND, timeout, unavailable만 user: null과 상태 필드로 표현하고 예상 밖 오류는 502로 실패합니다.

  5. 외부 응답과 관측 신호를 함께 남긴다

    운영 telemetry에서는 최종 HTTP status, partial 여부, route template, upstream RPC, gRPC code와 duration을 같은 request/trace ID로 연결합니다. 최소 예제의 실패 로그만으로 이 완료 조건이 충족되지는 않습니다.

failure mapping

같은 상류 결과도 route 계약에 따라 다르게 보인다

OK

직접 Users route: GET 200 · POST 201 · User
Order details 조합 route: 200 · dependencyStatus = "ok"
판정: 요청한 전체 데이터가 준비됨

NOT_FOUND

직접 Users route: 404 · User not found
Order details 조합 route: 200 · user: null, not-found
판정: Order가 있을 때만 조합 route의 부분 응답 허용

deadline 초과

직접 Users route: 504 · upstream timeout
Order details 조합 route: 200 · user: null, timeout
판정: 시간 예산 초과를 명시

UNAVAILABLE

직접 Users route: 503 · 이 예제의 계약
Order details 조합 route: 200 · user: null, unavailable
판정: 다른 외부 계약은 502 또는 503을 선택할 수 있음

INVALID_ARGUMENT

직접 Users route: 400 · 잘못된 직접 Users 입력
Order details 조합 route: 502 · 저장된 Order에서 만든 상류 요청 실패
판정: 같은 gRPC code도 어느 경계의 입력인지에 따라 외부 의미가 다름

그 밖의 예상하지 못한 상류 실패

직접 Users route: 502 · upstream failure
Order details 조합 route: 502 · partial로 숨기지 않음
판정: 알 수 없는 실패를 성공처럼 반환하지 않음
Users routes

직접 위임을 검증한다

GET /api/users/:userId와 POST /api/users가 각각 올바른 gRPC 메서드를 호출하는지 확인합니다. 성공 200·201과 실패 status별 응답, deadline을 검증하고, 비멱등 생성 요청은 idempotency 계약 없이 명시적·정책 재시도를 추가하지 않습니다.

Orders route

로컬 처리는 불필요한 상류 호출이 없어야 한다

POST /api/orders가 Orders provider만 호출하고 Users gRPC를 호출하지 않는지 확인합니다. 이 route의 도메인 검증과 데이터 소유권은 Gateway 공통 정책이 아니라 Orders 책임입니다.

composition

조합 순서와 부분 실패를 함께 검증한다

Details route는 Order가 없으면 404로 끝나고 Users를 호출하지 않아야 합니다. Order가 있으면 Users 결과를 합치며, 허용한 실패만 안정된 dependencyStatus 값으로 반환합니다.

observability

응답과 내부 경로를 한 trace로 맞춘다

운영 관측 계층을 구성한 뒤 gRPC 호출 유무, code, duration, 부분 응답 flag와 최종 status를 비교합니다. Route template을 metric label로 쓰고 높은 cardinality의 ID와 payload는 trace·보호된 로그에서 다룹니다.

운영 완료 조건: route가 의도한 내부 경로만 타고, deadline과 실패 분류가 문서화한 외부 응답으로 나타나며, 추가한 telemetry의 같은 request/trace ID로 원인을 추적할 수 있어야 합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 입력 정수 검증 실패는 RPC 전에 400, details의 로컬 Order 부재는 RPC 전에 404로 끝난다. 직접 Users와 Order가 있는 details만 유한한 deadline의 RPC로 간다. 직접 route의 성공은 GET 200/POST 201, 정해진 상류 실패는 HTTP 오류다. details의 성공은 200이며 NOT_FOUND/DEADLINE_EXCEEDED/UNAVAILABLE만 user:null과 dependencyStatus를 담은 200으로 반환한다. 둘의 기타 상류 실패는 502이며 partial로 숨기지 않는다. 유효한 Order를 읽기 전 원격 조회가 먼저 시작되거나 잘못된 입력이 상류에 가는 흐름이 아니다.
관계도의 보충 설명
원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1Gateway 라우팅·부분 실패·외부 응답 검증
6입력 정수 검증 실패는 RPC 전에 400, details의 로컬 Order 부재는 RPC 전에 404로 끝난다. 직접 Users와 Order가 있는 details만 유한한 deadline의 RPC로 간다. 직접 route의 성공은 GET 200/POST 201, 정해진 상류 실패는 HTTP 오류다. details의 성공은 200이며 NOT_FOUND/DEADLINE_EXCEEDED/UNAVAILABLE만 user:null과 dependencyStatus를 담은 200으로 반환한다. 둘의 기타 상류 실패는 502이며 partial로 숨기지 않는다. 유효한 Order를 읽기 전 원격 조회가 먼저 시작되거나 잘못된 입력이 상류에 가는 흐름이 아니다.
27OK 200 · 허용 실패 200
45허용 실패 →
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
RPC 전 검사

정수 입력과 details의 로컬 Order를 확인합니다.

입력 실패: 400 · Order 없음: 404 · RPC: 미호출유효한 로컬 입력만 원격 경계를 넘습니다.
유한한 deadline RPC

직접 Users 요청 또는 유효한 Order의 details 요청이 RPC를 실행합니다.

제한: 유한한 deadline · 산출물: 상류 결과원격 결과를 정한 범위에서 기다립니다.
완전·부분·오류 응답

details의 정해진 세 status만 partial 200이고 기타 실패는 502입니다.

partial: NOT_FOUND·DEADLINE_EXCEEDED·UNAVAILABLE · 기타: 502허용한 부분 실패만 공개 결과로 좁힙니다.

Gateway 라우팅·부분 실패·외부 응답 검증

입력 검사·로컬 Order 확인·유한한 deadline RPC·응답 검증·완전/선택 부분/오류 응답으로 경계가 바뀐다.

Nest Gateway · route별 실패 계약

같은 상류 실패를 직접 route와 조합 route의 약속으로 번역합니다

직접 Users route는 HTTP 오류를 반환합니다. Order가 있는 details route만 허용한 세 실패를 user:null·dependencyStatus로 나타냅니다. 나머지 상류 실패는 502이며 telemetry는 추가 구성합니다.

같은 상류 결과를 route 약속에 따라 번역한다 입력정수검증실패는RPC전에400,details의로컬Order부재는RPC전에404이다. 직접Users와Order가있는details만유한deadline의RPC로간다. 직접route는OKGET200/POST201,정한상류실패는HTTP오류이다. details는OK200또는NOT_FOUND/DEADLINE_EXCEEDED/UNAVAILABLE만user:null과dependencyStatus를담은200으로반환한다. 둘의기타상류실패는502이며partial로숨기지않는다. 유효Order를읽기전원격조회가먼저시작되거나잘못된입력이상류에가는흐름이아니다. 통과 details Order 있음 직접 직접 details 기타 실패 HTTP 경로 검증 실패는 400 · RPC 없음 route 선택 직접 Users / details details Order 조회 없음 → 404 · RPC 없음 Users RPC 직접 또는 Order 있음 유한 deadline 상류 결과 분류 OK / 허용 실패 / 기타 직접 결과 OK → 200 / 201 실패 → HTTP 오류 details 결과 OK200 · 허용실패200 user:null + 상태 예상 밖 상류 실패 둘 다 502
먼저 종료할 조건과 route별 실패 계약 입력정수검증실패는RPC전에400,details의로컬Order부재는RPC전에404이다. 직접Users와Order가있는details만유한deadline의RPC로간다. 직접route는OKGET200/POST201,정한상류실패는HTTP오류이다. details는OK200또는NOT_FOUND/DEADLINE_EXCEEDED/UNAVAILABLE만user:null과dependencyStatus를담은200으로반환한다. 둘의기타상류실패는502이며partial로숨기지않는다. 유효Order를읽기전원격조회가먼저시작되거나잘못된입력이상류에가는흐름이아니다. 있음 HTTP 경로 검증 ParseIntPipe 실패 → 400 통과 route 선택 직접 Users / details 직접 route Users RPC 상류 결과 details Order 조회 없으면 404 Users RPC Order가 있을 때 조회 유한한 deadline 상류 결과 분류 OK / 허용 실패 / 기타 직접 결과 OK:200/201 실패 → HTTP 오류 details 결과 OK → 200 허용실패 → 200 + 상태 예상 밖 상류 실패 둘 다 502 partial로 숨기지 않음
request budget

요청 한 건의 routing·composition·failure 경계

  1. HTTP 입력과 공통 정책을 먼저 확정한다

    Route parameter는 ParseIntPipe 같은 HTTP 경계에서 검증합니다. 인증·권한·rate limit과 request ID도 내부 호출 전에 적용해 잘못된 요청이 상류 서비스로 번지지 않게 합니다.

  2. route가 실제 처리 경로를 선택한다

    Users route는 gRPC unary RPC로 위임하고 Orders 생성은 로컬 provider를 호출합니다. Details route는 먼저 Order를 찾고, 존재할 때만 Users 조회를 이어 갑니다.

  3. 상류 호출에 유한한 deadline을 건다

    예제는 Users RPC에 Date.now() + 1_500을 전달합니다. 조합 route는 남은 HTTP 시간 예산 안에서 필요한 호출을 끝내야 하며, 운영 값은 tail latency와 부하 측정으로 정합니다.

  4. gRPC status를 route 계약으로 분류한다

    직접 Users route는 실패를 HTTP 예외로 번역합니다. Details route는 계약이 허용한 NOT_FOUND, timeout, unavailable만 user: null과 상태 필드로 표현하고 예상 밖 오류는 502로 실패합니다.

  5. 외부 응답과 관측 신호를 함께 남긴다

    운영 telemetry에서는 최종 HTTP status, partial 여부, route template, upstream RPC, gRPC code와 duration을 같은 request/trace ID로 연결합니다. 최소 예제의 실패 로그만으로 이 완료 조건이 충족되지는 않습니다.

failure mapping

같은 상류 결과도 route 계약에 따라 다르게 보인다

OK

직접 Users route: GET 200 · POST 201 · User
Order details 조합 route: 200 · dependencyStatus = "ok"
판정: 요청한 전체 데이터가 준비됨

NOT_FOUND

직접 Users route: 404 · User not found
Order details 조합 route: 200 · user: null, not-found
판정: Order가 있을 때만 조합 route의 부분 응답 허용

deadline 초과

직접 Users route: 504 · upstream timeout
Order details 조합 route: 200 · user: null, timeout
판정: 시간 예산 초과를 명시

UNAVAILABLE

직접 Users route: 503 · 이 예제의 계약
Order details 조합 route: 200 · user: null, unavailable
판정: 다른 외부 계약은 502 또는 503을 선택할 수 있음

INVALID_ARGUMENT

직접 Users route: 400 · 잘못된 직접 Users 입력
Order details 조합 route: 502 · 저장된 Order에서 만든 상류 요청 실패
판정: 같은 gRPC code도 어느 경계의 입력인지에 따라 외부 의미가 다름

그 밖의 예상하지 못한 상류 실패

직접 Users route: 502 · upstream failure
Order details 조합 route: 502 · partial로 숨기지 않음
판정: 알 수 없는 실패를 성공처럼 반환하지 않음
Users routes

직접 위임을 검증한다

GET /api/users/:userId와 POST /api/users가 각각 올바른 gRPC 메서드를 호출하는지 확인합니다. 성공 200·201과 실패 status별 응답, deadline을 검증하고, 비멱등 생성 요청은 idempotency 계약 없이 명시적·정책 재시도를 추가하지 않습니다.

Orders route

로컬 처리는 불필요한 상류 호출이 없어야 한다

POST /api/orders가 Orders provider만 호출하고 Users gRPC를 호출하지 않는지 확인합니다. 이 route의 도메인 검증과 데이터 소유권은 Gateway 공통 정책이 아니라 Orders 책임입니다.

composition

조합 순서와 부분 실패를 함께 검증한다

Details route는 Order가 없으면 404로 끝나고 Users를 호출하지 않아야 합니다. Order가 있으면 Users 결과를 합치며, 허용한 실패만 안정된 dependencyStatus 값으로 반환합니다.

observability

응답과 내부 경로를 한 trace로 맞춘다

운영 관측 계층을 구성한 뒤 gRPC 호출 유무, code, duration, 부분 응답 flag와 최종 status를 비교합니다. Route template을 metric label로 쓰고 높은 cardinality의 ID와 payload는 trace·보호된 로그에서 다룹니다.

운영 완료 조건: route가 의도한 내부 경로만 타고, deadline과 실패 분류가 문서화한 외부 응답으로 나타나며, 추가한 telemetry의 같은 request/trace ID로 원인을 추적할 수 있어야 합니다.

이 예의 조건

  • 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
  • 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
  • 입력 정수 검증 실패는 RPC 전에 400, details의 로컬 Order 부재는 RPC 전에 404로 끝난다. 직접 Users와 Order가 있는 details만 유한한 deadline의 RPC로 간다. 직접 route의 성공은 GET 200/POST 201, 정해진 상류 실패는 HTTP 오류다. details의 성공은 200이며 NOT_FOUND/DEADLINE_EXCEEDED/UNAVAILABLE만 user:null과 dependencyStatus를 담은 200으로 반환한다. 둘의 기타 상류 실패는 502이며 partial로 숨기지 않는다. 유효한 Order를 읽기 전 원격 조회가 먼저 시작되거나 잘못된 입력이 상류에 가는 흐름이 아니다.

관계도의 보충 설명

원래 전체 그림의 표시 문구·조건·주석
순번원문 표시
1Gateway 라우팅·부분 실패·외부 응답 검증
6입력 정수 검증 실패는 RPC 전에 400, details의 로컬 Order 부재는 RPC 전에 404로 끝난다. 직접 Users와 Order가 있는 details만 유한한 deadline의 RPC로 간다. 직접 route의 성공은 GET 200/POST 201, 정해진 상류 실패는 HTTP 오류다. details의 성공은 200이며 NOT_FOUND/DEADLINE_EXCEEDED/UNAVAILABLE만 user:null과 dependencyStatus를 담은 200으로 반환한다. 둘의 기타 상류 실패는 502이며 partial로 숨기지 않는다. 유효한 Order를 읽기 전 원격 조회가 먼저 시작되거나 잘못된 입력이 상류에 가는 흐름이 아니다.
27OK 200 · 허용 실패 200
45허용 실패 →
단계별 상태·값과 전환 이유
장면관찰 값전환 이유
RPC 전 검사

정수 입력과 details의 로컬 Order를 확인합니다.

입력 실패: 400 · Order 없음: 404 · RPC: 미호출유효한 로컬 입력만 원격 경계를 넘습니다.
유한한 deadline RPC

직접 Users 요청 또는 유효한 Order의 details 요청이 RPC를 실행합니다.

제한: 유한한 deadline · 산출물: 상류 결과원격 결과를 정한 범위에서 기다립니다.
완전·부분·오류 응답

details의 정해진 세 status만 partial 200이고 기타 실패는 502입니다.

partial: NOT_FOUND·DEADLINE_EXCEEDED·UNAVAILABLE · 기타: 502허용한 부분 실패만 공개 결과로 좁힙니다.
Users Service (gRPC 서버) 시작
  • 7장 2절의 gRPC 구성으로 준비한 users-service 프로젝트로 이동합니다. 아래 로그 대신 Users Service (HTTP Server)가 표시되면 7장 3절의 HTTP 설정이 남아 있는지 확인합니다.
  • cd users-service
  • npm run start:dev
  • 콘솔에 Users gRPC Microservice is listening on localhost:50051 메시지 확인.
Orders Service (API Gateway) 시작
  • cd orders-service
  • npm run start:dev
  • 콘솔에 API Gateway (Orders Service) is listening on port 3000 메시지 확인.
API 테스트 (Postman 또는 cURL)
  • 새로운 사용자 생성 (API Gateway -> Users Service gRPC)
  • POST http://localhost:3000/api/users
  • Headers: Content-Type: application/json
  • Body: {"name": "Evan", "email": "[email protected]"}
  • 응답: {"id":3,"name":"Evan","email":"[email protected]"}. Users 서버를 새로 시작해 초기 배열에 Alice와 Bob만 있는 경우입니다. 같은 프로세스에서 앞 절의 Charlie를 이미 생성했다면 ID는 4이며, 이후 생성한 사용자 수에 따라 달라집니다.
  • Users Service 콘솔: Users gRPC Service: Received request to create user: {"name":"Evan","email":"[email protected]"} 확인.
  • 사용자 정보 조회 (API Gateway -> Users Service gRPC)
  • GET http://localhost:3000/api/users/1
  • 응답: {"id":1,"name":"Alice","email":"[email protected]"}
  • 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": "[email protected]"
      },
      "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이나 로그에 넣지 않습니다.

독립 연습: 같은 상류 실패가 다른 HTTP 결과가 되는 이유

① GET /api/users/not-a-number를 보냈습니다. ② 메모리에 주문 ID 1이 이미 있고, GET /api/orders/1/details의 Users 조회가 NOT_FOUND를 반환했습니다. ③ GET /api/users/999의 Users 조회도 NOT_FOUND입니다. 각 HTTP 결과와 실제 Users 호출 여부를 예측하세요. 인증 등 추가 정책은 구성하지 않은 이 코드만을 기준으로 합니다.

풀이 확인

①은 ParseIntPipe에서 400으로 끝나고 컨트롤러가 호출되기 전이므로 Users RPC가 없습니다. ②는 주문을 먼저 찾은 뒤 Users를 호출했으며, 이 조합 route의 계약은 200과 user: null, dependencyStatus: "not-found"를 반환합니다. ③은 Users를 직접 조회하는 route이므로 같은 gRPC NOT_FOUND를 HTTP 404로 매핑합니다. 상태 코드만 보면 ②의 데이터가 모두 준비됐다고 오해할 수 있으므로 소비자는 dependencyStatus를 함께 읽어야 합니다. 이 결과들은 Gateway라는 이름의 공통 보장이 아니라 각 메서드가 구현한 외부 계약입니다.


API 게이트웨이 패턴은 마이크로서비스 아키텍처의 복잡성을 줄이고, 클라이언트에 일관된 인터페이스를 제공하기 위한 구조입니다.

NestJS에서는 HTTP 서버 기능과 마이크로서비스 클라이언트 기능을 한 애플리케이션 안에서 함께 구성할 수 있습니다.

이것으로 7장 마이크로서비스 아키텍처를 마칩니다.

NestJS 마이크로서비스 구성, 통신 방식, API 게이트웨이 패턴을 한 흐름으로 정리했습니다. Gateway는 내부 복잡도를 숨기되 도메인 소유권을 가져오지 않고, 외부 실패 계약과 관측 가능성까지 명시할 때 안정적인 경계가 됩니다.