API 게이트웨이 패턴 구현
지난 절에서는 NestJS와 Kafka를 활용하여 마이크로서비스 간 비동기 통신 및 이벤트 기반 아키텍처를 구현하는 방법을 살펴보았습니다. 이제 7장의 마지막으로, 마이크로서비스 아키텍처에서 중요한 역할을 하는 API 게이트웨이(API Gateway) 패턴에 대해 알아보고, NestJS를 사용하여 이를 구현하는 방법에 대해 자세히 설명하겠습니다.
마이크로서비스 아키텍처는 백엔드를 여러 개의 작은 서비스로 분해하여 독립적인 개발, 배포, 확장을 가능하게 합니다. 그러나 클라이언트(웹 애플리케이션, 모바일 앱 등)의 관점에서는 수많은 마이크로서비스와 직접 통신하는 것이 복잡하고 비효율적일 수 있습니다. 이럴 때 필요한 것이 바로 API 게이트웨이입니다.
게이트웨이 패턴이란?
API 게이트웨이는 클라이언트의 모든 API 요청을 단일 진입점으로 받아들이고, 이 요청을 적절한 마이크로서비스로 라우팅하는 서비스입니다. 즉, 클라이언트와 백엔드 마이크로서비스 간의 중개자 역할을 수행합니다.
API 게이트웨이의 주요 역할
- 요청 라우팅(Request Routing): 클라이언트의 요청 URL이나 헤더 등을 분석하여 해당 요청을 처리할 적절한 마이크로서비스로 전달합니다.
- 요청 합성(Request Composition): 여러 마이크로서비스의 응답을 받아 클라이언트에 필요한 형태로 조합하여 단일 응답으로 제공합니다. (Backend For Frontend, BFF 패턴과도 관련)
- 인증 및 권한 부여(Authentication & Authorization): 모든 요청에 대해 일관된 인증 및 권한 부여 로직을 적용하여 개별 마이크로서비스에 중복 구현하는 것을 방지합니다.
- 속도 제한(Rate Limiting): 특정 클라이언트나 사용자로부터의 요청 속도를 제한하여 서비스 과부하를 방지합니다.
- 캐싱(Caching): 자주 요청되는 데이터를 캐싱하여 백엔드 서비스의 부하를 줄이고 응답 속도를 높입니다.
- 로깅 및 모니터링(Logging & Monitoring): 모든 API 요청에 대한 중앙 집중식 로깅 및 모니터링 기능을 제공합니다.
- 프로토콜 변환(Protocol Translation): 클라이언트는 HTTP/REST로 요청하고, 게이트웨이가 이를 gRPC나 Kafka 이벤트 등으로 변환하여 백엔드 서비스와 통신할 수 있습니다.
- 장애 처리(Fault Tolerance): 백엔드 서비스의 장애를 감지하고, 서킷 브레이커(Circuit Breaker)나 재시도(Retry)와 같은 패턴을 적용하여 클라이언트에 안정적인 응답을 제공합니다.
API 게이트웨이의 장점
- 클라이언트 복잡도 감소: 클라이언트가 여러 서비스 엔드포인트를 알 필요 없이 단일 엔트리포인트만 호출하면 됩니다.
- 보안 강화: 인증/권한 부여를 중앙에서 처리하여 보안을 일관되게 적용할 수 있습니다.
- 유연한 마이크로서비스 변경: 백엔드 서비스 변경이 클라이언트에 미치는 영향을 최소화합니다.
- 성능 최적화: 캐싱, 압축 등으로 응답 속도를 개선할 수 있습니다.
API 게이트웨이의 단점
- 단일 실패 지점(Single Point of Failure): 게이트웨이가 다운되면 전체 시스템이 마비될 수 있습니다 (고가용성 확보 필요).
- 성능 병목(Performance Bottleneck): 모든 요청이 게이트웨이를 통과하므로, 게이트웨이가 성능 병목이 될 수 있습니다 (확장성 고려).
- 복잡성 증가: 게이트웨이 자체의 개발, 배포, 유지보수 복잡성이 추가됩니다.
NestJS에서 API 게이트웨이 구현하기
NestJS는 강력한 HTTP 서버 기능과 마이크로서비스 클라이언트 기능을 동시에 제공하여 API 게이트웨이를 구축하는 데 매우 적합합니다. 여기서는 orders-service
를 API 게이트웨이 역할을 겸하도록 확장하여 이전의 users-service
와 통신하는 예시를 보여드리겠습니다.
시나리오: 웹 클라이언트가 orders-service
(API 게이트웨이)에 HTTP 요청을 보내면, orders-service
는 내부적으로 users-service
(gRPC)를 호출하여 데이터를 조합하고 클라이언트에 응답합니다.
사전 준비
- 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 rxjs
npm install --save-dev @types/grpc__proto-loader
단계 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();
단계 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;
}
단계 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 { Controller, Get, Post, Body, Param, Inject, OnModuleInit, HttpException, HttpStatus } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { OrdersService } from './orders.service';
import { Observable, lastValueFrom } from 'rxjs';
// .proto 파일에 정의된 서비스 인터페이스 정의
interface UserService {
GetUserById(data: { id: number }): Observable<User>;
CreateUser(data: { name: string; email: string }): Observable<User>;
}
interface User {
id: number;
name: string;
email: string;
}
interface Order {
id: number;
userId: number;
item: string;
createdAt: Date;
}
@Controller('api') // 모든 API 요청의 접두사 (API Gateway 역할 명시)
export class OrdersController implements OnModuleInit {
private userService: UserService;
constructor(
private readonly ordersService: OrdersService,
@Inject('USERS_SERVICE') private readonly client: ClientGrpc,
) {}
onModuleInit() {
this.userService = this.client.getService<UserService>('UserService');
}
// --- API 게이트웨이 라우팅 및 요청 조합 예시 ---
// GET /api/users/:userId : 사용자 정보 조회 (Users Service로 라우팅)
@Get('users/:userId')
async getUser(@Param('userId') userId: string): Promise<User> {
const id = parseInt(userId, 10);
if (isNaN(id)) {
throw new HttpException('Invalid user ID', HttpStatus.BAD_REQUEST);
}
try {
const user = await lastValueFrom(this.userService.GetUserById({ id }));
if (!user || user.id === 0) { // gRPC에서 사용자 없는 경우 id=0으로 가정
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
return user;
} catch (error) {
console.error('gRPC Error for getUser:', error.details || error.message);
// gRPC 에러를 HTTP 에러로 변환하여 클라이언트에 전달
throw new HttpException(
`Failed to fetch user: ${error.details || error.message}`,
error.code === 5 ? HttpStatus.NOT_FOUND : HttpStatus.INTERNAL_SERVER_ERROR // gRPC NOT_FOUND(5)를 HTTP 404로 매핑
);
}
}
// POST /api/users : 사용자 생성 (Users Service로 라우팅)
@Post('users')
async createUser(@Body() userDto: { name: string; email: string }): Promise<User> {
try {
const newUser = await lastValueFrom(this.userService.CreateUser(userDto));
return newUser;
} catch (error) {
console.error('gRPC Error for createUser:', error.details || error.message);
throw new HttpException(
`Failed to create user: ${error.details || error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
// POST /api/orders : 주문 생성 (Orders Service 자체 처리)
@Post('orders')
createOrder(@Body() orderDto: { userId: number; item: string }): Order {
return this.ordersService.createOrder(orderDto.userId, orderDto.item);
}
// GET /api/orders/:orderId/details : 주문 및 관련 사용자 정보 조합 (Orders Service + Users Service)
// 이 엔드포인트는 API 게이트웨이의 "요청 합성" 기능을 보여줍니다.
@Get('orders/:orderId/details')
async getOrderDetails(@Param('orderId') orderId: string): Promise<any> {
const id = parseInt(orderId, 10);
if (isNaN(id)) {
throw new HttpException('Invalid order ID', HttpStatus.BAD_REQUEST);
}
// 1. 주문 정보 조회 (Orders Service 자체 로직)
const order = this.ordersService.findOne(id);
if (!order) {
throw new HttpException('Order not found', HttpStatus.NOT_FOUND);
}
// 2. 사용자 정보 조회 (Users Service 호출 - gRPC)
try {
const user = await lastValueFrom(this.userService.GetUserById({ id: order.userId }));
if (!user || user.id === 0) {
// 사용자 정보가 없더라도 주문 정보는 반환
return { order, user: null, message: 'User not found for this order.' };
}
return { order, user }; // 두 서비스의 응답을 조합하여 반환
} catch (error) {
console.error('gRPC Error for getOrderDetails (user):', error.details || error.message);
// 사용자 서비스에 문제가 있더라도 주문 정보는 반환
return { order, user: null, message: `Failed to fetch user details: ${error.details || error.message}` };
}
}
}
@Controller('api')
: 모든 엔드포인트에/api
접두사를 붙여 API 게이트웨이 역할을 명시합니다.- 라우팅:
/api/users
경로는users-service
로 라우팅되고,/api/orders
는orders-service
자체에서 처리됩니다. - 요청 합성:
/api/orders/:orderId/details
엔드포인트는orders-service
내부에서 주문 정보를 조회한 후, 해당 주문의userId
를 가지고 다시users-service
를 호출하여 사용자 정보를 가져옵니다. 최종적으로 두 정보를 조합하여 클라이언트에 단일 응답을 제공합니다. - 에러 처리: gRPC에서 발생한 에러를 캐치하여 HTTP 상태 코드와 메시지로 변환하여 클라이언트에 전달합니다. (예:
status.NOT_FOUND
(gRPC 코드 5)를HttpStatus.NOT_FOUND
(HTTP 404)로 매핑).
단계 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 {}
실행 및 테스트
Users Service (gRPC 서버) 시작
- 7.2절에서 만든
users-service
프로젝트로 이동합니다. 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": "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" } }
- Users Service 콘솔:
Users gRPC Service: Received request for user ID: 1
확인.
API 게이트웨이 패턴은 마이크로서비스 아키텍처의 복잡성을 관리하고, 클라이언트에게 일관되고 단순화된 인터페이스를 제공하는 데 필수적인 요소입니다. NestJS는 HTTP 서버 기능과 다양한 마이크로서비스 클라이언트 기능을 하나의 애플리케이션 내에서 효과적으로 통합할 수 있게 함으로써, 유연하고 강력한 API 게이트웨이를 구축하는 데 최적의 프레임워크 중 하나입니다.
이것으로 7장 "마이크로서비스 아키텍처"를 모두 마칩니다. 이제 여러분은 NestJS를 사용하여 마이크로서비스를 구축하고, 다양한 통신 방식을 활용하며, API 게이트웨이 패턴을 통해 클라이언트와 효율적으로 상호작용하는 방법을 이해하게 되었습니다.