nestjs with graphql - ch2 (Federation)

Jae MinΒ·2023λ…„ 11μ›” 20일
0

nestjs with graphql

λͺ©λ‘ 보기
2/6
post-custom-banner

πŸ”₯ MSA ꡬ쑰λ₯Ό graphql 둜 λ§Œλ“€μ–΄λ³΄μž πŸ”₯

그전에 μƒκ°ν•΄λ³΄μž...
grapqhl 은 μ—”λ“œν¬μΈνŠΈκ°€ ν•˜λ‚˜μΈλ°, MSA λ₯Ό μ–΄λ–»κ²Œ κ΅¬ν˜„ν•΄μ•Ό ν•˜μ§€...? λ­”κ°€ msa 둜 ν•  수 μžˆμ„ 것 κ°™μœΌλ©΄μ„œλ„... 이게 restapi 처럼 msa λ₯Ό ꡬ성 κ°€λŠ₯ν• κΉŒ...? μ—¬λŸ¬κ°€μ§€ 생각이 λ“ λ‹€. 음...각 app λ³„λ‘œ tcp μ„œλ²„λ₯Ό μ—΄μ–΄μ•Ό ν•˜λ‚˜? tcp 톡신이 ν•„μš”ν•œκ°€? μ–΄μ°¨ν”Ό μ—”λ“œν¬μΈνŠΈ ν•˜λ‚˜μΈλ°..?
ν•œλ²ˆ ν•΄λ³΄μž.

  1. 각 도메인에 λ”°λ₯Έ app 을 λ§Œλ“ λ‹€
    nest g app gateway, nest g app account, nest g app chat

  2. μƒμ„±ν•œ 앱은 rest 기반으둜 λ§Œλ“€κΈ° λ•Œλ¬Έμ— graphql 을 μœ„ν•œ resolver 생성이 ν•„μš”ν•˜λ‹€.
    nest g resource

    gateway λ₯Ό μ œμ™Έν•œ λ‹€λ₯Έ app 듀을 각자 μ„ νƒν•΄μ„œ resolver λ₯Ό μƒμ„±ν•˜λ©΄ κΈ°λ³Έ ꡬ쑰(resolver, service, dto, entity...) 및 둜직(CRUD)을 λ§Œλ“€μ–΄μ€€λ‹€.

  3. 이제 gateway μ—μ„œ 각 앱듀을 ν•˜λ‚˜λ‘œ 연결을 μ‹œμΌœμ€˜μ•Ό ν•œλ‹€.

    μ—¬κΈ°μ„œ μ€‘μš”ν•œ κ°œλ…μ΄ μžˆλ‹€.


Federation

Federation(μ§μ—­ν•˜λ©΄ μ—°λ°©)의 μ˜λ―ΈλŠ” graphql μ„œλ²„λ₯Ό 각 microservice 둜 μ£Όμž…μ„ ν•΄μ€€λ‹€λΌλŠ” μ˜λ―Έμ΄λ‹€.

μš”μ²­ν•œ 쿼리λ₯Ό ν•˜λ‚˜μ˜ λ¦¬μ‘Έλ²„μ—μ„œ λͺ¨λ‘ ν•΄κ²°ν•˜λŠ” 것이 μ•„λ‹Œ, 각 subgraph μ—μ„œ μŠ€ν‚€λ§ˆλ₯Ό μ²˜λ¦¬ν•΄μ„œ 처리된 schema λ₯Ό superGraph μ—μ„œ μ·¨ν•©ν•΄μ„œ ν”„λ‘ νŠΈλ‘œ λ˜μ Έμ€€λ‹€.

Federation 을 κ΅¬ν˜„ν•˜κΈ° μœ„ν•΄μ„œλŠ” ν•˜λ‚˜μ˜ gateway 와 ν•œκ°œ μ΄μƒμ˜ microservice(federated microservice) κ°€ ν•„μš”ν•˜λ‹€.
μ΄λ•Œ gateway λ₯Ό supergraph 라고 ν•˜κ³ , 각각의 service 듀을 subgraph 라고 μ΄ν•΄ν•˜λ©΄ νŽΈν•˜λ‹€.

federated microservice(subgraph) λŠ” 각각의 schema λ₯Ό 가지고 있고, gateway(supergraph) λŠ” κ·Έ schema 듀을 ν•œ 곳으둜 λͺ¨μ„ 수 μžˆλ‹€.

πŸ”₯ λ¬Όλ‘  ν•˜λ‚˜μ˜ resolver μ—μ„œ λͺ¨λ“  λ‘œμ§μ„ μ²˜λ¦¬ν•  μˆ˜λŠ” μžˆμ§€λ§Œ, κ·Έλ ‡κ²Œ ν•˜λ©΄ μ—¬λŸ¬ ν…Œμ΄λΈ”μ— join 을 κ±Έμ–΄μ•Ό ν•˜κ³  κ·Έλ ‡κ²Œ 되면 graphql 을 μ‚¬μš©ν•˜κ³ λ„ overFetching 을 ν•  수 밖에 μ—†λ‹€.
κ·Έλ ‡κΈ° λ•Œλ¬Έμ— μ›ν•˜λŠ” κ°’λ§Œ μ‘°νšŒν•˜κΈ° μœ„ν•΄ 각 entityλ₯Ό λ‹΄λ‹Ήν•˜λŠ” resolver μ—μ„œ 각각 μ²˜λ¦¬ν•˜κ³  superGraph μ—μ„œ ν•΄λ‹Ή entity 듀을 λͺ¨μ•„μ„œ λ°˜ν™˜ν•˜λŠ” 것이닀.

그러면 각각의 subgraph λ₯Ό μ–΄λ–»κ²Œ ν•œλ²ˆμ— λͺ¨μœΌλŠ”지 μ•„λž˜ μ½”λ“œλ₯Ό 톡해 ν™•μΈν•΄λ³΄μž.

// gateway.module.ts
import { Module } from '@nestjs/common';
import { GatewayService } from './gateway.service';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloGatewayDriver, ApolloGatewayDriverConfig } from '@nestjs/apollo';
import { IntrospectAndCompose } from '@apollo/gateway';
@Module({
  imports: [
    GraphQLModule.forRoot<ApolloGatewayDriverConfig>({
      driver: ApolloGatewayDriver,
      gateway: {
        supergraphSdl: new IntrospectAndCompose({
          // 각각의 subGraph λ₯Ό ν•˜λ‚˜λ‘œ μ—°κ²° μ‹œμΌœμ€€λ‹€.
          subgraphs: [
            {
              name: 'account',
              url: 'http://localhost:3001/graphql',
            },
            {
              name: 'talk',
              url: 'http://localhost:3002/graphql',
            },
          ],
        }),
      },
    }),
  ],
  providers: [GatewayService],
})
export class GatewayModule {}

μ½”λ“œλŠ” κ°„λ‹¨ν•˜λ‹€. ApolloGatewayDriver, IntrospectAndCompose class λ₯Ό μ΄μš©ν•΄μ„œ 각각의 subgrapqh λ₯Ό 톡합해쀄 수 μžˆλ‹€.

각각의 federated microservice 의 schema 쀑 @ObjectType κ°„μ˜ relation 을 ν™œμš©ν•˜λ©΄ ν•˜λ‚˜μ˜ query μ—μ„œ 데이터λ₯Ό κ°€μ Έμ˜€λŠ” 것이 κ°€λŠ₯ν•˜λ‹€. 그러기 μœ„ν•΄μ„œλŠ” directive μ§€μ‹œμ–΄ κ°€ ν•„μš”ν•˜λ‹€.

Directive

graphql μ—μ„œλŠ” λ™μΌν•œ μ΄λ¦„μ˜ @ObjectType 이 쑴재 ν•˜λ©΄ μ•ˆλœλ‹€

ν•˜μ§€λ§Œ μ—¬λŸ¬ μ•±μ—μ„œ λ™μΌν•œ λͺ¨λΈμ„ μ“°κ³  싢을 수 μžˆλ‹€. 예λ₯Ό λ“€μ–΄, 계정 νŽ˜μ΄μ§€μ—μ„œ 고객의 정보λ₯Ό 보여쀄 μˆ˜λ„ 있고, μ±„νŒ…λ°©μ—μ„œ 고객 정보λ₯Ό 보여쀄 μˆ˜λ„ μžˆλ‹€. μ±„νŒ…λ°©μ—μ„œλŠ” μ±„νŒ… 정보와 고객의 정보λ₯Ό 같이 λ³΄μ—¬μ€˜μ•Ό ν•˜λŠ” κ²½μš°κ°€ 생긴닀.
MSA ꡬ쑰에 λ”°λΌμ„œ 각 μ•±μ—μ„œ 각 λͺ¨λΈμ„ κ΅μ°¨ν•΄μ„œ μ’…μ†μ‹œν‚€λŠ”κ±΄ μ˜³μ§€ μ•Šμ€ 방법이기 λ•Œλ¬Έμ—, 각 μ•±μ—μ„œ 각 λͺ¨λΈμ„ λ‘˜λ‹€ 선언을 ν•΄μ€€λ‹€.

// chat > chat.model.ts
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
@Directive('@key(fields: "id")')
export class AccountModel {
  @Field(() => ID)
  id: string;
}



// account > account.model.ts
import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
@ObjectType()
@Directive('@key(fields: "id")')
// 동일 μ΄λ¦„μ˜ objectType 이 있으면 μ•ˆλœλ‹€
// -> λ™μΌν•œ objectType 이 μžˆλ‹€λ©΄ λ°˜λ“œμ‹œ @Directive('@key(fields: "id")') 둜 obejct 의 pk λ₯Ό μ„€μ •ν•΄μ€˜μ•Ό 함
export class AccountModel {
  @Field(() => ID)
  id: string;

  @Field(() => String)
  name: string;
}

μ΄λ ‡κ²Œ 될 경우 schema 에 같은 μ΄λ¦„μ˜ λͺ¨λΈμ΄ 생겨버리기 떄문에 graphql 철학에 μœ„λ°°λœλ‹€. μ΄λ•Œ, @Directive('@key(fields: "id")') 둜 문제λ₯Ό ν’€μ–΄μ€€λ‹€.
μ € λ°μ½”λ ˆμ΄ν„°λŠ” 쀑볡을 ν—ˆμš©ν•΄μ€€λ‹€λŠ” 의미λ₯Ό ν¬ν•¨ν•˜κ³  μžˆλ‹€. λ˜ν•œ, μ•ˆμ— μžˆλŠ” @Key λŠ” λͺ¨λΈμ—λ„ ν…Œμ΄λΈ”μ²˜λŸΌ PK λ₯Ό μ§€μ •ν•΄μ£ΌλŠ” 것이닀.
μ € PK둜 μ •ν•œ id κ°€ 같은 λͺ¨λΈμ΄ 있으면 λ‘κ°œλŠ” κ°™μ€κ²ƒμœΌλ‘œ νŒλ‹¨ν•΄μ„œ ν•˜λ‚˜λ‘œ λ¬Άμ–΄μ€€λ‹€.

κ²°κ΅­

@ObjectType()
export class AccountModel {
  @Field(() => ID)
  id: string;

  @Field(() => String)
  name: string;
}

// schema.gql
type AccountModel {
  id: ID!
  name: String!
}

λ‹€μŒκ³Ό 같은 ν•˜λ‚˜μ˜ λͺ¨λΈμ΄ λ˜λŠ” 것이닀.

ResolveField

μ²˜μŒμ— 이거 떄문에 μ• λ₯Ό μ©μ—ˆλ‹€.
아무리 κ΅¬κΈ€λ§ν•΄μ„œ λ‚˜μ˜€λŠ” μ„€λͺ…을 봐도 이해가 μ•ˆλ˜κ³ , λŒ€μ‹ μ— 이거λ₯Ό μ“°λ©΄ dataLoader λ­μ‹œκΈ° λ­μ‹œκΈ°... κ·Έλž˜μ„œ 유튜브 μ˜μƒ 및 지인듀을 ν†΅ν•΄μ„œ μ•Œμ•„λƒˆλ‹€.

ResolveField... 일단 Field λ‹€. resolve... ν•΄κ²°ν•˜λ‹€?

μ˜μ—­ν•˜λ©΄ ν•„λ“œλ₯Ό ν•˜λ‚˜ μ—΄μ–΄μ£ΌλŠ” 것이닀.
즉 ResolveField κ°€ μ†ν•΄μžˆλŠ” λ¦¬μ‘Έλ²„μ—μ„œ λ°˜ν™˜ν•˜λŠ” λͺ¨λΈμ— ν•„λ“œλ₯Ό ν•˜λ‚˜ 더 μΆ”κ°€ν•˜λŠ” ν–‰μœ„λ‹€.

// chat.resolver.ts
import {
  Resolver,
  Query,
  Args,
  Int,
  ResolveField,
  Parent,
} from '@nestjs/graphql';
import { ChatService } from './chat.service';
import { AccountModel, ChatModel } from './entities';

@Resolver(() => ChatModel)
// 이 resolver λŠ” chatModel 을 뽑아내기 μœ„ν•œ 리쑸버이닀
export class ChatResolver {
  constructor(private readonly chatService: ChatService) {}

  @Query(() => ChatModel)
  async getChatInfo(
    @Args('roomId', { type: () => Int }) roomId: number,
  ): Promise<ChatModel> {
    return {
      chatId: roomId,
    };
  }

  @ResolveField(() => AccountModel)
  user(@Parent() chat: ChatModel) {
    return { __typename: 'UserEntity', id: chat.chatId };
  }
}



// chat.model.ts
@ObjectType()
export class ChatModel {
  @Field(() => Number)
  chatId: number;
}

μœ„ λ¦¬μ‘Έλ²„λŠ” ChatModel을 λ°˜ν™˜ν•˜λŠ” 리쑸버이닀. @ResolveField λ₯Ό ν†΅ν•΄μ„œ ChatModel 에 user λΌλŠ” ν•„λ“œλ₯Ό μΆ”κ°€μ μœΌλ‘œ λ°˜ν™˜ν•˜κ² λ‹€λΌλŠ” λœ»μ΄λ‹€.

근데 User λŠ” AccountModel 이닀. AccountModel 은 Chat 앱이 μ•„λ‹ˆλΌ, Account 앱에 μ’…μ†λ˜μ–΄μžˆλ‹€. μ–΄λ–»κ²Œ κ°€μ Έμ˜€μ§€?
μ΄λ•Œ @Directive κ°€ 쓰인닀!!!

@Query() getChatInfo μΏΌλ¦¬λŠ” ChatModel 을 λ°˜ν™˜ν•œλ‹€.
그러면 @Parent() κ°€ ChatModel 인 user @ResolveField() κ°€ 이λ₯Ό μΊμΉ˜ν•œλ‹€. user @ResolveField() κ°€ { __typename: 'UserEntity', id: chat.chatId } λ₯Ό λ°˜ν™˜ν•˜λŠ”λ°, 이λ₯Ό λˆ„κ΅¬ν•œν…Œ λ°˜ν™˜ν•˜λŠ”κ°€..?

βœ… μ•žμ„œ λ§ν–ˆλ“―μ΄ graphql μ—μ„œλŠ” λ™μΌν•œ ObjectType이 μžˆμ„ 수 μ—†λ‹€.
chat app 에 μ’…μ†λ˜μ–΄ μžˆλŠ” Account Model 와
Account app 에 μ’…μ†λ˜μ–΄ μžˆλŠ” Account Model 이
@Directive key 둜 μ—°κ²°λ˜μ–΄ 있기 λ•Œλ¬Έμ—,
AccountModel 을 μ’…μ†ν•˜κ³  μžˆλŠ” Account app μ—μ„œ 이λ₯Ό μˆ˜μ‹ ν•œλ‹€.

λ§Œμ•½μ— λ˜λ‹€λ₯Έ app μ—μ„œλ„ Account Model 을 μ‚¬μš©ν•˜κ³  μžˆλ‹€λ©΄? μ–΄λŠμͺ½μ—μ„œ μˆ˜μ‹ ν• κΉŒ?
이런 고민은 잘λͺ»λλ‹€.
μ• μ΄ˆμ— Account Model 은 Account app μ—μ„œ λ‹€λ£¨λŠ”κ²ƒμ΄ μ˜³μ€ 방법이닀. (κ΄€μ‹¬μ‚¬μ˜ 뢄리가 μ μš©λœλ‹€)
κ·Έλ ‡κΈ° λ•Œλ¬Έμ— μ• μ΄ˆμ— 또 λ‹€λ₯Έ app μ—μ„œλŠ” @ResolveReference() 이 μ•„λ‹ˆλΌ, @ResolveField() user λ₯Ό μ‚¬μš©ν•˜λŠ” 것이 λ§žλ‹€.
즉, Account Model 은 Account app μ—μ„œλ§Œ λ‘œμ§μ„ ν’€μ–΄κ°€μž!

// account.resolver.ts
/**
   *
   * @description chat resolver μ—μ„œ μ²˜λ¦¬ν•˜μ§€ λͺ»ν•˜λŠ” 쿼리λ₯Ό μ—¬κΈ°μ„œ μ²˜λ¦¬ν•¨ (find account)
   * resolverField λ₯Ό μ—¬κΈ°μ„œ μˆ˜μ‹ ν•΄μ„œ μ²˜λ¦¬ν•œλ‹€.
   */
  @ResolveReference()
  resolveReference(reference: {
    __typename: string;
    id: string;
  }): AccountModel {
    return {
      id: reference.id,
      name: `${reference.id} + name`,
    };
  }


μ΄λ ‡κ²Œ ResolveField -> ResolveReference -> ResolveField -> ResolveReference -> ... 계속 ν•˜λ©΄ κ°€μž₯ λ§ˆμ§€λ§‰μ— μˆ˜μ‹ ν•˜λŠ” 리쑸버λ₯Ό μˆ˜λ§Œκ°€μ§€ μš”μ²­μ„ μ²˜λ¦¬ν•΄μ•Ό ν•˜λŠ” κ²½μš°κ°€ 생긴닀.
이λ₯Ό N+1 문제 라고 ν•˜λŠ”λ°, λ‹€μŒμ— λ‹€λ€„λ³΄μž.


Ref

https://blog.doctor-cha.com/integrating-graphql-services-with-graphql-federation
https://www.apollographql.com/docs/federation/

https://unsplash.com/ko/%EC%82%AC%EC%A7%84/%ED%8C%8C%EB%9E%80%EC%83%89-%ED%8E%98%EC%9D%B8%ED%8A%B8%EA%B0%80-%EC%B9%A0%ED%95%B4%EC%A7%84-%EC%82%AC%EB%9E%8C-%EB%B0%9C-EIyAz8blaAk

profile
μžμœ λ‘œμ›Œμ§€κ³  μ‹Άλ‹€λ©΄ κΈ°λ‘ν•˜λΌ.
post-custom-banner

0개의 λŒ“κΈ€