...
Node 프로젝트에서 pm2로 다중 클러스터 인프라를 구축했다면 세션 불일치 문제가 생기게 마련이다.
만일 서버가 종료되어 메모리가 날라가면 접속자들의 로그인이 모두 풀려버리게 된다.
따라서 이를 방지하기 위해 세션 아이디와 실제 사용자 정보를 외부 데이터베이스에 저장하는 편이다.
이때 개발자들이 많이 사용하는 것이 Redis db이다.
다른 데이터베이스를 사용해도 되지만, 세션은 빠릿빠릿하게 응답을 해야되기 떄문에 메모리 기반의 데이터베이스인 레디스를 사용한다.
지금부터 Node 프로젝트에서 Redis와 연결하고 사용하는 법을 알아보자.
Redis Database 설치
레디스를 로컬에 설치해도 되고, 클라우드로도 사용할 수가 있다. 마음에 드는 것을 골라 설치하자.
개인적으로 로컬 보다는 클라우드로 하는게 관리나 협업 측면에서 편리하고 더 좋았다.
Redis 로컬(윈도우) 설치
Redis 클라우드 설치
redislabs 등록을 맞췄으면, 서버에서 위의 redislabs 사이트의 레디스 db에 접속하기 위해서는, 필요한 정보가 있는데 레디스 host와 port, username, password 4가지를 알아야 한다. (링크 참고)
계정을 등록하고 얻은 redis db의 host와 password 정보들을 env 파일에 환경변수로 저장해준다.
# .env 파일
REDIS_HOST=redis-10943.c114.us-east-1-4.ec2.cloud.redislabs.com
REDIS_PORT=10943
REDIS_USERNAME=default
REDIS_PASSWORD=abcdefghijklmnopqrstuvwsyz
노드에서 Cache 기능 사용하기
Node ↔ Redis v4 연결하기
> npm i redis # redis 패키지 설치
해당 redis 버젼은 최신 v4 기준으로 연재한다.
최근 redis v3에서 v4로 업그레이드 됬는데, 그냥 새로운 패키지 하나를 만든것과 같을정도로 문법과 내부동작이 완전히 달라졌다.
이제 redis v4 버젼은 콜백 기반이 아닌 기본적으로 promise 객체 기반 비동기로 동작되도록 변경했다.
그래도 옛날 문법으로 사용하고 싶다면,createClient({ legacyMode: true })로 레거시 모드를 설정해주면 호환성 문제없이 사용이 가능하다.
간단하게 v4에서 무엇이 바뀌없는지 살펴보려면, 다음 마이그레이션 가이드를 참고하길 바란다.
https://github.com/redis/node-redis/blob/HEAD/docs/v3-to-v4.md
로컬 redis에 연결하기
const express = require('express');
const dotenv = require('dotenv');
const redis = require('redis');
dotenv.config(); // env환경변수 파일 가져오기
//* Redis 연결
const redisClient = redis.createClient({ legacyMode: true }); // legacy 모드 반드시 설정 !!
redisClient.on('connect', () => {
console.info('Redis connected!');
});
redisClient.on('error', (err) => {
console.error('Redis Client Error', err);
});
redisClient.connect().then(); // redis v4 연결 (비동기)
const redisCli = redisClient.v4; // 기본 redisClient 객체는 콜백기반인데 v4버젼은 프로미스 기반이라 사용
redis 클라우드에 연결하기
const express = require('express');
const dotenv = require('dotenv');
const redis = require('redis');
dotenv.config(); // env환경변수 파일 가져오기
//* Redis 연결
// redis[s]://[[username][:password]@][host][:port][/db-number]
const redisClient = redis.createClient({
url: `redis://${process.env.REDIS_USERNAME}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
legacyMode: true, // 반드시 설정 !!
});
redisClient.on('connect', () => {
console.info('Redis connected!');
});
redisClient.on('error', (err) => {
console.error('Redis Client Error', err);
});
redisClient.connect().then(); // redis v4 연결 (비동기)
const redisCli = redisClient.v4; // 기본 redisClient 객체는 콜백기반인데 v4버젼은 프로미스 기반이라 사용
Javascript Redis 문법
자바스크립트 redis 문법은 대부분 redis cli 문법 그대로 메서드 형태로 붙여 쓰면 된다.
만일 아래와 같이 String 키-값을 가져오는 redis cli 명령어가 있을 경우, 자바스크립트에서 redis db와 연결해준 객체에 redisClient.명령어(key, value) 이런식으로 사용하면 된다.
> set key 123
> get key
// redis v3 문법 (콜백 함수 기반)
redisClient.set('key', '123');
redisClient.get('key', (err, value) => {
console.log(value); // 123
})
// redis v4 문법 (프로미스 객체 기반)
let bool = await redisClient.v4.set('key', '123'); // OK
let data = await redisClient.v4.get('key'); // 123
Redis CRUD 구현
const redisCli = redisClient.v4 // 기본 redisClient 객체는 콜백기반인데 v4버젼은 프로미스 기반이라 사용
// GET
router.get('/', (req, res, next) => {
await redisCli.get('username');
});
// POST
router.post('/set', (req, res, next) => {
await redisCli.set('username', 'inpa');
});
// DELETE
router.delete('/del', (req, res, next) => {
// exist : 키가 존재하는지
const n = await redisCli.exists('username'); // true: 1 , false: 0
if(n) await redisCli.del('username');
});
// PUT
router.put('/rename', (req, res, next) => {
// username이라는 키값이 있다면 그 값을 helloname으로 바꿈
redisCli.rename('username', 'helloname');
});
Redis 자료형 명령어
const redisCli = redisClient.v4 // 기본 redisClient 객체는 콜백기반인데 v4버젼은 프로미스 기반이라 사용
// String
await redisCli.set('name', 'nyong')
await redisCli.get('name') // nyong
// List
await redisCli.rpush('fruits', 'apple', 'orange', 'pineapple')
await redisCli.lpush('fruits', 'banana', 'pear')
await redisCli.lrange('fruits', 0, -1) // ['pear', 'banana', 'apple', 'orange', 'apple']
// 0 과 -1은 시작과 끝 인덱스를 의미
// Hash
await redisCli.hmset('friends', 'name', 'nyong', 'age', 30)
await redisCli.hgetall('friends') // { name : 'nyong', age : 30 }
// Set
await redisCli.sadd('fruits', 'apple', 'orange', 'pear', 'banana', 'apple')
await redisCli.smembers('fruits') // ['banana', 'apple', 'orange', 'pear'] // apple은 2개여서 중복제거
// Sorted Set
await redisCli.zadd('fruits', 1, 'apple', 5, 'orange', 3, 'pear', 4, 'banana', 8, 'grape')
await redisCli.zrange('fruits', 0, -1) // ['apple', 'pear', 'banana', 'orange', 'grape']
Redis Events 종류
connect | 클라이언트가 서버에 대한 연결이 될 경우 |
ready | 클라이언트가 서버에 대한 연결을 성공적으로 시작할 경우 |
end | 클라이언트가 .quit() 또는 .disconnect()를 통해 서버 연결을 끊었을 경우 |
error | 서버에 연결할 수 없거나 예기치 않게 연결이 끊어지는 등 네트워크 오류가 발생한 경우. |
reconnecting | 클라이언트가 서버에 다시 연결을 시도하고 있을 경우 |
redisClient.on('connect', () => {
console.info('Redis connected!');
});
redisClient.on('error', (err) => {
console.error('Redis Client Error', err);
});
Redis 트랜잭션(Transactions)
.multi() 를 호출한 다음 명령 메소드들을 체이닝으로 연결하여 트랜잭션을 구성할 수 있다.
트랜잭션이 완료되면 .exec()를 호출하고 결과가 포함된 배열을 다시 받게 된다.
await redisCli.set('another-key', 'another-value');
const [setKeyReply, otherKeyValue] = await redisCli
.multi()
.set('key', 'value')
.get('another-key')
.exec(); // ['OK', 'another-value']
인라인 Redis Command
만일 자바스크립트 패키지에서 지원하는 redis cli 명령어 api가 없을 경우, 다음과 같이 인라인으로 직접 지정해서 보낼수 있다. (orm의 인라인 쿼리 같은 개념)
await client.sendCommand(['SET', 'key', 'value', 'NX']); // 'OK'
await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2']
Redis 캐싱 전략 구현하기
Cache Aside 패턴 구현하기
app.get('/get', async (req, res) => {
let value = await redisCli.get(req.key); // redis get key 를 한다.
if (value) {
// 만약 redis(캐시 메모리)에 데이터가 있다면 그대로 반환 → Cache Hit
res.send(value);
} else {
// 만약 redis(캐시 메모리)에 데이터가 없다면 DB에서 조회 → Cache Miss
let data = await sequelize.query('SELECT data FROM tables where id = key');
await redisCli.set(key, data); // 그리고 그 데이터를 캐시에 저장하고 반환
res.send(value);
}
});
Cache Check 미들웨어 활용
보다 깔끔한 라우팅 코드를 원한다면, express의 미들웨어 함수로 다음과 같이 구현할수도 있다.
// 캐시 체크를 위한 미들웨어
const checkCache = (req, res, next) => {
let value = await redisCli.get(req.key); // redis get key 를 한다.
if (value) {
// Redis에 저장된게 존재하면 바로 클라이언트에 응답
res.send(data);
} else {
// Redis에 저장된게 없기 때문에 다음 미들웨어 실행
next();
}
};
app.get('/get', checkCache, async (req, res) => {
// 만약 redis(캐시 메모리)에 데이터가 없다면 DB에서 조회 → Cache Miss
let data = await sequelize.query(`SELECT data FROM tables where id = ${req.key}`);
await redisCli.set(key, data); // 그리고 그 데이터를 캐시에 저장하고 반환
res.send(value);
});
캐싱 기간 설정하기
캐시 메모리는 기본적으로 용량이 매우 적기 때문에, 데이터를 캐싱할때 항상 기간을 설정하여 자동 삭제되도록 설정해야 한다.
router.post('/set', (req, res, next) => {
await redisCli.set('username', 'inpa');
await redisCli.expire('username', 3600); // 3600초 후에 username 키 삭제
if(redisCli.ttl('username') < 300) {
// usename 키 남은 시간이 300초 이하 일 경우...
}
});
await redisCli.setex('username', 3600, 'inpa'); // setex 명령어로 키-밸류와 기간을 한번에 지정할 수도 있다.
노드에서 Session Store 사용하기
Redis 를 유용하게 활용할 수 있는 경우 또 하나는 session 이다.
일반적으로 express-session에서의 세션은 휘발성 인메모리 데이터베이스에 잠깐 저장되고 서버가 종료되면 사라지게 된다.
이때 redis db서버를 사용하여 세션관리를 하면 서버가 꺼져도 데이터가 유지되게 할수 있으며, 복수 서버 환경(클러스터링)에서도 세션을 공유할 수 있게 된다.
Connect-redis 설치 & 사용법
connect-redis는 멀티 프로세스 간 세션 공유를 위해 레디스와 익스프레스를 연결해주는 패키지이다.
기존에는 로그인할 때 express-session의 세션 아이디와 실제 사용자 정보가 서버 메모리에 저장된다.
싱글 서버를 사용할땐 문제가 없지만 pm2를 통해 노드 클러스터 서버 인프라를 구축했을때 세션 불일치 문제가 생긴다.
따라서 세션 불일치를 해결하기 위해 이 세션 데이터를 서버 메모리가 아닌 redis db에 저장하도록 코드를 구현해보자.
> npm i connect-redis
const express = require('express');
const cookieParser = require('cookie-parser');
const dotenv = require('dotenv');
const session = require('express-session');
const redis = require('redis');
const RedisStore = require('connect-redis')(session); // express-session 객체를 넣는다.
dotenv.config(); // env환경변수 파일 가져오기
const app = express();
//* Redis 연결
// redis[s]://[[username][:password]@][host][:port][/db-number]
const redisClient = redis.createClient({
url: `redis://${process.env.REDIS_USERNAME}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
legacyMode: true, // 반드시 설정 !! 설정 안하면 connect-redis 동작 안함
});
redisClient.on('connect', () => {
console.info('Redis connected!');
});
redisClient.on('error', (err) => {
console.error('Redis Client Error', err);
});
redisClient.connect().then(); // redis v4 연결 (비동기)
const redisCli = redisClient.v4; // 기본 redisClient 객체는 콜백기반인데 v4버젼은 프로미스 기반이라 사용
//* 세션 쿠키 미들웨어
app.use(cookieParser(process.env.COOKIE_SECRET));
const sessionOption = {
resave: false,
saveUninitialized: true,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
store: new RedisStore({ client: redisClient, prefix: 'session:' }), // 세션 데이터를 로컬 서버 메모리가 아닌 redis db에 저장하도록 등록
};
app.use(session(sessionOption));
createClient({ legacyMode: true })로 레거시 모드를 설정하지 않으면 connect-redis 모듈이 동작 안하니 주의
sessionOption.store는 세션을 어디에 저장할지 고르는 옵션 정보로, 기본값은 서버의 메모리 스토리지에 저장하는데, 이것을 RedisStore로 바꾸게 되면 세션 데이터를 Redis에 저장하게 된다.
즉, 재기동을 하여도 세션이 날라가는 문제가 발생하지 않도록 처리해줄 수 있다.
이제 서버를 실행해보고 개발자 도구의 Application 탭에 cookies 탭을 확인하면 다음과 같이 connect.sid 로 세션 쿠키가 서명되어 저장되어 있는 것을 볼 수 있다.
그리고 redis db를 확인해 보면 다음과 같이 session:세션ID 형태로 키가 저장 됨을 확인 할 수 있다.
값을 조회해 보면 세션 쿠키 객체가 문자열 형태로 들어있는 것을 확인 할 수 있다.
Node API 서버를 구축할때 사람들이 많이 들 쓰는 api 사용량을 제한하는 패키지 'express-rate-limit' 역시 사용량을 서버 메모리에 기록하므로 서버를 재시작하거나 다른 서버로 로드밸런싱 하면 사용량 기록이 초기화되게 된다.
따라서 이것도 redis에 기록하는 것이 좋다.
express-rate-limit 패키지와 rate-limit-redis 패키지를 같이 사용하면 된다.
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.