오늘의 주제는 채팅방에 있는 채팅내용을 DB조회가 Redis에 캐싱을 하여 조회하는 방법에 대해서 이야기 해보고자 합니다.
NestJS에서 Custom Interceptor를 활용하여 채팅 추가/조회 하는 부분에 Interceptor를 적용하였습니다.
Redis를 사용하기위하여 docker파일을 작성하였습니다. ( Mac M1 기준)
version: '3'
services:
local-redis:
image: redis:latest
container_name: local-redis
restart: always
ports:
- "6379:6379"
volumes:
- ./db/redis/data:/data
platform: linux/x86_64
CacheManager를 사용하기위한 모듈을 추가해줍니다.
CacheModule.register({
store: redisCacheStore,
host: process.env.REDIS_HOST || redisConfig.host,
port: process.env.REDIS_PORT || redisConfig.port,
isGlobal: true,
ttl: 60,
}),
채팅을 조회하는 부분과 추가하는 부분을 메타데이터 설정으로 핸들러를 나누어 분기 시킵니다.
분기 시키기 위한 CustomDecorator 입니다.
export const CACHE_ACTION_METADATA = "cache:CACHE_ACTION";
export const CacheAction = (cacheActions: string): CustomDecorator<string> =>
SetMetadata(CACHE_ACTION_METADATA, cacheActions);
Chatting Interceptor 는 아래와 같은 코드로 작성 되었습니다.
1. READ일때 Redis에서 채팅내용이 있다면 불러오고 그렇지 않다면 DB 조회로 넘긴후 캐싱 기록
2. CREATE일때는 Redis에 내용을 저장합니다. (최대 1000개의 데이터까지 저장하고, 만료시간은 1시간으로 추가될때마다 초기화 시켜줍니다.)
- 1000개의 채팅 내역을 Caching 할 수 있고, 만약 사용자가 1시간동안 채팅하지 않는다면 캐싱 내역이 사라지게 될 것입니다.
import {
CallHandler,
ExecutionContext,
Inject,
Injectable,
Logger,
LoggerService,
NestInterceptor,
} from "@nestjs/common";
import { Request } from "express";
import { Observable, tap, of } from "rxjs";
import { CACHE_MANAGER } from "@nestjs/cache-manager";
import { Cluster } from "ioredis";
import { ResponseMessage } from "src/chatting/dto/chatting.message.dto";
import { CACHE_ACTION_METADATA } from "./cache.constants";
import { Reflector } from "@nestjs/core";
@Injectable()
export class ChatCacheInterceptor implements NestInterceptor {
constructor(
@Inject(CACHE_MANAGER)
private readonly cacheManager: any, // IORedis.Redis 타입으로 주입받음
@Inject(Logger)
private readonly logger: LoggerService,
private readonly reflector: Reflector
) {}
async intercept(
context: ExecutionContext,
next: CallHandler<any>
): Promise<Observable<any>> {
const redisClient: Cluster = await this.cacheManager.store.getClient();
const actions: string = this.reflector.get<string>(
CACHE_ACTION_METADATA,
context.getHandler()
);
switch (actions) {
case "CREATE":
return next.handle().pipe(
tap((data : ResponseMessage) => {
const { room_id, id } = data;
redisClient.zadd(`room:${room_id}`, id, JSON.stringify(data));
redisClient.zremrangebyrank(`room:${room_id}`, 0, -1001);
redisClient.expire(`room:${room_id}`, 3600); // TTL 설정 (새로운 채팅 메세지가 추가되면 다시 사용될 가능성이 높음)
})
);
case "READ":
// REST API 요청
const request = context.switchToHttp().getRequest();
if (request.method === "GET" && request.params.id) {
const id = Number(request.params.id);
const cursor =
request.query.cursor !== "null"
? Number(request.query.cursor)
: null;
let chatList = [];
// cursor가 null이면, 최근 50개의 채팅을 가져옵니다.
if (!cursor) {
chatList = await redisClient.zrevrange(`room:${id}`, 0, 49);
} else {
// 그렇지 않은 경우에는 cursor를 기반으로 50개의 채팅을 가져옵니다.
chatList = await redisClient.zrevrangebyscore(
`room:${id}`,
cursor - 1,
"-inf",
"LIMIT",
0,
50
);
}
if (chatList && chatList.length > 49) {
this.logger.log(`Redis에서 채팅내용을 불러옵니다. RoomID : ${id}`);
chatList = chatList.map((chat) => JSON.parse(chat));
return of(chatList.reverse());
}
}
// 해당 자료가 없다면 db 에서 조회 합니다.
return next.handle();
default:
return next.handle();
}
}
}
구현한 인터셉터를 적용시킵니다.
// 채팅 추가 부분 ( WebSocket Gateway )
SubscribeMessage("SendMessage")
@UseInterceptors(ChatCacheInterceptor)
@CacheAction("CREATE")
async handleMessage(
@GetWsUser() user: User,
@MessageBody() message: RequestMessage
): Promise<ResponseMessage> {
const room: Room = await this.roomService.getRoom(message.room_id);
room.last_chat = message.message;
await this.roomService.updateRoomStatus(room);
const not_read_chat: number =
room.type !== RoomType.Individual ? room.participant.length : 0;
const ChattingMessage: Chatting = await this.chattingService.createChatting(
message,
user,
room
);
const responseMessage: ResponseMessage = {
id: ChattingMessage.id,
room_id: room.id,
user_id: user.id,
message: message.message,
not_read_chat,
createdAt: ChattingMessage.createdAt,
};
this.server
.to(String(message.room_id))
.emit("SendMessage", responseMessage);
return responseMessage;
}
// 채팅 조회 부분
@UseInterceptors(ChatCacheInterceptor)
@CacheAction("READ")
@Get("chattings/:id")
@ApiOperation({
summary: "방 ID로 채팅리스트들을 가져옵니다. API",
description: "방 ID로 채팅리스트들을 가져옵니다.",
})
@ApiCreatedResponse({ description: "방 ID로 채팅리스트들을 가져옵니다." })
async GetChattingList(
@Param("id") id: number,
@Query("cursor") cursor: number
): Promise<Chatting[]> {
return await this.chattingService.getChattingList(id, cursor);
}
GitHUB를 참조 바랍니다.
https://github.com/rhkdguskim/kakaoTalk_backend
GitHub - rhkdguskim/kakaoTalk_backend: 카카오톡 NestJS 백앤드 서버
카카오톡 NestJS 백앤드 서버. Contribute to rhkdguskim/kakaoTalk_backend development by creating an account on GitHub.
github.com