icon

스키마 정의와 리졸버 구현


 GraphQL 스키마는 API의 타입 시스템을 정의하며, 리졸버는 이 스키마에 따라 데이터를 실제로 가져오거나 수정하는 함수입니다.

 NestJS에서는 이들을 타입스크립트 클래스와 데코레이터를 사용하여 우아하게 구현할 수 있습니다.

GraphQL 스키마 기본 구성 요소

  1. 타입
@ObjectType()
class User {
  @Field(type => ID)
  id: string;
 
  @Field()
  name: string;
 
  @Field(type => [Post])
  posts: Post[];
}
  1. 쿼리
@Resolver(of => User)
class UserResolver {
  @Query(returns => User)
  async user(@Args('id') id: string) {
    return this.userService.findById(id);
  }
}
  1. 뮤테이션
@Resolver(of => User)
class UserResolver {
  @Mutation(returns => User)
  async createUser(@Args('input') input: CreateUserInput) {
    return this.userService.create(input);
  }
}
  1. 서브스크립션
@Resolver(of => Post)
class PostResolver {
  @Subscription(returns => Post)
  postAdded() {
    return this.pubSub.asyncIterator('postAdded');
  }
}

@nestjs/graphql 주요 데코레이터

  1. @ObjectType() : GraphQL 객체 타입 정의
  2. @InputType() : 입력 타입 정의
  3. @Field() : 필드 정의
  4. @Args() : 인자 정의
  5. @Resolver() : 리졸버 클래스 정의
  6. @Query(), @Mutation(), @Subscription() : 각각의 작업 정의

 예시

@InputType()
class CreateUserInput {
  @Field()
  name: string;
 
  @Field(type => [String])
  roles: string[];
}
 
@Resolver(of => User)
class UserResolver {
  @Query(returns => [User])
  async users(@Args('roles', { type: () => [String] }) roles: string[]) {
    return this.userService.findByRoles(roles);
  }
}

리졸버 구현

  1. 쿼리 리졸버
@Resolver(of => User)
class UserResolver {
  constructor(private userService: UserService) {}
 
  @Query(returns => User)
  async user(@Args('id') id: string) {
    return this.userService.findById(id);
  }
}
  1. 뮤테이션 리졸버
@Resolver(of => User)
class UserResolver {
  constructor(private userService: UserService) {}
 
  @Mutation(returns => User)
  async updateUser(
    @Args('id') id: string,
    @Args('input') input: UpdateUserInput
  ) {
    return this.userService.update(id, input);
  }
}
  1. 필드 리졸버
@Resolver(of => User)
class UserResolver {
  constructor(private postService: PostService) {}
 
  @ResolveField()
  async posts(@Parent() user: User) {
    return this.postService.findByUserId(user.id);
  }
}

스키마와 TypeScript 타입 자동 생성

 GraphQLModule 설정

GraphQLModule.forRoot<ApolloDriverConfig>({
  driver: ApolloDriver,
  autoSchemaFile: 'schema.gql',
  definitions: {
    path: join(process.cwd(), 'src/graphql.ts'),
  },
}),

 이 설정으로 schema.gql 파일과 src/graphql.ts 파일이 자동 생성됩니다.

복잡한 타입 관계 모델링

  1. 일대다 관계
@ObjectType()
class User {
  @Field(type => ID)
  id: string;
 
  @Field(type => [Post])
  posts: Post[];
}
 
@ObjectType()
class Post {
  @Field(type => ID)
  id: string;
 
  @Field(type => User)
  author: User;
}
  1. 다대다 관계
@ObjectType()
class Product {
  @Field(type => ID)
  id: string;
 
  @Field(type => [Category])
  categories: Category[];
}
 
@ObjectType()
class Category {
  @Field(type => ID)
  id: string;
 
  @Field(type => [Product])
  products: Product[];
}

 리졸버 구현

@Resolver(of => User)
class UserResolver {
  @ResolveField()
  async posts(@Parent() user: User) {
    return this.postService.findByUserId(user.id);
  }
}

인터페이스와 유니온 타입

  1. 인터페이스
@InterfaceType()
abstract class Node {
  @Field(type => ID)
  id: string;
}
 
@ObjectType({ implements: Node })
class User implements Node {
  @Field(type => ID)
  id: string;
 
  @Field()
  name: string;
}
  1. 유니온 타입
@ObjectType()
class Book {
  @Field()
  title: string;
}
 
@ObjectType()
class Movie {
  @Field()
  director: string;
}
 
@createUnionType({
  name: 'SearchResult',
  types: () => [Book, Movie],
})
export class SearchResultUnion {}

GraphQL 지시어(Directive) 정의 및 사용

  1. 지시어 정의
@Directive('@upper')
export class UpperDirective {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    field.resolve = async function(...args) {
      const result = await resolve.apply(this, args);
      if (typeof result === 'string') {
        return result.toUpperCase();
      }
      return result;
    };
  }
}
``
2. 지시어 사용
```typescript
@ObjectType()
class User {
  @Field()
  @Directive('@upper')
  name: string;
}

Best Practices 및 성능 최적화 전략

  1. 데이터 로더 사용
import DataLoader from 'dataloader';
 
@Injectable()
export class UserDataLoader {
  constructor(private readonly userService: UserService) {}
 
  public readonly batchUsers = new DataLoader(async (userIds: string[]) => {
    const users = await this.userService.findByIds(userIds);
    return userIds.map(id => users.find(user => user.id === id));
  });
}
  1. 필드 수준 인증
@Resolver(of => User)
class UserResolver {
  @ResolveField()
  @UseGuards(RolesGuard)
  async secretInfo(@Parent() user: User) {
    return this.userService.getSecretInfo(user.id);
  }
}
  1. 스키마 분할
@Module({
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: 'schema.gql',
      include: [UsersModule, PostsModule],
    }),
    UsersModule,
    PostsModule,
  ],
})
export class AppModule {}
  1. 쿼리 복잡도 제한
GraphQLModule.forRoot({
  validationRules: [
    depthLimitRule(5),
    complexityLimiter({
      maxComplexity: 20,
      estimators: [
        fieldConfigEstimator(),
        simpleEstimator({ defaultComplexity: 1 }),
      ],
    }),
  ],
}),
  1. 응답 캐싱
@Resolver(of => User)
class UserResolver {
  @Query(returns => User)
  @CacheKey('user')
  @CacheTTL(30)
  async user(@Args('id') id: string) {
    return this.userService.findById(id);
  }
}

 NestJS에서는 복잡한 타입 관계를 모델링할 때는 일대다, 다대다 관계를 필드 데코레이터와 리졸버 함수를 통해 표현할 수 있습니다.

 이는 데이터베이스의 관계를 GraphQL 스키마에 효과적으로 매핑할 수 있게 해줍니다.

 인터페이스와 유니온 타입을 사용하면 더욱 유연한 스키마 설계가 가능합니다.

 인터페이스를 통해 공통 필드를 정의하고, 유니온 타입으로 여러 타입 중 하나를 반환하는 필드를 만들 수 있습니다.

 추가적으로 GraphQL 지시어를 사용하면 스키마에 메타데이터를 추가하고 런타임 동작을 수정할 수 있습니다.

 이는 권한 체크, 데이터 변환 등 다양한 용도로 활용될 수 있습니다.