Infrastructure

Redis 캐시 구현하기: Node.js로 쉽게 따라하는 방법

Redis에 대해서

안녕하세요 Devloo 입니다 :). 이번 시간에는 Redis에 대해 자세히 탐구해보려고 합니다. 이 글에서는 Redis의 개념부터 Redis를 이용한 캐시 적용 방법, 간단한 설치법과 명령어 사용법까지 알아보겠습니다. 또한, Node.js에서 Redis를 활용하는 방법도 코드 예시와 함께 설명하겠습니다.

Redis에 대한 설명
Redis 설명 : 레디스의 자세한 설명

Redis에 대하여

Redis란 무엇인가?

Redis는 Remote Dictionary Server의 약자로, 모든 데이터를 주 메모리(RAM)에 저장하는 인메모리 데이터베이스입니다. RAM에 데이터를 저장하므로 디스크 검색이 필요한 다른 DBMS보다 접근 속도가 훨씬 빠릅니다. 또한, Redis는 NoSQL 데이터베이스로, 키-값 쌍을 저장하는 스키마 없는 데이터베이스입니다. Redis는 초기화 시간이 거의 없어서 애플리케이션 테스트를 빠르게 수행할 수 있고, 이는 개발 생산성을 향상시킵니다.

하지만 Redis는 휘발성 데이터베이스입니다. 시스템이 갑자기 중단되면 모든 데이터가 손실될 수 있습니다. 이를 방지하기 위해 Redis는 복제 기능을 제공하여 데이터 백업을 생성할 수 있습니다. Redis는 주로 애플리케이션 성능 향상을 위해 캐시로 사용되며, 자주 접근하는 데이터나 계산에 시간이 오래 걸리는 데이터를 저장하여 빠른 접근을 제공합니다.

Redis, 빠른 속도의 비밀

Redis는 단일 스레드로 설계되었지만 높은 성능을 자랑합니다. 그 이유를 설명해보면 아래와 같습니다:

  1. 인메모리 키-값 저장소이기 때문입니다. 메모리에 데이터를 저장하므로 읽기/쓰기 속도와 응답 시간이 빠릅니다.
  2. IO 멀티플렉싱을 사용하여 단일 스레드가 여러 소켓 연결을 동시에 대기할 수 있습니다. 이는 여러 CPU 코어를 활용하여 성능을 향상시킵니다.
  3. 효율적인 저수준 데이터 구조를 채택합니다. LinkedList, SkipList, HashTable 등 다양한 데이터 구조를 사용하여 메모리에 데이터를 효율적으로 저장합니다.
Redis가 빠른 이유
Redis가 빠른 이유

Redis 캐시의 작동 원리

클라이언트가 데이터를 요청하면 서버는 먼저 Redis Cache에서 해당 키를 찾습니다. 만약 Redis Cache에 키가 있다면 Cache Hit가 발생하고 사용자는 캐시된 데이터를 받게 됩니다. 만약 Redis Cache에 키가 없다면 Cache Miss입니다. 서버는 데이터베이스나 REST API를 통해 가장 최신 정보를 가져올 것입니다.

Redis 캐시의 작동 방법

Redis 사용 방법

Redis 설치하기 (Mac)
  1. Homebrew를 사용하여 설치 (Mac homebrew 설치법):
$ brew install redis
  1. Redis 실행:
$ brew services start redis<br>$ brew services stop redis<br>$ brew services restart redis
  1. Redis CLI 사용:
$ redis-cli

[Windows에서 설치하기]
링크 : https://kitty-geno.tistory.com/133

Redis 명령어

Redis 키(Keys) 명령어

Redis는 키에 대한 다양한 작업을 수행하기 위해 다음과 같은 기본 명령어를 제공합니다.

  • SET key value: 키-값 쌍을 설정합니다.
  • GET key: 주어진 키에 대한 값을 가져옵니다.
  • DEL key: 주어진 키를 삭제합니다.
  • EXISTS key: 키가 존재하는지 여부를 확인합니다.
  • KEYS pattern: 특정 패턴과 일치하는 모든 키를 찾습니다.
  • FLUSHALL: Redis 내부의 모든 데이터를 삭제합니다.
  • SETEX key seconds value: 주어진 시간(seconds) 후에 만료되는 키-값을 설정합니다.
  • TTL key: 키의 만료까지 남은 시간을 반환합니다.
(base) devloo@devlooui-MacBookPro ~ % redis-cli
127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> set key2 value2
OK
127.0.0.1:6379> set key3 value3
OK
127.0.0.1:6379> get key1
"value1"
127.0.0.1:6379> get key2
"value2"
127.0.0.1:6379> get key3
"value3"
127.0.0.1:6379> keys *
1) "key3"
2) "key2"
3) "key1"
127.0.0.1:6379> exists key3
(integer) 1
127.0.0.1:6379> del key3
(integer) 1
127.0.0.1:6379> exists key3
(integer) 0
127.0.0.1:6379> keys *
1) "key2"
2) "key1"
127.0.0.1:6379> setex key3 10 value3
OK
127.0.0.1:6379> keys *
1) "key3"
2) "key2"
3) "key1"
127.0.0.1:6379> ttl key3
(integer) 5
127.0.0.1:6379> ttl key3
(integer) 4
127.0.0.1:6379> ttl key3
(integer) 3
127.0.0.1:6379> ttl key3
(integer) 2
127.0.0.1:6379> ttl key3
(integer) 1
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> keys *
1) "key2"
2) "key1"
127.0.0.1:6379> flushall
OK
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379>
Redis 리스트(List) 명령어

Redis 리스트는 간단한 문자열 목록입니다. Redis 리스트에서는 목록의 처음이나 끝에 요소를 추가할 수 있습니다.

  • LPUSH key value: 배열의 가장 왼쪽 끝에 요소를 추가합니다.
  • RPUSH key value: 배열의 가장 오른쪽 끝에 요소를 추가합니다.
  • LRANGE key startIndex stopIndex: 시작 인덱스와 종료 인덱스 사이의 요소 목록을 표시합니다.
  • LPOP key: 배열의 가장 왼쪽 요소를 제거하고 반환합니다.
  • RPOP key: 배열의 가장 오른쪽 요소를 제거하고 반환합니다.
(base) devloo@devlooui-MacBookPro ~ % redis-cli
127.0.0.1:6379> lpush subjects math
(integer) 1
127.0.0.1:6379> lrange subjects 0 1
1) "math"
127.0.0.1:6379> lpush subjects astronomy
(integer) 2
127.0.0.1:6379> lrange subjects 0 2
1) "astronomy"
2) "math"
127.0.0.1:6379> rpush subjects humanity
(integer) 3
127.0.0.1:6379> lrange subjects 0 3
1) "astronomy"
2) "math"
3) "humanity"
127.0.0.1:6379> lpop subjects
"astronomy"
127.0.0.1:6379> lrange subjects 0 2
1) "math"
2) "humanity"
127.0.0.1:6379> rpop subjects
"humanity"
127.0.0.1:6379> lrange subjects 0 2
1) "math"
127.0.0.1:6379>
Redis 셋(Set) 명령어

Set은 배열과 달리 모든 요소가 고유합니다. 배열에서는 인덱스를 사용하여 요소를 검색할 수 있지만, Set에서는 키가 필요하므로 인덱스로 검색하는 것은 허용되지 않습니다. 배열은 삽입 순서를 유지하지만, Set은 순서가 없습니다. 즉, Set 내의 항목이 나타나는 순서를 예측할 수 없습니다. Set 명령어는 아래와 같습니다.

  • SADD key member: 집합에 멤버를 추가합니다.
  • SMEMBERS key: 주어진 집합의 멤버를 표시합니다.
  • SREM key member: 주어진 멤버를 집합에서 제거합니다.
(base) devloo@devloo-MacBookPro ~ % redis-cli
127.0.0.1:6379> sadd subjects math humanity astronomy chemistry
(integer) 4
127.0.0.1:6379> smembers subjects
1) "astronomy"
2) "humanity"
3) "chemistry"
4) "math"
127.0.0.1:6379> srem subjects chemistry
(integer) 1
127.0.0.1:6379> smembers subjects
1) "astronomy"
2) "humanity"
3) "math"
127.0.0.1:6379>
Redis 해쉬(Hash) 명령어

해싱은 단일 키 내에 키-값 쌍을 저장할 수 있게 합니다. 해시 명령어는 다음과 같습니다.

  • HSET key field value: 해시에 키-값 쌍을 설정합니다.
  • HGET key field: 해시 필드의 값을 가져옵니다.
  • HGETALL key: 해시의 모든 키-값 쌍을 가져옵니다.
  • HEDEL key field: 해시에서 주어진 필드를 삭제합니다.
  • HEXISTS key field: 해시 내에서 필드의 존재 여부를 확인합니다.
(base) devloo@devlooui-MacBookPro ~ % redis-cli
127.0.0.1:6379> hset address city Seattle
(integer) 1
127.0.0.1:6379> hset address state Washington
(integer) 1
127.0.0.1:6379> hset address country US
(integer) 1
127.0.0.1:6379> hget address state
"Washington"
127.0.0.1:6379> hgetall address
1) "city"
2) "Seattle"
3) "state"
4) "Washington"
5) "country"
6) "US"
127.0.0.1:6379> hexists address city
(integer) 1
127.0.0.1:6379> hdel address city
(integer) 1
127.0.0.1:6379> hexists address city
(integer) 0
127.0.0.1:6379> hgetall address
1) "state"
2) "Washington"
3) "country"
4) "US"
127.0.0.1:6379>

Node.js를 사용한 Redis 캐싱

GET 및 POST 엔드포인트가 있는 Express 애플리케이션을 생성합니다. 실습 예제에서는 외부의 가짜 REST API(jsonplaceholder.typicode.com/posts)를 사용하여 게시물 목록을 가져오고 새로운 게시물을 생성합니다.

아래 단계를 따라 애플리케이션을 설정하세요.

  1. npm init -y 명령어로 package.json 파일을 생성합니다.
  2. Express 앱에 필요한 모든 종속성을 다음과 같이 설치하세요:
    npm i express dotenv axios body-parser
  3. npm i ioredis를 사용하여 Redis 종속성을 설치하세요.
  4. package.json 파일은 다음과 같아야 합니다.
{
  "name": "redis-setup",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^1.1.3",
    "body-parser": "^1.20.1",
    "dotenv": "^16.0.3",
    "express": "^4.18.2",
    "ioredis": "^5.2.3"
  }
}

5. .env 파일에 Redis 호스트, 포트, TTL (Time-to-Live) 및 Timeout을 추가하세요. .env 파일에는 Redis 예제를 위한 기본 REST API URL도 포함됩니다. .env 파일의 내용은 아래와 같습니다.

REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_TTL=30
REDIS_TIMEOUT=5000
BASE_URL=https://jsonplaceholder.typicode.com/posts

6. index.js 파일 내에서 서버를 시작하세요.

require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const app = express();

app.use(bodyParser.json());

app.listen(8000, () => {
    console.log('server started!');
});

7. 다음 내용을 포함하는 caching.js 파일을 생성하세요.

  • hostport 및 commandTimeout으로 Redis 인스턴스를 생성합니다. 지정된 시간(밀리초) 내에 응답이 없으면 “command timed out” 오류가 발생합니다.
  • set() 메서드는 캐시 키-값 쌍을 설정하고 만료를 적용합니다. Redis 값은 항상 문자열이므로 데이터를 설정하기 전에 문자열로 변환해야 합니다.
  • get() 메서드는 키-값 쌍을 검색합니다.
  • del() 메서드는 캐시 키를 제거합니다.

아래는 caching.js의 예제 코드입니다.

const Redis = require("ioredis");
const { REDIS_HOST, REDIS_PORT, REDIS_TTL, REDIS_TIMEOUT } = process.env;

let redis;

// Redis 인스턴스 생성
(async () => {
    redis = new Redis({
        host: REDIS_HOST,
        port: REDIS_PORT,
        commandTimeout: REDIS_TIMEOUT
    });
    redis.on("error", (err) => {
        console.log(err);
    });
})();

// Redis 캐시에서 키 데이터 가져오기
async function getCache(key) {
    try {
        const cacheData = await redis.get(key);
        return cacheData;
    } catch (err) {
        return null;
    }
}

// 지정된 만료 시간으로 Redis 캐시 키 설정
function setCache(key, data, ttl = REDIS_TTL) {
    try {
        redis.set(key, JSON.stringify(data), "EX", ttl);
    } catch (err) {
        return null;
    }
}

// 주어진 Redis 캐시 키 제거
function removeCache(key) {
    try {
        redis.del(key);
    } catch (err) {
        return null;
    }
}

module.exports = { getCache, setCache, removeCache };
캐싱된 GET 메서드

외부 API에서 모든 게시물을 가져오기 위한 GET 엔드포인트인 /getAll을 생성합니다. 먼저 고유한 캐시 키를 선택해 값을 저장합니다. getCache(key) 메서드를 호출해 캐시 키에 이미 값이 있는지 확인합니다. 키가 있으면 캐시된 데이터를 사용자에게 제공하며, 이를 Cache hit라고 합니다. 키가 없으면 Cache miss로 간주하고 데이터를 REST API에서 가져와 setCache(key, data) 메서드를 호출해 값을 저장합니다. 아래 코드를 참조하여 GET 엔드포인트를 생성하세요.

require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const axios = require('axios');
const { getCache, setCache } = require('./caching');
const app = express();

const { BASE_URL } = process.env;
const cacheKey = `getAll/posts`;

// 미들웨어
app.use(bodyParser.json());

// 게시물 가져오기
app.get('/getAll', async (req, res, next) => {
    try {
        const response = {};
        const cacheData = await getCache(cacheKey);
        if (cacheData) {
            response['message'] = 'cache hit';
            response['posts'] = JSON.parse(cacheData);
        } else {
            const result = await axios.get(BASE_URL);
            const { data } = result;
            response['message'] = 'cache miss';
            response['posts'] = data;
            setCache(cacheKey, data);
        }
        res.status(200).send(response);
    } catch (err) {
        res.status(400).send(err);
    }
});

app.listen(8000, () => {
    console.log('server started!');
});
캐싱되기 전 GET 메서드의 결과

GET 엔드포인트를 테스트하기 위해 Postman 도구를 사용합니다. 결과를 가져오기 위해 GET 요청을 보냅니다. 아래 이미지는 캐싱하기 이전의 GET 게시물의 결과를 보여줍니다.

캐싱되기 전 GET 메서드 테스트 결과

Redis 캐시에 새로운 키가 추가될 것입니다. 우리는 Redis에서 새로 추가된 키를 확인하기 위해 다음과 같이 키를 검사할 수 있습니다.

127.0.0.1:6379> keys *
1)"getAll/posts"
127.0.0.1:6379

캐싱 후 GET 메서드 결과

Redis에 캐시 키가 설정된 후 다시 GET 요청을 보내서 결과를 가져옵니다. 데이터는 Redis 캐시에서 가져올 것입니다. 아래 이미지는 캐싱 후 GET posts의 Postman 결과를 보여줍니다.

캐싱 후 GET 메서드 테스트 결과

Postman 테스트 결과를 주의 깊게 관찰하면 성능 차이를 확인할 수 있습니다. Redis 캐싱을 사용한 후 응답 시간이 크게 줄어들게 됩니다.

캐싱된 POST 메서드

새로운 게시물을 외부 API에 추가하기 위한 POST 엔드포인트인 /create을 생성합니다. 데이터는 req.body 내에 전달됩니다. 새로운 게시물이 추가될 때 이전에 설정된 Redis 키가 삭제되어야 합니다. 이때 removeCache() 메서드가 이를 수행합니다. 이렇게 함으로써 데이터 일관성을 유지할 수 있습니다. 아래 코드를 참조하여 POST 엔드포인트를 생성하세요.

require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const axios = require('axios');
const { removeCache } = require('./caching');
const app = express();

const { BASE_URL } = process.env;
const cacheKey = `getAll/posts`;

// 미들웨어
app.use(bodyParser.json());

// 새로운 게시물 생성
app.post('/create', async (req, res, next) => {
    try {
        const response = await axios.post(BASE_URL, req.body);
        if (response) {
            const { data: posts } = response;
            removeCache(cacheKey);
            res.status(201).send(posts);
        }
    } catch (err) {
        res.status(400).send(err);
    }
});

app.listen(8000, () => {
    console.log('server started!');
});
POST 메서드의 결과

Postman 도구에서 POST 요청을 보냅니다. POST 요청의 본문은 JSON으로 전송됩니다. 아래 이미지는 게시물 생성 API의 결과를 보여줍니다.

POST 메서드의 테스트 결과

새로운 게시물을 생성하면 Redis 키가 삭제됩니다.

요약

지금까지 Node.js에서 Redis 캐싱을 활용하는 방법을 살펴보았습니다. Redis를 캐시로 사용하면 느린 하부 저장소에 접근할 필요가 줄어들어 데이터 검색 성능이 향상됩니다. 비록 Redis가 단일 스레드로 작동하지만, 여전히 매우 빠른 인메모리 데이터베이스입니다.

이번 시간에는 Redis에 대한 설명과 사용 방법, Node.js와 연동한 엔드포인트 구축까지 알아보았습니다.
끝까지 읽어주셔서 정말 감사합니다 ㅎㅎ (_ _) !
혹시 궁금하신 사항이 있으시면, 편하게 댓글 남겨주세요

Written by 개발자서동우
안녕하세요! 저는 기술 분야에서 활동 중인 개발자 서동우입니다. 명품 플랫폼 (주)트렌비의 창업 멤버이자 CTO로 활동했으며, AI 기술회사 (주)헤드리스의 공동 창업자이자 CTO로서 역할을 수행했습니다. 다양한 스타트업에서 일하며 회사의 성장과 더불어 비즈니스 상황에 맞는 기술 선택, 개발팀 구성 및 문화 정착에 깊은 경험을 쌓았습니다. 개발 관련 고민은 언제든지 편하게 연락주세요 :) https://linktr.ee/dannyseo Profile

Leave a Reply

Your email address will not be published. Required fields are marked *