icon

gRPC를 이용한 서비스 간 통신


 gRPC(gRPC Remote Procedure Call)는 Google에서 개발한 고성능, 오픈소스 RPC(Remote Procedure Call) 프레임워크입니다.

 이는 효율적인 데이터 직렬화와 강력한 API 계약을 제공하여 마이크로서비스 아키텍처에서 널리 사용됩니다.

gRPC vs RESTful API

  1. 프로토콜 : gRPC는 HTTP/2를 사용, REST는 주로 HTTP/1.1 사용
  2. 데이터 형식 : gRPC는 Protocol Buffers, REST는 주로 JSON 사용
  3. API 계약 : gRPC는 엄격한 타입 계약, REST는 상대적으로 유연함
  4. 성능 : gRPC가 일반적으로 더 높은 성능 제공
  5. 스트리밍 : gRPC는 양방향 스트리밍 지원, REST는 제한적

NestJS에 gRPC 설정

  1. 의존성 설치
npm install @nestjs/microservices @grpc/grpc-js @grpc/proto-loader
  1. 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;
}
  1. 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 스트리밍

  1. 서버 스트리밍
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);
}
  1. 클라이언트 스트리밍
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 }),
    });
  });
}
  1. 양방향 스트리밍
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();
  });
});

성능 최적화 및 모니터링

  1. 프로토콜 버퍼 최적화 : 필드 번호 최적화, 불필요한 필드 제거
  2. 연결 풀링 : gRPC 클라이언트 연결 재사용
  3. 비동기 처리 : 비동기 호출을 통한 병렬 처리
  4. 압축 사용 : gzip 압축 활성화
  5. 모니터링 : Prometheus와 Grafana를 사용한 메트릭 수집 및 시각화

Best Practices와 주의사항

  1. 프로토콜 버퍼 버전 관리 : 하위 호환성 유지
  2. 에러 처리 : 명확한 에러 코드 및 메시지 정의
  3. 타임아웃 설정 : 적절한 타임아웃 값 설정
  4. 보안 : TLS/SSL 사용, 토큰 기반 인증 구현
  5. 문서화 : proto 파일 주석을 통한 API 문서화
  6. 테스트 자동화 : 단위 테스트 및 통합 테스트 구현
  7. 로깅 : 구조화된 로깅 구현
  8. 헬스 체크 : gRPC 헬스 체크 서비스 구현
  9. 백프레셔 처리 : 클라이언트 측 스트리밍에서 백프레셔 고려
  10. 모니터링 : 호출 빈도, 지연 시간, 에러율 등 주요 메트릭 모니터링

 NestJS에서 gRPC를 사용하면 고성능의 타입 안전한 마이크로서비스 통신을 구현할 수 있습니다. Protocol Buffers를 통한 강력한 계약 정의와 HTTP/2 기반의 효율적인 통신은 서비스 간 상호작용을 최적화합니다.

 gRPC 서버와 클라이언트 구현은 NestJS의 데코레이터와 DI 시스템을 활용하여 간결하고 직관적으로 작성할 수 있습니다. 다양한 스트리밍 옵션을 통해 실시간 데이터 처리와 양방향 통신을 효과적으로 구현할 수 있습니다.

 인터셉터를 사용하면 로깅, 에러 처리, 인증 등의 횡단 관심사를 깔끔하게 처리할 수 있습니다. 이는 코드의 재사용성과 유지보수성을 높이는 데 도움이 됩니다.

 테스팅 전략은 단위 테스트부터 통합 테스트까지 다양한 수준에서 이루어져야 합니다. NestJS의 테스팅 모듈과 gRPC 클라이언트를 활용하면 효과적인 테스트 슈트를 구성할 수 있습니다.

 성능 최적화를 위해서는 프로토콜 버퍼 설계, 연결 관리, 비동기 처리 등 다양한 측면을 고려해야 합니다. 모니터링 도구를 통해 서비스의 성능과 건강 상태를 지속적으로 관찰하는 것도 중요합니다.

 마지막으로, gRPC 서비스를 설계하고 구현할 때는 버전 관리, 에러 처리, 보안, 문서화 등 다양한 측면을 고려해야 합니다. 이러한 Best Practices를 따르면 안정적이고 확장 가능한 마이크로서비스 아키텍처를 구축할 수 있습니다.

 NestJS와 gRPC의 조합은 강력하고 효율적인 마이크로서비스 통신을 가능하게 합니다. 타입 안전성, 고성능, 양방향 스트리밍 등의 이점을 활용하여 복잡한 분산 시스템을 효과적으로 구축할 수 있습니다. 지속적인 모니터링과 최적화를 통해 시스템의 성능과 안정성을 유지하고 개선해 나가는 것이 중요합니다.