$ npm install cache-manager
$ npm install -D @types/cache-manager
docker run -d \
-e REDIS_PASSWORD=dosimpact\
-p 6380:6379 \
--name redis_api_cache \
--restart always \
redis:latest /bin/sh -c 'redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}'
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: process.env.NODE_ENV === 'dev' ? '.env.dev' : '.env.test',
ignoreEnvFile: process.env.NODE_ENV === 'prod',
}),
CacheModule.register({
store: redisStore as CacheStoreFactory,
url: process.env.REDIS_API_CACHE_URL,
ttl: +process.env.REDIS_API_CACHE_TTL, // 10초 캐슁
// max: 3, // 3개의 key값 유지
}),
TypeOrmModule.forFeature([
Category,
CategoryList,
Corporation,
DailyStock,
FinancialStatement,
]),
],
controllers: [FinanceController],
providers: [FinanceService],
exports: [FinanceService],
})
cacheManager.set('name',dododo) // redis에 key-value 등록
cacheManager.get('name') // redis에 key-value 일릭
cacheManager.del('name') // redis에 key-value 삭제
import { CacheKey, CacheTTL, CACHE_MANAGER} from '@nestjs/common';
import { Cache } from 'cache-manager';
@Controller()
export class AppController {
constructor(
// ✅ CACHE_MANAGER 라는 예약된 상수로 캐시매니저를 주입받는다.
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache,
) {
// ✅ 캐시 매니저를 다루는 기본적인 함수들
const test = async () => {
// await cacheManager.set('name', 'dodo', { ttl: 1000 });
console.log(await cacheManager.get('name'));
console.log(await cacheManager.del('name'));
console.log(await cacheManager.get('name'));
};
}
// ✅ 더하기에 대해서 캐싱처리를 해보자.
@Get('/adder2')
async findAll2(
@Query('a') a: number,
@Query('b') b: number,
@Req() request: Request,
): Promise<number> {
// ✅ 더하기에 대해, 캐시가 있는 경우, 캐시값을 리턴한다.
const memo: string | null = await this.cacheManager.get(request.url);
if (memo) {
return Number(memo);
}
// ✅ 그렇지 않다면 더하기 값을 구하고 캐시 처리후 리턴한다.
const sleep = async (ms) => new Promise((res) => setTimeout(res, ms));
console.log('wait...', a, '+', b);
await sleep(3000);
console.log('wait...', a, '+', b);
const res = a + b;
await this.cacheManager.set(request.url, res);
return res;
}
}
CASE1 은 캐시처리를 수동으로 해서 높은 자유도를 주지만 반복적인 코드가 있다.
그러면 특정 컨트롤러 객체에 요청이 들어오면 알아서 CASE1과 같은 로직을 해주면 좋을 것 같다.
컨트롤러에 캐시인터셉터를 적용시키면 가능하다.
하지만 query,params,body에 대한 차이점은 무시한다.
변수가 없는 GET 요청에 적합
import { UseInterceptors,CacheInterceptor,CacheKey, CacheTTL, CACHE_MANAGER} from '@nestjs/common';
import { Cache } from 'cache-manager';
//✅ 네스트가 제공해주는 인터셉터에 캐시 인터셉터(역시 네스트가 제공) 데코레이터를 달아주자.
@UseInterceptors(CacheInterceptor)
@Controller()
export class AppController {
constructor(
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache,
) { }
// ✅ CacheKey 값을 설정해 주면 자동으로 캐시 처리가 된다.
// TTL 로 Time to Live 값 설정가능
// ❌, 하지만 문제점은 a,b 가 바뀌어도 3초동안 캐시된 값을 리턴한다.
@CacheKey('adder')
@CacheTTL(3)
@Get('/adder')
async findAll(@Query('a') a: number, @Query('b') b: number): Promise<number> {
const sleep = async (ms) => new Promise((res) => setTimeout(res, ms));
console.log('wait...', a, '+', b);
await sleep(3000);
console.log('wait...', a, '+', b);
return a + b;
}
}
import { CacheInterceptor, ExecutionContext, Injectable } from '@nestjs/common';
import { Request } from 'express';
// ✅( 0) CacheInterceptor : path 만 동일하면 캐슁
// -문제점 : querystring이 다르면 다른요청이 와야하는데, 그렇지 못함
// ✅ (1) 캐시정책 : originalUrl (Params/query) 이 동일하면 캐슁
@Injectable()
export class HttpCacheInterceptor extends CacheInterceptor {
trackBy(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<Request>();
return request.originalUrl;
}
}
// ✅ (2) 캐시정책 : protocol://hostname+originalUrl 이 동일하면 캐슁
@Injectable()
export class HttpHostCacheInterceptor extends CacheInterceptor {
trackBy(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<Request>();
return request.protocol + '://' + request.hostname + request.originalUrl;
}
}
// ✅ (3) 캐시정책 : originalUrl (Params/query) + Body 동일하면 캐슁
@Injectable()
export class HttpBodyCacheInterceptor extends CacheInterceptor {
trackBy(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<Request>();
return request.originalUrl + JSON.stringify(request.body);
}
}
import { HttpBodyCacheInterceptor } from './common/service/HttpCacheInterceptor';
// ✅ 인터셉터 하나로 모든 컨트롤러에 캐시 적용
@UseInterceptors(HttpBodyCacheInterceptor)
@Controller()
export class AppController {
constructor(
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache,
) {}
@Post('/adder3')
async findAll3(@Body() { a, b }: { a: number; b: number }): Promise<number> {
const sleep = async (ms) => new Promise((res) => setTimeout(res, ms));
console.log('wait...', a, '+', b);
await sleep(3000);
console.log('wait...', a, '+', b);
const res = a + b;
return res;
}
}
import * as redisStore from 'cache-manager-redis-store';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: process.env.NODE_ENV === 'dev' ? '.env.dev' : '.env.test',
ignoreEnvFile: process.env.NODE_ENV === 'prod',
}),
CacheModule.register({
store: redisStore as CacheStoreFactory,
url: process.env.REDIS_API_CACHE_URL,
ttl: +process.env.REDIS_API_CACHE_TTL, // 10초 캐슁
// max: 3, // 3개의 key값 유지
}),
TypeOrmModule.forFeature([
Category,
CategoryList,
Corporation,
DailyStock,
FinancialStatement,
]),
],
controllers: [FinanceController],
providers: [FinanceService],
exports: [FinanceService],
})
export class FinanceModule {}
// ✅ (3) 캐시정책 : originalUrl (Params/query) + Body 동일하면 캐슁
@Injectable()
export class HttpBodyCacheInterceptor extends CacheInterceptor {
trackBy(context: ExecutionContext) {
const request = context.switchToHttp().getRequest<Request>();
return request.originalUrl + JSON.stringify(request.body);
}
}
import { HttpBodyCacheInterceptor } from 'src/common/service/HttpCacheInterceptor';
@UseInterceptors(HttpBodyCacheInterceptor)
@Controller('/api/finance/')
export class FinanceController {
constructor(private readonly financeService: FinanceService) {}
// stock --- api
// (1) 기업 리스트 출력
@Get('corporations')
async getCorporations() {
return this.financeService.getCorporations();
}
// (2) 기업 리스트 출력 (검색어 기능 )
@Get('corporations/:term')
async getCorporationsWithTerm(@Param('term') term: string) {
return this.financeService.getCorporationsWithTerm({ term });
}
// (3) 기업 리스트 출력 (검색어 기능 )
@Get('corporation/:term')
async getCorporation(@Param('term') term: string) {
return this.financeService.getCorporation({ term });
}
// daily-stock --- api
@Get('dailystock/:term')
async getDailyStocks(
@Param('term') term: string,
@Query('skip') skip: number,
@Query('take') take: number,
) {
return this.financeService.getDailyStocks({ term, take, skip });
}
}
감사합니다