NestJS + gRPC 엔터프라이즈 아키텍처
확장 가능한 마이크로서비스 백엔드 구축을 위한 기본 골조
📋 개요
이 가이드는 NestJS와 gRPC를 활용한 엔터프라이즈급 백엔드 아키텍처의 기본 골조를 설명합니다. REST 대신 gRPC를 채택하여 타입 안전성과 성능을 확보하고, 계층형 아키텍처로 관심사를 명확히 분리합니다.
- 타입 안전성: Proto → TypeScript 자동 생성
- 성능: Binary 프로토콜로 빠른 직렬화
- 확장성: 모듈 기반 구조
- 테스트 용이: 계층 분리로 단위 테스트 쉬움
- Runtime: Node.js 20 + TypeScript 5.4
- Framework: NestJS 10.x + Fastify
- Protocol: gRPC + Protocol Buffers
- Database: PostgreSQL + TypeORM
🔄 요청 처리 흐름
클라이언트 요청은 다음 계층을 순차적으로 거쳐 처리됩니다.
| 계층 | 역할 | 예시 |
|---|---|---|
| Interceptor | 요청/응답 변환, 메타데이터 추출 | UserMetadataInterceptor |
| Guard | 인증/인가 검증 | AuthGuard, RoleGuard |
| Pipe | 데이터 변환 및 검증 | ZodValidationPipe |
| Controller | 요청 라우팅, gRPC 핸들러 | @GrpcMethod 데코레이터 |
| Service | 비즈니스 로직 | 트랜잭션, 외부 API 호출 |
| Repository | 데이터 접근 추상화 | TypeORM 쿼리 |
📁 폴더 구조
기능별 모듈 구조로 관심사를 분리합니다. 각 모듈은 독립적으로 동작할 수 있습니다.
src/ ├── main.ts # 애플리케이션 진입점 ├── app.module.ts # 루트 모듈 │ ├── config/ # 환경 설정 │ └── app.config.ts │ ├── constants/ # 상수 정의 │ ├── error-message.const.ts │ └── inject-token.const.ts │ ├── modules/ # 기능 모듈 │ ├── feature-a/ # 도메인 모듈 예시 │ │ ├── feature-a.module.ts │ │ ├── controllers/ │ │ ├── services/ │ │ ├── repositories/ │ │ ├── entities/ │ │ ├── dtos/ │ │ ├── mappers/ │ │ ├── validations/ # Zod 스키마 │ │ └── protos/ # gRPC 정의 │ │ │ ├── database/ # DB 연결 설정 │ └── shared/ # 공유 모듈 │ ├── guards/ │ ├── interceptors/ │ └── pipes/ │ └── utils/ # 유틸리티 함수
⚡ gRPC 서비스 설정
1. Proto 파일 정의
서비스 인터페이스를 Protocol Buffers로 정의합니다.
example.protosyntax = "proto3"; package example; // 서비스 정의 service ExampleService { rpc GetItem(GetItemRequest) returns (GetItemResponse); rpc CreateItem(CreateItemRequest) returns (CreateItemResponse); rpc ListItems(ListItemsRequest) returns (ListItemsResponse); } // 메시지 정의 message GetItemRequest { string id = 1; } message GetItemResponse { string id = 1; string name = 2; int32 quantity = 3; }
2. NestJS gRPC 부트스트랩
main.tsimport { NestFactory } from '@nestjs/core'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { join } from 'path'; async function bootstrap() { const app = await NestFactory.create(AppModule); // gRPC 마이크로서비스 연결 app.connectMicroservice<MicroserviceOptions>({ transport: Transport.GRPC, options: { package: 'example', protoPath: join(__dirname, './protos/example.proto'), url: '0.0.0.0:5000', maxReceiveMessageLength: 10 * 1024 * 1024, // 10MB }, }); await app.startAllMicroservices(); await app.listen(3000); }
3. Controller 구현
example.controller.tsimport { Controller, UseGuards } from '@nestjs/common'; import { GrpcMethod, Payload, Metadata } from '@nestjs/microservices'; @Controller() export class ExampleController { constructor(private readonly exampleService: ExampleService) {} @GrpcMethod('ExampleService', 'GetItem') @UseGuards(AuthGuard) async getItem( @Payload(new ZodValidationPipe(getItemSchema)) request: GetItemRequest, @Metadata() metadata: GrpcMetadata, ): Promise<GetItemResponse> { return this.exampleService.getItem(request.id); } }
🛡️ Zod 런타임 검증
TypeScript의 컴파일 타임 검증을 넘어, Zod로 런타임에도 요청 데이터를 검증합니다.
class-validator 대비 더 간결한 문법, 자동 타입 추론, 그리고 복잡한 검증 로직을 쉽게 표현할 수 있습니다.
검증 스키마 정의
validations/get-item.schema.tsimport { z } from 'zod'; export const getItemSchema = z.object({ id: z.string().uuid('유효한 UUID 형식이 아닙니다'), }); export const createItemSchema = z.object({ name: z.string().min(1).max(100), quantity: z.number().int().positive(), tags: z.array(z.string()).optional(), metadata: z.record(z.string()).optional(), }); // 타입 자동 추론 export type CreateItemInput = z.infer<typeof createItemSchema>;
ValidationPipe 구현
shared/pipes/zod-validation.pipe.tsimport { PipeTransform, BadRequestException } from '@nestjs/common'; import { ZodSchema, ZodError } from 'zod'; export class ZodValidationPipe<T> implements PipeTransform<unknown, T> { constructor(private schema: ZodSchema<T>) {} transform(value: unknown): T { const result = this.schema.safeParse(value); if (!result.success) { const errors = result.error.errors.map(err => ({ field: err.path.join('.'), message: err.message, })); throw new BadRequestException({ errors }); } return result.data; } }
🔐 Guard를 활용한 인증/인가
Guard는 요청이 Controller에 도달하기 전에 권한을 검증합니다.
shared/guards/auth.guard.tsimport { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; @Injectable() export class AuthGuard implements CanActivate { constructor( private readonly userRepository: UserRepository, ) {} async canActivate(context: ExecutionContext): Promise<boolean> { const rpcContext = context.switchToRpc(); const metadata = rpcContext.getContext(); // gRPC 메타데이터에서 인증 정보 추출 const userId = metadata.get('user-id')?.[0]; const token = metadata.get('authorization')?.[0]; if (!userId || !token) { return false; } // 사용자 검증 로직 const user = await this.userRepository.findById(userId); return user !== null && user.isActive; } }
🔄 Interceptor로 메타데이터 처리
Interceptor는 요청 전후에 공통 로직을 실행합니다. 메타데이터 추출, 로깅, 응답 변환에 활용됩니다.
shared/interceptors/user-metadata.interceptor.tsimport { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { Observable } from 'rxjs'; @Injectable() export class UserMetadataInterceptor implements NestInterceptor { constructor( private readonly userProfileRepository: UserProfileRepository, ) {} async intercept( context: ExecutionContext, next: CallHandler, ): Promise<Observable<any>> { const rpcContext = context.switchToRpc(); const metadata = rpcContext.getContext(); // 1. 메타데이터에서 사용자 ID 추출 const userId = metadata.get('user-id')?.[0]; if (userId) { // 2. 사용자 프로필 조회 const profile = await this.userProfileRepository.findByUserId(userId); // 3. 메타데이터에 추가 정보 설정 metadata.set('user-profile', profile); metadata.set('user-role', profile?.role); } return next.handle(); } }
💾 Repository 패턴
데이터 접근 로직을 Service에서 분리하여 테스트 용이성과 유지보수성을 높입니다.
repositories/example.repository.tsimport { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @Injectable() export class ExampleRepository { constructor( @InjectRepository(ExampleEntity, 'main') // 연결 이름 지정 private readonly repository: Repository<ExampleEntity>, ) {} async findById(id: string): Promise<ExampleEntity | null> { return this.repository.findOne({ where: { id }, relations: ['relatedEntity'], }); } async findByCondition(params: FindParams): Promise<ExampleEntity[]> { const query = this.repository .createQueryBuilder('e') .where('e.status = :status', { status: params.status }); if (params.startDate) { query.andWhere('e.createdAt >= :start', { start: params.startDate }); } return query.getMany(); } async save(entity: ExampleEntity): Promise<ExampleEntity> { return this.repository.save(entity); } }
🗄️ 다중 데이터베이스 연결
TypeORM으로 여러 PostgreSQL 인스턴스에 동시 연결합니다.
modules/database/database.module.tsimport { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigService } from '@nestjs/config'; @Module({ imports: [ // 주 데이터베이스 TypeOrmModule.forRootAsync({ name: 'main', useFactory: (config: ConfigService) => ({ type: 'postgres', host: config.get('MAIN_DB_HOST'), port: config.get('MAIN_DB_PORT'), database: config.get('MAIN_DB_NAME'), entities: [MainEntity, AnotherEntity], synchronize: false, }), inject: [ConfigService], }), // 읽기 전용 데이터베이스 TypeOrmModule.forRootAsync({ name: 'readonly', useFactory: (config: ConfigService) => ({ type: 'postgres', host: config.get('READONLY_DB_HOST'), // ... 설정 }), inject: [ConfigService], }), ], }) export class DatabaseModule {}
📐 핵심 아키텍처 원칙
Controller → Service → Repository → Entity 순서로 의존성을 단방향으로 유지합니다.
NestJS IoC 컨테이너로 느슨한 결합을 유지하고 테스트 시 Mock 주입이 용이합니다.
각 클래스는 하나의 책임만 가집니다. Controller는 라우팅, Service는 비즈니스 로직만 담당합니다.
Entity와 DTO 간 변환을 Mapper 클래스로 분리하여 계층 간 결합도를 낮춥니다.
| 패턴 | 적용 위치 | 목적 |
|---|---|---|
| Guard 패턴 | 요청 전처리 | 인증/인가 검증 |
| Interceptor 패턴 | 요청/응답 가로채기 | 로깅, 메타데이터 처리, 응답 변환 |
| Pipe 패턴 | 데이터 변환 | 검증, 타입 변환 |
| Repository 패턴 | 데이터 접근 | DB 로직 캡슐화 |
| Factory 패턴 | 객체 생성 | 파일 생성기 등 동적 객체 생성 |
✅ 정리
이 아키텍처는 다음과 같은 상황에 적합합니다:
- 마이크로서비스 간 gRPC 통신이 필요한 경우
- 타입 안전성이 중요한 엔터프라이즈 환경
- 복잡한 인증/인가 로직이 필요한 경우
- 여러 데이터베이스에 동시 접근해야 하는 경우
- 확장 가능한 모듈 구조가 필요한 경우
실제 프로젝트 구성을 위해서는 package.json, tsconfig.json, Docker Compose 등의 상세 설정이 담긴 설치 가이드 문서가 추가로 필요합니다.
'Tech Notes' 카테고리의 다른 글
| Next.js 긴급 보안 취약점 CVE-2025-66478 정리 (0) | 2026.01.12 |
|---|---|
| Python 데이터 분석 4대 라이브러리 정리 (0) | 2026.01.07 |
| 2025 AI 코딩 도구 총정리 (0) | 2025.12.17 |
| Python OnPromise Structure 비교 Electron + Python vs PyWebView + PyInstaller (0) | 2025.12.08 |
| Electron Desktop App으로 웹 앱을 데스크톱 앱으로 만들기 (1) | 2025.11.13 |