gRPC

gRPC

gRPC๋Š” ๊ณ ์„ฑ๋Šฅ ์˜คํ”ˆ ์†Œ์Šค ๋ฒ”์šฉ RPC ํ”„๋ ˆ์ž„ ์›Œํฌ์ž…๋‹ˆ๋‹ค.

Installation

์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ํ•„์š”ํ•œ ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค.

$ npm i --save grpc @grpc/proto-loader

Transporter

gRPC ์ „์†ก๊ธฐ๋กœ ์ „ํ™˜ํ•˜๋ ค๋ฉด createMicroservice()๋ฉ”์†Œ๋“œ์— ์ „๋‹ฌ๋œ ์˜ต์…˜ ๊ฐ์ฒด๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@@filename(main)
const app = await NestFactory.createMicroservice(ApplicationModule, {
  transport: Transport.GRPC,
  options: {
    package: 'hero',
    protoPath: join(__dirname, 'hero/hero.proto'),
  },
});

info ํžŒํŠธ join()ํ•จ์ˆ˜๋Š” path ํŒจํ‚ค์ง€์—์„œ ๊ฐ€์ ธ์˜ค๊ณ  Transport ์—ด๊ฑฐ๋Š” @nestjs/microservices์—์„œ ์˜จ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Options

์ „์†ก๊ธฐ ๋™์ž‘์„ ๊ฒฐ์ •ํ•˜๋Š” ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์˜ต์…˜์ด ๋งŽ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

url

Connection url

protoLoader

NPM ํŒจํ‚ค์ง€ ์ด๋ฆ„ (๋‹ค๋ฅธ ํ”„๋กœํ†  ๋กœ๋”๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋Š” ๊ฒฝ์šฐ)

protoPath

.proto ํŒŒ์ผ์˜ ์ ˆ๋Œ€ (๋˜๋Š” ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ์— ๋Œ€ํ•œ) ๊ฒฝ๋กœ

loader

@grpc/proto-loader์˜ต์…˜. ์—ฌ๊ธฐ.์— ์ž˜ ์„ค๋ช…๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค

package

Protobuf package name

credentials

Server credentials (read more)

Overview

์ผ๋ฐ˜์ ์œผ๋กœ package ์†์„ฑ์€ protobuf ํŒจํ‚ค์ง€ ์ด๋ฆ„์„ ์„ค์ •ํ•˜๊ณ  protoPath๋Š”.proto ์ •์˜ ํŒŒ์ผ์˜ ๊ฒฝ๋กœ์ž…๋‹ˆ๋‹ค. 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;
}

์œ„์˜ ์˜ˆ์—์„œ, ์šฐ๋ฆฌ๋Š” HeroById๋ฅผ ์ž…๋ ฅ์œผ๋กœ ์˜ˆ์ƒํ•˜๊ณ  Hero ๋ฉ”์‹œ์ง€๋ฅผ ๋ฆฌํ„ดํ•˜๋Š” FindOne() gRPC ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋…ธ์ถœํ•˜๋Š” HeroService๋ฅผ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ํ”„๋กœํ†  ํƒ€์ž… ์ •์˜๋ฅผ ์ถฉ์กฑ์‹œํ‚ค๋Š” ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ •์˜ํ•˜๋ ค๋ฉด @GrpcMethod() ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. ์ด์ „์— ์•Œ๋ ค์ง„ @MessagePattern()์€ ๋” ์ด์ƒ ์œ ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

@@filename(hero.controller)
@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);
}

info ํžŒํŠธ @GrpcMethod()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋Š” @nestjs/microservices ํŒจํ‚ค์ง€์—์„œ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

HeroService๋Š” ์„œ๋น„์Šค ์ด๋ฆ„์ธ ๋ฐ˜๋ฉด, FindOne์€ FindOne() gRPC ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๊ฐ€๋ฆฌํ‚ต๋‹ˆ๋‹ค. ํ•ด๋‹นํ•˜๋Š” findOne() ๋ฉ”์†Œ๋“œ๋Š” ๋‘๊ฐ€์ง€ ์ธ์ˆ˜, ์ฆ‰ ํ˜ธ์ถœ์ž๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋œ data์™€ gRPC ์š”์ฒญ์˜ ๋ฉ”ํƒ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” metadata๋ฅผ ์ทจํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ, FindOne์€ ์‹ค์ œ๋กœ ์—ฌ๊ธฐ์„œ ์ค‘๋ณต๋ฉ๋‹ˆ๋‹ค. @GrpcMethod()์— ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜์ง€ ์•Š์œผ๋ฉด Nest๋Š” ์ž๋™์œผ๋กœ ์ฒซ ๋ฒˆ์งธ ๋ฌธ์ž (์˜ˆ :findOne->FindOne)์™€ ํ•จ๊ป˜ ๋ฉ”์†Œ๋“œ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

@@filename(hero.controller)
@Controller()
export class HeroService {
  @GrpcMethod()
  findOne(data: HeroById, metadata: any): Hero {
    const items = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Doe' },
    ];
    return items.find(({ id }) => id === data.id);
  }
}

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ธ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜์ง€ ๋ชปํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ Nest๋Š” ํด๋ž˜์Šค ์ด๋ฆ„์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

@@filename(hero.controller)
@Controller()
export class HeroService {
  @GrpcMethod()
  findOne(data: HeroById, metadata: any): Hero {
    const items = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Doe' },
    ];
    return items.find(({ id }) => id === data.id);
  }
}

Client

ํด๋ผ์ด์–ธํŠธ ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค๋ ค๋ฉด @Client()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

@Client({
  transport: Transport.GRPC,
  options: {
    package: 'hero',
    protoPath: join(__dirname, 'hero/hero.proto'),
  },
})
client: ClientGrpc;

์ด์ „ ์˜ˆ์™€ ๋น„๊ตํ•˜์—ฌ ์•ฝ๊ฐ„์˜ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ClientProxy ํด๋ž˜์Šค ๋Œ€์‹ , getService() ๋ฉ”์†Œ๋“œ๋ฅผ ์ œ๊ณตํ•˜๋Š” ClientGrpc๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. getService()์ œ๋„ค๋ฆญ ๋ฉ”์„œ๋“œ๋Š” ์„œ๋น„์Šค ์ด๋ฆ„์„ ์ธ์ˆ˜๋กœ ์‚ฌ์šฉํ•˜๊ณ  ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

@@filename(hero.controller)
onModuleInit() {
  this.heroService = this.client.getService<HeroService>('HeroService');
}

heroService ๊ฐ์ฒด๋Š” .proto ํŒŒ์ผ ๋‚ด์— ์ •์˜๋œ ๊ฒƒ๊ณผ ๋™์ผํ•œ ๋ฉ”์†Œ๋“œ ์„ธํŠธ๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๊ทœ์น™์€ ์†Œ๋ฌธ์ž (์ž์—ฐ ๊ทœ์น™์„ ๋”ฐ๋ฅด๊ธฐ ์œ„ํ•ด)์ž…๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ gRPC HeroService ์ •์˜์—๋Š” FindOne()ํ•จ์ˆ˜๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. heroService ์ธ์Šคํ„ด์Šค๋Š” findOne()๋ฉ”์†Œ๋“œ๋ฅผ ์ œ๊ณตํ•œ๋‹ค๋Š” ์˜๋ฏธ์ž…๋‹ˆ๋‹ค.

interface HeroService {
  findOne(data: { id: number }): Observable<any>;
}

๋ชจ๋“  ์„œ๋น„์Šค ๋ฉ”์†Œ๋“œ๋Š” Observable์„ ๋ฆฌํ„ดํ•ฉ๋‹ˆ๋‹ค. Nest๋Š” RxJS ์ŠคํŠธ๋ฆผ์„ ์ง€์›ํ•˜๊ณ  ์ž˜ ์ž‘๋™ํ•˜๋ฏ€๋กœ HTTP ์ฒ˜๋ฆฌ๊ธฐ ๋‚ด์—์„œ๋„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@@filename(hero.controller)
@Get()
call(): Observable<any> {
  return this.heroService.findOne({ id: 1 });
}

์ „์ฒด ์ž‘์—… ์˜ˆ๋Š” ์—ฌ๊ธฐ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

gRPC Streaming

GRPC ์ž์ฒด๋Š” ์ŠคํŠธ๋ฆผ์œผ๋กœ ์•Œ๋ ค์ง„ ๋กฑํ…€ ๋ผ์ด๋ธŒ ์—ฐ๊ฒฐ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์ŠคํŠธ๋ฆผ์€ ์ฑ„ํŒ…, ๊ด€์ฐฐ ๋˜๋Š” ์ฒญํฌ ๋ฐ์ดํ„ฐ ์ „์†ก๊ณผ ๊ฐ™์€ ์„œ๋น„์Šค ์‚ฌ๋ก€์— ๋งค์šฐ ์œ ์šฉํ•œ ๋„๊ตฌ๊ฐ€ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ณต์‹ ๋ฌธ์„œ (์—ฌ๊ธฐ)์—์„œ ์ž์„ธํ•œ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Nest๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์œผ๋กœ GRPC ์ŠคํŠธ๋ฆผ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

  • RxJS Subject + Observable ํ•ธ๋“ค๋Ÿฌ: Controller ๋ฉ”์†Œ๋“œ ๋‚ด์—์„œ ๋ฐ”๋กœ ์‘๋‹ต์„ ์“ฐ๊ฑฐ๋‚˜ Subject/Observable ์†Œ๋น„์ž์—๊ฒŒ ์ „๋‹ฌํ•˜๋Š” ๋ฐ ์œ ์šฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

  • ์ˆœ์ˆ˜ GRPC ํ˜ธ์ถœ ์ŠคํŠธ๋ฆผ ํ•ธ๋“ค๋Ÿฌ: ๋…ธ๋“œ ํ‘œ์ค€ Duplex ์ŠคํŠธ๋ฆผ ํ•ธ๋“ค๋Ÿฌ์— ๋Œ€ํ•œ ๋‚˜๋จธ์ง€ ๋””์ŠคํŒจ์น˜๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ผ๋ถ€ ์‹คํ–‰๊ธฐ์— ์ „๋‹ฌํ•˜๋Š” ๋ฐ ์œ ์šฉ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Subject strategy

@GrpcStreamMethod()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋Š” ํ•จ์ˆ˜ ๋งค๊ฐœ ๋ณ€์ˆ˜๋ฅผ RxJS Observable๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

// Set decorator with selecting a Service definition from protobuf package
// the string is matching to: package proto_example.orders.OrdersService
@GrpcStreamMethod('orders.OrderService')
handleStream(messages: Observable<any>): Observable<any> {
  const subject = new Subject();
  messages.subscribe(message => {
    console.log(message);
    subject.next({
      shipmentType: {
        carrier: 'test-carrier',
      },
    });
  });
  return subject.asObservable();
}

@GrpcStreamMethod()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์™€์˜ ์ „์ด์ค‘ ์ƒํ˜ธ ์ž‘์šฉ์„ ์ง€์›ํ•˜๋ ค๋ฉด ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์†Œ๋“œ์—์„œ RxJS 'Observable'์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Pure GRPC call stream handler

@GrpcStreamCall()๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋Š” grpc.ServerDuplexStream๊ณผ ๊ฐ™์€ ํ•จ์ˆ˜ ๋งค๊ฐœ ๋ณ€์ˆ˜๋ฅผ ์ œ๊ณตํ•˜๋ฉฐ .on('data',callback), .write(message)๋˜๋Š” .cancel()๊ณผ ๊ฐ™์€ ํ‘œ์ค€ ๋ฉ”์†Œ๋“œ๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ „์ฒด ์„ค๋ช…์„œ๋Š” ์—ฌ๊ธฐ์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// Set decorator with selecting a Service definition from protobuf package
// the string is matching to: package proto_example.orders.OrdersService
@GrpcStreamCall('orders.OrderService')
handleStream(stream: any) {
  stream.on('data', (msg: any) => {
    console.log(msg);
    // Answer here or anywhere else using stream reference
    stream.write({
      shipmentType: {
        carrier: 'test-carrier',
      },
    });
  });
}

์ด ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์—๋Š” ํŠน์ • ๋ฆฌํ„ด ๋งค๊ฐœ ๋ณ€์ˆ˜๋ฅผ ์ œ๊ณตํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ŠคํŠธ๋ฆผ์€ ๋‹ค๋ฅธ ํ‘œ์ค€ ์ŠคํŠธ๋ฆผ ์œ ํ˜•๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌ๋  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค.

Last updated

Was this helpful?