gRPC를 이용한 서비스 간 통신
gRPC(gRPC Remote Procedure Call)는 Google에서 개발한 고성능, 오픈소스 RPC(Remote Procedure Call) 프레임워크입니다.
이는 효율적인 데이터 직렬화와 강력한 API 계약을 제공하여 마이크로서비스 아키텍처에서 널리 사용됩니다.
gRPC vs RESTful API
- 프로토콜 : gRPC는 HTTP/2를 사용, REST는 주로 HTTP/1.1 사용
- 데이터 형식 : gRPC는 Protocol Buffers, REST는 주로 JSON 사용
- API 계약 : gRPC는 엄격한 타입 계약, REST는 상대적으로 유연함
- 성능 : gRPC가 일반적으로 더 높은 성능 제공
- 스트리밍 : gRPC는 양방향 스트리밍 지원, REST는 제한적
NestJS에 gRPC 설정
- 의존성 설치
npm install @nestjs/microservices @grpc/grpc-js @grpc/proto-loader
- Protocol Buffers 정의 (hero.proto)
syntax = "proto3";
package hero;
service HeroService {
rpc FindOne (HeroById) returns (Hero) {}
}
message HeroById {
int32 id = 1;
}
message Hero {
int32 id = 1;
string name = 2;
}
- NestJS 모듈 설정
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { HeroController } from './hero.controller';
@Module({
imports: [
ClientsModule.register([
{
name: 'HERO_PACKAGE',
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero.proto'),
},
},
]),
],
controllers: [HeroController],
})
export class HeroModule {}
gRPC 서버 구현
import { Controller, Get, Param } from '@nestjs/common';
import { GrpcMethod } from '@nestjs/microservices';
@Controller()
export class HeroController {
@GrpcMethod('HeroService', 'FindOne')
findOne(data: HeroById, metadata: any): Hero {
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
return items.find(({ id }) => id === data.id);
}
}
gRPC 클라이언트 구현
import { Controller, Get, Inject, OnModuleInit, Param } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
import { Observable } from 'rxjs';
import { HeroService } from './hero.interface';
@Controller('heroes')
export class HeroController implements OnModuleInit {
private heroService: HeroService;
constructor(@Inject('HERO_PACKAGE') private client: ClientGrpc) {}
onModuleInit() {
this.heroService = this.client.getService<HeroService>('HeroService');
}
@Get(':id')
call(@Param('id') id: string): Observable<any> {
return this.heroService.findOne({ id: +id });
}
}
gRPC 스트리밍
- 서버 스트리밍
rpc FindMany (HeroById) returns (stream Hero) {}
@GrpcMethod('HeroService', 'FindMany')
findMany(data: HeroById): Observable<Hero> {
const items = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
return from(items);
}
- 클라이언트 스트리밍
rpc CreateMany (stream HeroById) returns (Heroes) {}
@GrpcMethod('HeroService', 'CreateMany')
createMany(data$: Observable<HeroById>): Promise<Heroes> {
const heroes = [];
return new Promise((resolve, reject) => {
data$.subscribe({
next: (hero) => heroes.push(hero),
error: (err) => reject(err),
complete: () => resolve({ heroes }),
});
});
}
- 양방향 스트리밍
rpc FindManyStream (stream HeroById) returns (stream Hero) {}
@GrpcStreamMethod('HeroService', 'FindManyStream')
findManyStream(data$: Observable<HeroById>): Observable<Hero> {
const heroes = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Doe' },
];
return data$.pipe(
map(({ id }) => heroes.find(hero => hero.id === id)),
);
}
gRPC 인터셉터
로깅 인터셉터 예시
import { Injectable } from '@nestjs/common';
import { RpcException } from '@nestjs/microservices';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements GrpcInterceptor {
intercept(context: GrpcContext, next: (context: GrpcContext) => Observable<any>): Observable<any> {
console.log('Before...');
const now = Date.now();
return next(context)
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
catchError(err => throwError(() => new RpcException(err.message)))
);
}
}
gRPC 서비스 테스팅
단위 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { HeroController } from './hero.controller';
describe('HeroController', () => {
let controller: HeroController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HeroController],
}).compile();
controller = module.get<HeroController>(HeroController);
});
it('should return a hero', () => {
expect(controller.findOne({ id: 1 }, {})).toEqual({ id: 1, name: 'John' });
});
});
통합 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { ClientGrpc, ClientsModule, Transport } from '@nestjs/microservices';
import { join } from 'path';
describe('HeroController (e2e)', () => {
let app: INestApplication;
let client: ClientGrpc;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
ClientsModule.register([
{
name: 'HERO_PACKAGE',
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero.proto'),
},
},
]),
],
}).compile();
app = moduleFixture.createNestApplication();
app.connectMicroservice({
transport: Transport.GRPC,
options: {
package: 'hero',
protoPath: join(__dirname, 'hero.proto'),
},
});
await app.startAllMicroservices();
await app.init();
client = app.get('HERO_PACKAGE');
});
it('should return a hero', async () => {
const heroService = client.getService<HeroService>('HeroService');
const hero = await heroService.findOne({ id: 1 }).toPromise();
expect(hero).toEqual({ id: 1, name: 'John' });
});
afterAll(async () => {
await app.close();
});
});
성능 최적화 및 모니터링
- 프로토콜 버퍼 최적화 : 필드 번호 최적화, 불필요한 필드 제거
- 연결 풀링 : gRPC 클라이언트 연결 재사용
- 비동기 처리 : 비동기 호출을 통한 병렬 처리
- 압축 사용 : gzip 압축 활성화
- 모니터링 : Prometheus와 Grafana를 사용한 메트릭 수집 및 시각화
Best Practices와 주의사항
- 프로토콜 버퍼 버전 관리 : 하위 호환성 유지
- 에러 처리 : 명확한 에러 코드 및 메시지 정의
- 타임아웃 설정 : 적절한 타임아웃 값 설정
- 보안 : TLS/SSL 사용, 토큰 기반 인증 구현
- 문서화 : proto 파일 주석을 통한 API 문서화
- 테스트 자동화 : 단위 테스트 및 통합 테스트 구현
- 로깅 : 구조화된 로깅 구현
- 헬스 체크 : gRPC 헬스 체크 서비스 구현
- 백프레셔 처리 : 클라이언트 측 스트리밍에서 백프레셔 고려
- 모니터링 : 호출 빈도, 지연 시간, 에러율 등 주요 메트릭 모니터링
NestJS에서 gRPC를 사용하면 고성능의 타입 안전한 마이크로서비스 통신을 구현할 수 있습니다. Protocol Buffers를 통한 강력한 계약 정의와 HTTP/2 기반의 효율적인 통신은 서비스 간 상호작용을 최적화합니다.
gRPC 서버와 클라이언트 구현은 NestJS의 데코레이터와 DI 시스템을 활용하여 간결하고 직관적으로 작성할 수 있습니다. 다양한 스트리밍 옵션을 통해 실시간 데이터 처리와 양방향 통신을 효과적으로 구현할 수 있습니다.
인터셉터를 사용하면 로깅, 에러 처리, 인증 등의 횡단 관심사를 깔끔하게 처리할 수 있습니다. 이는 코드의 재사용성과 유지보수성을 높이는 데 도움이 됩니다.
테스팅 전략은 단위 테스트부터 통합 테스트까지 다양한 수준에서 이루어져야 합니다. NestJS의 테스팅 모듈과 gRPC 클라이언트를 활용하면 효과적인 테스트 슈트를 구성할 수 있습니다.
성능 최적화를 위해서는 프로토콜 버퍼 설계, 연결 관리, 비동기 처리 등 다양한 측면을 고려해야 합니다. 모니터링 도구를 통해 서비스의 성능과 건강 상태를 지속적으로 관찰하는 것도 중요합니다.
마지막으로, gRPC 서비스를 설계하고 구현할 때는 버전 관리, 에러 처리, 보안, 문서화 등 다양한 측면을 고려해야 합니다. 이러한 Best Practices를 따르면 안정적이고 확장 가능한 마이크로서비스 아키텍처를 구축할 수 있습니다.
NestJS와 gRPC의 조합은 강력하고 효율적인 마이크로서비스 통신을 가능하게 합니다. 타입 안전성, 고성능, 양방향 스트리밍 등의 이점을 활용하여 복잡한 분산 시스템을 효과적으로 구축할 수 있습니다. 지속적인 모니터링과 최적화를 통해 시스템의 성능과 안정성을 유지하고 개선해 나가는 것이 중요합니다.