서브스크립션과 실시간 기능 구현
GraphQL 서브스크립션은 실시간 데이터 업데이트를 클라이언트에 제공하는 강력한 기능입니다.
NestJS에서는 이를 효과적으로 구현할 수 있는 도구와 패턴을 제공합니다.
GraphQL 서브스크립션 개념
서브스크립션은 웹소켓을 통해 서버에서 클라이언트로 실시간 데이터를 푸시하는 메커니즘입니다.
클라이언트가 특정 이벤트를 구독하면, 해당 이벤트 발생 시 서버가 자동으로 데이터를 전송합니다.
NestJS에서 서브스크립션 설정
- 의존성 설치
npm install @nestjs/graphql @nestjs/websockets graphql-subscriptions
- 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 시스템 구현
- PubSub 프로바이더 생성
import { PubSub } from 'graphql-subscriptions';
@Module({
providers: [
{
provide: 'PUB_SUB',
useValue: new PubSub(),
},
],
})
export class PubSubModule {}
- 이벤트 발행
@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을 사용한 확장 전략
- Redis PubSub 설치
npm install graphql-redis-subscriptions ioredis
- 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
- 효율적인 필터링 : 서버 측 필터링을 사용하여 불필요한 데이터 전송 최소화
- 연결 관리
@Subscription(returns => Post, {
resolve: (payload) => payload.postAdded,
onSubscribe: (variables) => {
// 연결 설정 로직
},
})
postAdded() {
return this.pubSub.asyncIterator('postAdded');
}
- 에러 처리
@Catch()
export class SubscriptionExceptionFilter implements GqlExceptionFilter {
catch(exception: Error) {
return new Error('Subscription error: ' + exception.message);
}
}
- 성능 모니터링 : 활성 구독 수, 메시지 처리량 등 모니터링
- 연결 제한 : DoS 공격 방지를 위한 연결 수 제한 구현
- 재연결 전략 : 클라이언트 측 재연결 로직 구현
- 테스트 : 단위 테스트 및 통합 테스트 작성
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 솔루션을 사용하여 확장성을 확보해야 합니다.
서버 측 이벤트 필터링과 클라이언트 측 서브스크립션 필터링을 적절히 조합하면 네트워크 트래픽을 최소화하고 클라이언트에 필요한 데이터만 전달할 수 있습니다. 서버 측 필터링은 불필요한 데이터 전송을 막아 성능을 향상시키고, 클라이언트 측 필터링은 더 세밀한 데이터 제어를 가능하게 합니다.