GraphQL 서브스크립션은 실시간 데이터 업데이트를 클라이언트에 제공하는 강력한 기능입니다.
NestJS에서는 이를 효과적으로 구현할 수 있는 도구와 패턴을 제공합니다.
GraphQL 서브스크립션 개념
서브스크립션은 웹소켓을 통해 서버에서 클라이언트로 실시간 데이터를 푸시하는 메커니즘입니다.
클라이언트가 특정 이벤트를 구독하면, 해당 이벤트 발생 시 서버가 자동으로 데이터를 전송합니다.
NestJS에서 서브스크립션 설정
의존성 설치
npm install @nestjs/graphql @nestjs/websockets graphql-subscriptions
Copy
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 {}
Copy
서브스크립션 리졸버 구현
@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' );
}
}
Copy
PubSub 시스템 구현
PubSub 프로바이더 생성
import { PubSub } from 'graphql-subscriptions' ;
@ Module ({
providers: [
{
provide: 'PUB_SUB' ,
useValue: new PubSub (),
},
],
})
export class PubSubModule {}
Copy
이벤트 발행
@ 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 ;
}
}
Copy
서버 측 이벤트 필터링
서버에서 특정 조건에 맞는 이벤트만 클라이언트에 전송
@ Subscription ( returns => Post , {
filter : ( payload , variables ) =>
payload . postAdded . authorId === variables . authorId ,
})
postAdded (@ Args ( 'authorId' ) authorId : number ) {
return this . pubSub . asyncIterator ( 'postAdded' );
}
Copy
클라이언트 측 서브스크립션 필터링
클라이언트에서 필요한 데이터만 요청
subscription OnPostAdded ( $authorId : ID !) {
postAdded ( authorId : $authorId ) {
id
title
content
}
}
Copy
대규모 애플리케이션에서의 확장성
Redis PubSub을 사용한 확장 전략
Redis PubSub 설치
npm install graphql-redis-subscriptions ioredis
Copy
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 {}
Copy
인증과 권한 부여
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' );
}
}
Copy
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 {
// 토큰 검증 구현
}
}
Copy
복잡한 실시간 기능 구현
채팅 시스템 예시
@ 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 );
}
}
Copy
Best Practices
효율적인 필터링 : 서버 측 필터링을 사용하여 불필요한 데이터 전송 최소화
연결 관리
@ Subscription ( returns => Post , {
resolve : ( payload ) => payload . postAdded ,
onSubscribe : ( variables ) => {
// 연결 설정 로직
},
})
postAdded () {
return this . pubSub . asyncIterator ( 'postAdded' );
}
Copy
에러 처리
@ Catch ()
export class SubscriptionExceptionFilter implements GqlExceptionFilter {
catch ( exception : Error ) {
return new Error ( 'Subscription error: ' + exception . message );
}
}
Copy
성능 모니터링 : 활성 구독 수, 메시지 처리량 등 모니터링
연결 제한 : 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 ));
});
});
Copy
GraphQL 서브스크립션을 구현하는 것은 실시간 기능을 애플리케이션에 통합하는 강력한 방법입니다.
웹소켓을 통한 실시간 통신은 채팅 애플리케이션, 실시간 대시보드, 협업 도구 등 다양한 사용 사례에서 중요한 역할을 합니다.
PubSub 시스템은 서브스크립션의 핵심 요소로, 이벤트 발행과 구독을 관리합니다. 소규모 애플리케이션에서는 메모리 내 PubSub이 충분할 수 있지만, 대규모 시스템에서는 Redis와 같은 외부 PubSub 솔루션을 사용하여 확장성을 확보해야 합니다.
서버 측 이벤트 필터링과 클라이언트 측 서브스크립션 필터링을 적절히 조합하면 네트워크 트래픽을 최소화하고 클라이언트에 필요한 데이터만 전달할 수 있습니다. 서버 측 필터링은 불필요한 데이터 전송을 막아 성능을 향상시키고, 클라이언트 측 필터링은 더 세밀한 데이터 제어를 가능하게 합니다.