[AWS] Redis로 API 캐싱하기
이전 포스트
https://blog.naver.com/sssang97/222462346909
Redis는 여러가지 용도로 사용할 수 있지만, 가장 뛰어난 사용례 중 하나는 캐싱이다.
자주 사용되는 결과값을 미리 저장해놓고 꺼내쓰기만 하는 것이다.
내 경우에는 사이트의 메인페이지에서 사용되는 일부 API들이 꽤 무거운 편이었다.
메인페이지니까 당연히 엄청 자주 사용되는데, 자주 바뀌는 것도 아니면서 괜히 성능만 잔뜩먹다 한계치에 도달하는 경우가 좀 있었다.
그래서 이걸 1시간 주기로 캐싱해서 결과값을 렌더링하기로 결정했다.
Redis 생성
관리비용을 최대한 줄이고 싶다면 AWS의 Elasticache로 레디스를 올리는게 좋다.
AWS에 올릴 경우는 다음 포스트를 참조한다.
https://blog.naver.com/sssang97/222462346909
하고싶다면 직접 아무데다 올려서 써도 되긴 한다.
레디스용 프록시 함수 생성
이건 Elasticache로 redis를 사용할 때만 필요한 방법이다.
Elasticache는 무조건 같은 VPC에서만 사용할 수 있기 때문에, 유연한 사용을 원한다면 같은 VPC의 중간다리를 만들어서 사용하는 것이 편리하다.
이게 필수는 아니고, 그냥 사용하려는 서버에서 바로 호출해도 된다. VPC만 어떻게든 동일하게 맞출 수 있다면 사용에는 문제가 없다.
함수를 만들고, Elasticache와 동일한 VPC를 설정에 적용해준다.

그리고 람다에서 VPC를 통해 접근할 수 있도록 AWSLambdaVPCAccessExecutionRole 역할을 람다의 권한에 추가해준다.

그리고 소스는 대강 아래와 같은 식으로 구성하면 될 것이다.
이벤트값에 키와 값이 다 있다면 삽입으로 간주하고 삽입, 키만 있다면 조회를 해오도록 해뒀다.

const redis = require('redis');
const { promisify } = require("util");
const port = 6379;
const host = '...apn2.cache.amazonaws.com';
const expireTime = 60 * 60; //만료시간
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: null,
};
console.log(`event: ${JSON.stringify(event)}`)
try {
const client = redis.createClient(port, host);
const getAsync = promisify(client.get).bind(client);
const setexAsync = promisify(client.setex).bind(client);
if(event.value) {
await setexAsync(event.key, expireTime, event.value)
} else {
const data = await getAsync(event.key)
response.body = data;
}
} catch(error) {
console.error(error)
}
return response;
};
만료시간은 취향껏 조정하면 되고, 레디스의 자동 만료 기능이 필요없다면 setex 대신에 set을 사용하면 된다.
서버에 캐싱 적용
그럼 위에서 만든것으로 캐시를 붙여보자.
여기서의 예제는 Nestjs이나, 다른 언어를 사용하더라도 크게 다르지 않을 것이다.
다음과 같이 방금의 람다 호출 함수를 정돈해두고
import aws from 'aws-sdk';
let lambda = null;
try {
aws.config.update({
region: 'ap-northeast-2',
accessKeyId: ...,
secretAccessKey: ...,
});
lambda = new aws.Lambda();
} catch (error) {
console.error(error);
}
export async function redisProxy(payload: { key: string; value?: string }) {
const response = await lambda
?.invoke({
FunctionName: 'redis-proxy-function',
Payload: JSON.stringify(payload),
})
?.promise();
const responsePayload = JSON.parse(response?.Payload);
return responsePayload?.body;
}
캐싱용 인터셉터를 정의한다.
import {
CallHandler,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
NestInterceptor,
UnauthorizedException,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { redisProxy } from '../functions/redis-proxy';
@Injectable()
export class RedisCacheInterceptor implements NestInterceptor {
async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest() as Request;
const response = context.switchToHttp().getResponse() as Response;
const url = request?.url;
const cachedResponse = await redisProxy({ key: url });
<br>
if (cachedResponse) {
response.json(JSON.parse(cachedResponse));
return new Observable();
} else {
return next.handle().pipe(
tap(async (data) => {
await redisProxy({ key: url, value: JSON.stringify(data) });
}),
);
}
}
}
url을 키, 리스폰스를 값으로 해서 url에 저장된 캐시값이 있다면 그걸 곧장 반환.
없다면 정상실행을 시키고 리스폰스를 캐시에 저장한다.
이러고 이 인터셉터를 캐싱시키고싶은 핸들러에 달아주면 된다.

그렇다.