- 사용처
- 랭킹 시스템: 최신 영화, 인기 영화등
- 사용자 세션 데이터: 세션 정보 저장, 토큰 검증 프로세스 스킵, 토큰 밴
- 변화가 적은 데이터 캐싱: 영화 상세내용
- 외부 API 캐싱: 외부 API 결과를 캐싱해서 외부 리소스 사용 비용 절감
- Rate Limiting, Throttling: 사용자의 요청 횟수를 캐싱한 후 특정 횟수를 넘으면 에러를 번환 할 수 있다.
- 장점
- 퍼포먼스 향상: 데이터를 빠르게 가져오고 백엔드 서비스 과부하를 최소화 할 수 있다.
- Scalability: 캐싱을 사용하지 않을때보다 훨씬 높은 트래픽을 감당할 수 있다.
- 비용 절감: 비싼 리소스를 캐싱 해두어서 비용절감 효과를 누릴 수 있다.
- UX 개선: 퍼포먼스가 좋아지면서 자연스럽게 UX 개선이 된다.
- 단점
- 스테일 (Stale) 데이터: 데이터 신선도가 떨어진다. 즉, 최신 데이터가 아니다.
- 메모리 사용 증가: 캐시는 빠른 접근이 목적이기 때문에 메모리에 저장된다. 메모리 사용량이 늘어난다.
- 디자인 복잡성: 소프트웨어 아키텍처에 캐시가 포함되면서 디자인 복잡도가 높아진다.
- 보안 리스크: 적합한 데이터를 캐싱하지 않으면 보안 리스크가 생길 수 있다.
- 설치
yarn add @nestjs/cache-manager cache-manager
https://springdream0406.tistory.com/194
- 사용
// movie.module.ts
imports: [
CacheModule.register(),
],
모듈에 ttl 적용하고 싶으면
// movie.module.ts
imports: [
CacheModule.register({
ttl: 3000,
}),
cache-manager 버전에 따라 ttl 초기준이 바뀜.
https://docs.nestjs.com/techniques/caching
++ 개인적으로 Controller에 CacheKey 없이 적용하는게 간단하고 좋은듯.
- Controller에 적용
@Get('recent')
@UseInterceptors(CacheInterceptor)
@CacheKey('getMoviesRecent')
@CacheTTL(1000)
getMoviesRecent() {
return this.movieService.findRecent();
}
CacheKey, CacheTTL 생략가능,
CacheKey 없으면 각 api에 적용됨 = param으로 변경값 들어오면 각 param에 cache적용됨 => 필터링 별로 각각 캐시 적용됨
- Service에 적용
// movie.service.ts
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
constructor(
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache,
) {}
async findRecent() {
const cacheData = await this.cacheManager.get('MOVIE_RECENT');
if (cacheData) return cacheData;
const data = this.movieRepository.find({
order: {
createdAt: 'DESC',
},
take: 10,
});
await this.cacheManager.set('MOVIE_RECENT', data, 0);
return data;
}
ttl 0 = 무한
await this.cacheManager.set('MOVIE_RECENT', data);
service에서도 ttl 설정되어 있으면 service 설정 우선
++ middleware에서 검증하고, guard에서 또 확인하는 예제 코드들을 생각해보면 개인적으로 middleware에서의 검증에 대해 비관적임.
차라리 가드에 cache 기능 추가하는게 나을듯
- Middleware에 사용
// app.module.ts
imports: [
CacheModule.register({
ttl: 0,
isGlobal: true,
}),
],
// bearer-token.middleware.ts
@Injectable()
export class BearerTokenMiddleware implements NestMiddleware {
constructor(
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache,
) {}
async use(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader) return next();
const token = this.validateBeaerToken(authHeader);
const tokenKey = `TOKEN_${token}`;
const cachePayload = await this.cacheManager.get(tokenKey);
if (cachePayload) {
req.user = cachePayload;
return next();
}
const decodedPayload = this.jwtService.decode(token);
if (decodedPayload.type !== 'refresh' && decodedPayload.type !== 'access')
throw new UnauthorizedException('잘못된 토큰입니다!');
const secretKey =
decodedPayload.type === 'refresh'
? envKeys.refreshTokenSecret
: envKeys.accessTokenSecret;
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get<string>(secretKey),
});
// payload['exp']
const expiryDate = +new Date(payload['exp'] * 1000);
const now = +Date.now();
const differenceInSeconds = (expiryDate - now) / 1000;
await this.cacheManager.set(
tokenKey,
payload,
Math.max((differenceInSeconds - 30) * 1000, 1),
);
req.user = payload;
next();
} catch (e) {
if (e.name === 'TokenExpiredError')
throw new UnauthorizedException('토큰이 만료됐습니다.');
next();
}
}
validateBeaerToken(rawToken: string) {
const basicSplit = rawToken.split(' ');
if (basicSplit.length !== 2)
throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
const [bearer, token] = basicSplit;
if (bearer.toLowerCase() !== 'bearer')
throw new BadRequestException('토큰 포맷이 잘못됐습니다!');
return token;
}
}
한번 검증된 토큰을 expire에 맞춰 ttl을 설정하여 expire전까지 토큰 검증절차를 pass하는 코드.
예제 코드처럼 특정값을 사용해서 만드는 ttl의 경우 주의 필요. (0이 안되게)
cpu의 사용량을 낮추지만 대신 메모리 사용량이 증가하는 단점이 있음.
++ TokenBlock의 경우 블락하는 api 만들어서 들어온 토큰정보를 cache에 `$BLOCK_TOKEN_${token}`으로 위코드처럼 만든 후 set 하고 위의 middleware 코드의 token 이후 바로 cache에서 `$BLOCK_TOKEN_${token}` get해서 있으면 에러 날리는 방식
- Throttling
// throttle.decorator.ts
export const Throttle = Reflector.createDecorator<{
count: number;
unit: 'minute';
}>();
데코 만들고 (unit은 원하는거 추가 가능)
// throttle.interceptor.ts
@Injectable()
export class ThrottleInterceptor implements NestInterceptor {
constructor(
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache,
private readonly reflector: Reflector,
) {}
async intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
// URL_USERID_MINUTE
// VALUE -> count
// 로그인 안한대상도 하고 싶다면 IP로 대체
const userId = request?.user?.sub;
if (!userId) return next.handle();
const throttleOptions = this.reflector.get<{
count: number;
unit: 'minute';
}>(Throttle, context.getHandler());
if (!throttleOptions) return next.handle();
const date = new Date();
const minute = date.getMinutes();
const key = `${request.method}_${request.path}_${userId}_${minute}`;
const count = await this.cacheManager.get<number>(key);
console.log(key);
console.log(count);
if (count && count >= throttleOptions.count)
throw new ForbiddenException('요청 가능 횟수를 넘어섰습니다!');
return next.handle().pipe(
tap(async () => {
const count = (await this.cacheManager.get<number>(key)) ?? 0;
this.cacheManager.set(key, count + 1, 60000);
}),
);
}
}
유저데이터 넣은 키 만들고 접속할때마다 count1씩 증가, ttl 6초 넣음 => 6초 내에 같은 key로 접속한 횟수 데코로 넘겨받은 count비교 후 에러 던지기
// app.module.ts
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ThrottleInterceptor,
},
],
전역 설정
// movie.controller.ts
@Get()
@Public()
@Throttle({
count: 5,
unit: 'minute',
})
@UseInterceptors(CommonCacheInterceptor)
getMoives(
@Query() dto: GetMoviesDto, //
@UserId() userId?: number,
) {
return this.movieService.findAll(dto, userId);
}
컨트롤러에 데코 달아주고 설정넣어주기
https://fastcampus.co.kr/classroom/239666
'코딩 > Nest.js' 카테고리의 다른 글
Logging (2) | 2024.10.26 |
---|---|
Task Scheduling (1) | 2024.10.26 |
Multer (0) | 2024.10.25 |
Custom Decorator (1) | 2024.10.24 |
Exception Filter (1) | 2024.10.23 |