icon

서브스크립션과 실시간 기능 구현


 GraphQL 서브스크립션은 실시간 데이터 업데이트를 클라이언트에 제공하는 강력한 기능입니다.

 NestJS에서는 이를 효과적으로 구현할 수 있는 도구와 패턴을 제공합니다.

GraphQL 서브스크립션 개념

 서브스크립션은 웹소켓을 통해 서버에서 클라이언트로 실시간 데이터를 푸시하는 메커니즘입니다.

 클라이언트가 특정 이벤트를 구독하면, 해당 이벤트 발생 시 서버가 자동으로 데이터를 전송합니다.

NestJS에서 서브스크립션 설정

  1. 의존성 설치
npm install @nestjs/graphql @nestjs/websockets graphql-subscriptions
  1. GraphQLModule 설정
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
 
@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: 'schema.gql',
      installSubscriptionHandlers: true,
    }),
  ],
})
export class AppModule {}

서브스크립션 리졸버 구현

 @Subscription 데코레이터를 사용한 리졸버 예시

import { Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
 
@Resolver(of => Post)
export class PostResolver {
  constructor(private pubSub: PubSub) {}
 
  @Subscription(returns => Post)
  postAdded() {
    return this.pubSub.asyncIterator('postAdded');
  }
}

PubSub 시스템 구현

  1. PubSub 프로바이더 생성
import { PubSub } from 'graphql-subscriptions';
 
@Module({
  providers: [
    {
      provide: 'PUB_SUB',
      useValue: new PubSub(),
    },
  ],
})
export class PubSubModule {}
  1. 이벤트 발행
@Resolver(of => Post)
export class PostResolver {
  constructor(@Inject('PUB_SUB') private pubSub: PubSub) {}
 
  @Mutation(returns => Post)
  async createPost(@Args('input') input: CreatePostInput) {
    const newPost = await this.postService.create(input);
    this.pubSub.publish('postAdded', { postAdded: newPost });
    return newPost;
  }
}

서버 측 이벤트 필터링

 서버에서 특정 조건에 맞는 이벤트만 클라이언트에 전송

@Subscription(returns => Post, {
  filter: (payload, variables) => 
    payload.postAdded.authorId === variables.authorId,
})
postAdded(@Args('authorId') authorId: number) {
  return this.pubSub.asyncIterator('postAdded');
}

클라이언트 측 서브스크립션 필터링

 클라이언트에서 필요한 데이터만 요청

subscription OnPostAdded($authorId: ID!) {
  postAdded(authorId: $authorId) {
    id
    title
    content
  }
}

대규모 애플리케이션에서의 확장성

 Redis PubSub을 사용한 확장 전략

  1. Redis PubSub 설치
npm install graphql-redis-subscriptions ioredis
  1. Redis PubSub 구현
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
 
const options = {
  host: 'localhost',
  port: 6379,
};
 
@Module({
  providers: [
    {
      provide: 'PUB_SUB',
      useValue: new RedisPubSub({
        publisher: new Redis(options),
        subscriber: new Redis(options),
      }),
    },
  ],
})
export class PubSubModule {}

인증과 권한 부여

 WebSocket 연결에 인증 적용

import { UseGuards } from '@nestjs/common';
import { WebSocketGuard } from './websocket.guard';
 
@Resolver(of => Post)
@UseGuards(WebSocketGuard)
export class PostResolver {
  @Subscription(returns => Post)
  postAdded() {
    return this.pubSub.asyncIterator('postAdded');
  }
}

 WebSocketGuard 구현

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
 
@Injectable()
export class WebSocketGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const ctx = GqlExecutionContext.create(context);
    const { connectionParams } = ctx.getContext();
    
    // 토큰 검증 로직
    return this.validateToken(connectionParams.token);
  }
 
  private validateToken(token: string): boolean {
    // 토큰 검증 구현
  }
}

복잡한 실시간 기능 구현

 채팅 시스템 예시

@Resolver(of => Message)
export class ChatResolver {
  constructor(
    private chatService: ChatService,
    @Inject('PUB_SUB') private pubSub: PubSub,
  ) {}
 
  @Mutation(returns => Message)
  async sendMessage(@Args('input') input: SendMessageInput) {
    const message = await this.chatService.createMessage(input);
    this.pubSub.publish('messageSent', { messageSent: message });
    return message;
  }
 
  @Subscription(returns => Message, {
    filter: (payload, variables) =>
      payload.messageSent.roomId === variables.roomId,
  })
  messageSent(@Args('roomId') roomId: string) {
    return this.pubSub.asyncIterator('messageSent');
  }
 
  @Query(returns => [Message])
  async getMessages(@Args('roomId') roomId: string) {
    return this.chatService.getMessagesByRoomId(roomId);
  }
}

Best Practices

  1. 효율적인 필터링 : 서버 측 필터링을 사용하여 불필요한 데이터 전송 최소화
  2. 연결 관리
@Subscription(returns => Post, {
  resolve: (payload) => payload.postAdded,
  onSubscribe: (variables) => {
    // 연결 설정 로직
  },
})
postAdded() {
  return this.pubSub.asyncIterator('postAdded');
}
  1. 에러 처리
@Catch()
export class SubscriptionExceptionFilter implements GqlExceptionFilter {
  catch(exception: Error) {
    return new Error('Subscription error: ' + exception.message);
  }
}
  1. 성능 모니터링 : 활성 구독 수, 메시지 처리량 등 모니터링
  2. 연결 제한 : DoS 공격 방지를 위한 연결 수 제한 구현
  3. 재연결 전략 : 클라이언트 측 재연결 로직 구현
  4. 테스트 : 단위 테스트 및 통합 테스트 작성
describe('ChatResolver', () => {
  it('should publish message on sendMessage', async () => {
    const pubSub = { publish: jest.fn() };
    const resolver = new ChatResolver(chatService, pubSub);
    
    await resolver.sendMessage({ content: 'Hello', roomId: '1' });
    
    expect(pubSub.publish).toHaveBeenCalledWith('messageSent', expect.any(Object));
  });
});

 GraphQL 서브스크립션을 구현하는 것은 실시간 기능을 애플리케이션에 통합하는 강력한 방법입니다.

 웹소켓을 통한 실시간 통신은 채팅 애플리케이션, 실시간 대시보드, 협업 도구 등 다양한 사용 사례에서 중요한 역할을 합니다.

 PubSub 시스템은 서브스크립션의 핵심 요소로, 이벤트 발행과 구독을 관리합니다. 소규모 애플리케이션에서는 메모리 내 PubSub이 충분할 수 있지만, 대규모 시스템에서는 Redis와 같은 외부 PubSub 솔루션을 사용하여 확장성을 확보해야 합니다.

 서버 측 이벤트 필터링과 클라이언트 측 서브스크립션 필터링을 적절히 조합하면 네트워크 트래픽을 최소화하고 클라이언트에 필요한 데이터만 전달할 수 있습니다. 서버 측 필터링은 불필요한 데이터 전송을 막아 성능을 향상시키고, 클라이언트 측 필터링은 더 세밀한 데이터 제어를 가능하게 합니다.