Nestjs에서 Redis 사용기

윤학·2023년 3월 13일
2

Nestjs

목록 보기
2/12
post-thumbnail

Redis란?

Redis는 속도가 상당히 빠른 인메모리의 키 값 데이터 구조이다.
Set, Hash, List, Sorted Set, Streams등의 여러 자료구조를 활용해서 애플리케이션에서 다양한 기능들을 구현해 볼 수 있다.

Redis에 대한 설명은 너무 많고 잘 나오기 때문에 패스!!

왜 사용했는지?

저번 프로젝트를 했을 때 Redis를 로그인 과정 시에 해당 유저가 Naver Cloud API를 통해 받은 전화번호 인증번호와 인증번호 별 시도 횟수, 하루 인증번호 요청 횟수, 차단시간을 필요에 따라 TTL값과 함께 사용하였다.

이번 프로젝트에서는 사용자가 영상요약을 요청하고 완전히 업로드를 할 때까지 중간에 임시로 저장해 놓는 용도, 비디오 별 좋아요 사용자 목록 저장용, 좋아요 많이 받은 순으로 비디오를 가져오기 위한 순위 집계용으로 사용하였다.

Nest에서 기본적으로 CacheModule을 제공해서 외부 저장소로 Redis를 사용할 수 있지만 난 Redis에서 사용할 수 있는 다양한 명령어들을 사용하기 위해 node-redis라는 Redis Client를 이용하였다.

그럼 시작!

1. 설치

npm install redis 설치는 굉장히 단순하여 이 명령어 하나면 끝!

2. Redis 연결

1. RedisProvider

redis.provider.ts

import { createClient } from 'redis';

export const redisProvider = [
    {
        provide: 'REDIS_CLIENT',
        useFactory: async () => {
            const client = createClient({
                url: 'redis://172.18.10.11:6379'
            });
            await client.connect();
            return client;
        }
    }
]

내 현재 구성은 AWS EC2의 1개의 인스턴스내에 각 서비스 별 DockerDocker Compose에서 구성한 네트워크의 ip를 각각 부여해 구성하고 있어 알맞은 url을 적어준다.

또한, createClient함수는 RedisClientOptions를 인자로 받고 있는데 url 말고도 다양한 옵션들이 있으니 필요하면 쓰면 됨!

마지막으로 추후에 REDIS_CLIENT라는 토큰 값으로 Provider를 주입하여 사용할 수 있게 했다.

2. RedisModule

redis.module.ts

import { Module } from "@nestjs/common";
import { redisProvider } from "./redis.provider";

@Module({
    providers: [...redisProvider],
    exports: [...redisProvider]
})
export class RedisModule {}

모듈화를 시켰으면 다른 모듈에서도 사용할 수 있도록 내보내준 뒤 Redis를 사용하고자 하는 모듈에서 Redis 모듈을 import해서 사용한다.

3. DI

Injection tokens
Can be types (class names), strings or symbols. This depends on how the provider with which it is associated was defined. Providers defined with the @Injectable() decorator use the class name. Custom Providers may use strings or symbols as the injection token.

@Inject 데코레이터의 설명을 보면 사용하고자 하는 Service에서 나 이제 Redis를 사용할거다!라고 알려줘야 하는데 그걸 아까 설정한 토큰 값으로 주입을 해서 알려줄 수 있다고 나와있다.

그래서 바로 주입!

 constructor(
        @Inject('REDIS_CLIENT')
        private readonly redis: RedisClientType
    ) {}

여기까지 설정했으면 이런식으로 여기에서 원하는 명령어를 사용하는 방법을 보고 사용할 수 있다.

const originVideoPath = await this.redis.HGET('process:video:list', `user:${userId}`);

3. 고려할점

1. Redis Client 선택

Redis Clientnode에서 Redis를 사용할 수 있는 유명한 라이브러리로 node-redisioredis가 있었지만 Redis의 공식 자바스크립트 라이브러리는 node-redis여서 이 라이브러리를 사용해왔다.

추가적으로 Mongoose처럼 Redis에서 JS의 객체와 Object Mapping을 사용하여 개발할 수 있도록 해주는데 OM을 사용하려면 redis-om-node라는 라이브러리를 사용해야 하고 그러기 위해선 RedisJSONRedisSearch모듈이 필요하여 node-redis 라이브러리를 사용하여야 한다.

하지만 이 글에 따르면 인수로 버퍼가 올 경우 인수당 매번 3번의 소켓 쓰기를 하여 CPU사용률이 더 높다고 나와 있으니 ioredis도 고려해보자.

2. 메모리 관리

Redis의 Collection 중 SETHashHash Table을 사용하여 저장되는데 SET은 자신이 설정한 키의 개수보다 적거나 값이 정수일 때는 배열을 사용하며, Hash는 자신이 설정한 hash-zipmap-max-entries보다 키 개수가 적거나 값의 길이가 자신이 설정한 hash-max-ziplist-value 값의 길이보다 적을 때 Ziplist를 이용한다.

이 조건을 만족하지 못하게 될 경우부터는 Hash Table로 변환되어 저장이 되는데 메모리 최적화 문서에도알 수 있듯이 가능하면 Hash를 사용할 것을 권장하고 있고, Hash에 저장할 필드값이 많을 경우 인스타그램의 Hash 사용에서 볼 수 있듯이 hash-max-ziplist-entries의 값을 변경하여 어떤식으로 메모리를 적게 사용할 수 있는지 알 수 있다.

3. 영속성

Redis는 기본적으로 메모리 저장소이기 때문에 서버가 재시작하면 저장되었던 데이터가 전부 사라진다.

Redis를 캐싱용도로만 사용한다면 데이터의 유실 여부는 그렇게 중요하지 않겠지만 데이터베이스의 한 종류로 사용한다면 알고는 있어야 한다고 생각한다.

그래서 살펴보면 Redis에서는 디스크에 메모리에 있는 데이터를 쓸 수 있는 2가지 방법이 있다.

AOF 방식

조회를 제외한 입력, 수정, 삭제 명령이 수행될 때마다 해당 명령을 기록한다.
기록시점은 always, everysec, no가 있는데 everysec을 사용하도록 권장하고 있다.

이유는 1초 사이의 데이터가 유실될 가능성이 있지만 성능에 영향을 거의 미치지 않으며 데이터를 보존할 수 있기 때문!(always로 사용시 성능이 급격히 저하됨)

또한, 이렇게 계속해서 파일에 기록된다면 파일의 사이즈가 너무 커져 OS 파일 사이즈 제한이 걸리수도 있고, 재시작 시 로드 시간이 너무 오래 걸릴 수 있다는 점때문에 rewrite를 수행해야된다.

rewrite를 수행하면 파일 사이즈가 작아지는데 그 이유는 메모리에는 똑같은 명령어에 대해서 최종 데이터 값만 저장하고 나머지는 사라지기 때문!

INCY 명령어를 1000번 하고 rewrite하면 최종 값인 1000만 메모리에 저장!

그럼 rewrite를 계속 수행하면 되지 않나?

적은 파일 사이즈에서 계속해서 rewrite가 수행된다면 그 또한 불필요한 일이기 때문에 막기위해 auto-aof-rewrite-percentage로 파일 사이즈가 일정 퍼센트 이상일 경우에만 수행되도록 설정할 수 있고 auto-aof-rewrite-min-sizerewrite되는 최소 사이즈를 설정할 수 있는데 공식문서에는 percentage보단 AUTO-AOF-REWRITE-SPEC-TIME으로 설정한 시간마다 rewrite 하는 것을 권장하고 있다.

그리고 AOF파일은 text 파일로 되어 있기 때문에 내가 실수로 FLUSHALL같은 명령어를 사용하는 재앙이 일어날지라도 빠르게 서버를 내리고 AOF파일을 열어서 FLUSHALL명령어를 삭제하고 재시작하면 다시 데이터가 복구되게 할 수 있다 ㅎㅎ..

RDB방식

설정한 시간동안 설정한 개수 이상의 키가 변경되면 메모리에 있는 전체 데이터를 디스크에 저장하는 방식

하지만 이런 이벤트마다 메모리 전체를 디스크에 쓰는 것은 문제를 발생시킬 수 있다 하여 데이터를 디스크에 보관하는 것은 AOF방식을 사용하는 것을 권장하고 있고 기본적으로 비활성화 되어 있다.

4. Key

이번에 식별 id를 AUTO_INCREMENT가 아닌 uuid로 사용하였는데 이걸 쓰면서 생각이 든 건 Redis에서 사용할 수 있는 Bitmap구조를 사용할 수 없다는 것이였다.

AUTO_INCREMENT를 사용한다면 Bitmap에서 각 유저별 첫 방문, 검색 여부, 현재 접속 중인지에 대한 여부등을 굉장히 유용하게 사용할 수 있을 것 같았다.

그래서 어떻게 하면 유저 별로 Redis에서 사용할 Key를 발급할 수 있을지 생각하다 가장 쉬운 방법은 회원가입 시 redis_key라는 필드로 AUTO_INCREMENT값을 주는 것이라고 생각했다.

이렇게 하면 로그인 성공 시 uuid와 함께 key를 넘겨줘서 필요한 Api 요청시 사용할 수 있고, 탈퇴 시에도 해당 유저의 key값만 처리를 해주면 된다고 생각했기 때문!

실무에서는 어떻게 활용하는지 알고 싶다...

마무리하면서..

RedisAWSGoogle에서 제공하는 서비스 말고 자체적으로 MongodbAtlas와 같은 Cloud 서비스가 있는 것으로 아는데 메모리량이 곧 돈이기 때문에 메모리를 어떻게 효율적으로 사용할 수 있는지에 대한 공부는 계속해서 필요하다고 느낀다.

참고

Node-Redis
Redis Memory Optimization
Redis Configure File
Redis Persistence Introduction
Redis BenchMark By AOF
인스타그램의 Redis 사용
Ably의 Migrating from Node Redis to Ioredis

profile
해결한 문제는 그때 기록하자

0개의 댓글