캐시 적용에 대해 생각하다보니 3가지 의문이 생겼다.
그러려면 우선 NestJS의 요청 처리 과정에 대해 알 필요가 있다. (그래야 어느 부분에 추가할지 알 수 있으니)
요청
→ 미들웨어(글로벌 → 모듈)
→ 가드(글로벌 → 컨트롤러 → 라우트)
→ 인터셉터(글로벌 → 컨트롤러 → 라우터, pre-controller)
→ 파이프(글로벌 → 컨트롤러 → 라우트 → 라우트 매개변수)
→ 컨트롤러 → 서비스
→ 인터셉터(라우트 → 컨트롤러 → 글로벌, post-request)
→ 예외 필터(라우트 → 컨트롤러 → 글로벌)
→ 응답
이 중 어느곳에서 캐싱을 처리해야 할까?
일단 가드는 지나야 하고, 컨트롤러 전이어야 하니 인터셉터 혹은 파이프일 거라는 추측이 가능하다.
하지만 파이프는 transform/validation 목적이기 때문에 땡
NestJS 공식문서를 확인해보면 인터셉터 파트에 캐싱 목적으로 사용할 수 있음이 나와있다.
요청이 들어왔을 때 캐시 데이터를 확인한 후
검색해보니 캐시 인터셉터를 사용할 수 있도록 Nest가 제공하고 있다.
https://docs.nestjs.com/techniques/caching#caching
이제 위 문서를 읽어보면, 왜 캐싱이 인터셉터를 사용하는지 이해할 수 있다.
문서 왈
Nest provides a unified API for various cache storage providers. The built-in one is an in-memory data store. However, you can easily switch to a more comprehensive solution, like Redis.
목표는 redis를 이용하는 거지만, 일단 편하게 따라 해보자. 나중에 바꾸면 될듯
정리
AppModule에 imports
import { CacheModule, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [CacheModule.register()],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
클래스 생성자에 추가
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
@Injectable()
export class AppService {
constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
getHello(): string {
return 'Hello World!';
}
}
get: 캐시에서 아이템을 검색, 없으면 null 리턴
const value = await this.cacheManager.get('key');
set await this.cacheManager.set('key', 'value');
기본 만료 시간은 5초, TTL 설정 가능
await this.cacheManager.set('key', 'value', { ttl: 1000 });
아이템 삭제 await this.cacheManager.del('key');
리셋 await this.cacheManager.reset();
인터셉터를 추가하면 자동으로 캐싱됨
@Controller()
@UseInterceptors(CacheInterceptor)
export class AppController {
@Get()
findAll(): string[] {
return [];
}
}
💡 GET 요청만 캐시됨. 또한, @Res() 오브젝트를 주입하는 경로에는 캐시 인터셉트 사용 불가능 (https://docs.nestjs.com/interceptors#response-mapping)
그렇기에 3.x.x로 받아준다.
그런 다음 설정을 변경해주면 됨
import { CacheModule, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import type { ClientOpts } from 'redis';
import * as redisStore from 'cache-manager-redis-store';
@Module({
imports: [
CacheModule.register<ClientOpts>({
store: redisStore,
host: 'localhost',
port: 6379,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
+) 당연하지만 redis 서버를 실행시켜둬야 연결됨
import { CACHE_MANAGER, Controller, Get, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
@Get()
async getHello(): Promise<string> {
const value: string = await this.cacheManager.get('hello');
if (value === null) {
await this.cacheManager.set('hello', 'cache item', 60);
return 'no cache exists';
}
return value;
}
}
redis-cli -h localhost -p 6379
로 접속 후 monitor 명령어를 실행한다.
그리고 curl http://localhost:3000/
명령어로 요청을 보내자.
아래와 같이 로그가 찍히는 것을 확인할 수 있다.
중간 두 줄이 get → if (value === null) true → set으로 흐르는 로직이고 마지막은 캐시값이 있으니 get 후 리턴하는 로직이다.
이렇게 다른 명령어(keys *
, get [key]
)로도 확인 가능
현재 cache-manager가 5버전으로 업데이트된지 얼마 지나지 않아서, 오류가 존재하는 것 같다. 해당 내용은 여기서 확인
import { CacheModule, Module } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store';
@Module({
imports: [
CacheModule.register({
store: redisStore,
host: 'localhost',
port: 6379,
ttl: 0,
}),
})
export class MentorsModule {}
@Get(':intraId')
@UseInterceptors(CacheInterceptor)
@ApiBearerAuth('access-token')
@ApiOperation({
summary: 'Get mentor details',
description: '멘토에 대한 모든 정보를 반환합니다.',
})
@ApiCreatedResponse({
description: '멘토 정보',
type: MentorDto,
})
async getMentorDetails(
@Param('intraId') intraId: string,
): Promise<MentorDto> {
return this.mentorsService.findMentorByIntraId(intraId);
}
이렇게 경로마다 설정 가능 (캐시 키는 /api/v1/mentors/{intraId}로 기본 설정됨 - 요청 라우트)
모듈에 등록할 때 TTL에 0을 설정했기 때문에 자동 삭제가 사라짐
→ 멘토 정보가 수정되는 곳(PATCH /join
, PATCH /:intraId
)에서 캐시를 삭제해주는 로직 추가
export class MentorsController {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
@Patch('join')
@Roles('mentor')
@UseGuards(JwtGuard, RolesGuard)
@ApiBearerAuth('access-token')
@ApiOperation({
summary: 'Post join mentor',
description:
'멘토 필수정보(이름, 이메일, 슬랙아이디, 가능시간, 멘토링 가능 상태, 회사, 직급)를 받아서 저장합니다.',
})
async join(
@Body() body: JoinMentorDto,
@User() user: JwtUser,
): Promise<boolean> {
await this.mentorsService.updateMentorDetails(user.intraId, body);
await this.cacheManager.del(`/api/v1/mentors/${user.intraId}`);
return true;
}
핸들러 내부에서 캐시를 직접 조작하려면 생성자에 캐시매니저를 추가해야 한다.
카테고리 컨트롤러에서는 주로 카테고리/키워드를 받아오는 역할이고, 수정 api가 존재하진 않지만.. 수정이 언젠간 일어날 수도 있기 때문에 일단 TTL을 한시간으로 주었다.
import { CacheInterceptor, CacheModule, Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
imports: [
CacheModule.register({
store: redisStore,
host: 'localhost',
port: 6379,
ttl: 60 * 60,
}),
...
providers: [
KeywordsService,
{
provide: APP_INTERCEPTOR,
useClass: CacheInterceptor,
},
],
})
export class CategoriesModule {}
요런식으로 프로바이더에 추가하면 컨트롤러 전체에 캐싱이 적용된다. (당연히 GET 메소드에만 적용)
하지만 카테고리 컨트롤러에서 멘토 리스트를 받아오게 되는데, 여기서 문제가 발생한다.
→ TTL을 0으로 설정하고, 변경을 추적해서 캐시 데이터를 알맞게 지워줄 수가 없음
그리고 한시간은 너무 긺 → 얘만 따로 TTL을 짧게 설정해주었다.
@Get(':category')
@CacheTTL(60)
@ApiOperation({
summary: 'Get mentor list',
description:
'카테고리(+ keywords, mentorName)를 포함하는 멘토의 리스트를 반환합니다.',
})
@CacheTTL
데코레이터를 이용하면 모듈에 설정해둔 TTL값을 오버라이드한다.