Redis는 캐싱, 요청 제한(rate limiting) 등 다양한 프로젝트에서 널리 사용되는 대표적인 인메모리 데이터베이스입니다.
이 글에서는 Redis를 인메모리 데이터베이스로 활용하는 방법, Redis를 사용해야 하는 이유, 그리고 반드시 알아야 할 핵심 기능들을 살펴봅니다. 그럼 시작해 보겠습니다.
인메모리 데이터베이스란 무엇일까?
전통적인 데이터베이스는 접근 속도를 높이기 위해 데이터베이스의 일부(주로 자주 조회되는 '핫(hot)' 인덱스)를 메모리에 올려두고, 나머지 데이터는 디스크에 저장합니다.
반면 Redis는 지연 시간(latency)을 최소화하고 데이터를 빠르게 저장하고 조회하는 것에 집중합니다. 그래서 SSD나 HDD 같은 저장 장치가 아닌 메모리(RAM) 위에서 전부 동작합니다. 속도가 곧 생명인 셈이죠!
Redis는 키-값(key-value) 방식의 데이터베이스입니다. 하지만 단순한 데이터베이스라고 생각하면 오산입니다. 키와 값을 저장하고 조회할 수 있는 방법이 무척 다양하기 때문입니다.
왜 Redis가 필요할까?
Redis는 여러 방식으로 활용할 수 있지만, 크게 두 가지 이유를 꼽을 수 있습니다.
- 애플리케이션 코드 레이어를 상태 비저장(stateless)으로 만들고 싶을 때 — 코드가 상태를 갖지 않으면 수평 확장(horizontal scaling)이 가능해집니다. Redis를 중앙 저장소로 사용하고, 코드는 순수하게 로직 처리에만 집중하게 만들 수 있습니다.
- 여러 애플리케이션이 데이터를 공유해야 할 때 — 예를 들어 누군가
payments.codedamn.com사이트에 무차별 대입 공격(bruteforce)을 시도하는 것을 감지했다면,login.codedamn.com에서도 해당 사용자를 차단하고 싶을 겁니다. Redis를 사용하면 서로 분리되어 있거나 느슨하게 연결된 여러 서비스가 하나의 공유 메모리 공간을 함께 사용할 수 있습니다.
Redis는 알아야 할 명령어가 많지 않아 배우기가 비교적 쉽습니다. 이제 Redis의 핵심 개념과 유용한 기본 명령어들을 살펴보겠습니다.
Redis CLI
Redis에는 REPL 방식으로 동작하는 CLI(명령줄 인터페이스)가 내장되어 있습니다. 입력하는 모든 내용이 즉시 평가되고 결과를 확인할 수 있습니다.
CLI에서 간단히 PING 명령어로 'hello world'를 실행해 볼 수 있습니다. 애플리케이션에서 Redis를 다루다가 특정 키의 값이나 서버 상태를 빠르게 확인해야 할 때 이 REPL 환경이 매우 유용합니다.
자주 사용하는 Redis 명령어
Redis의 동작 방식을 이해하는 데 도움이 되는 가장 흔히 쓰이는 명령어들을 소개합니다.
SET
SET은 Redis에서 키에 값을 설정하는 명령어입니다.
SET mehul "developer from india"
위 명령은 mehul이라는 키에 developer from india라는 값을 설정합니다.
GET
GET은 설정해 둔 키의 값을 조회합니다.
GET mehul
앞서 설정한 값인 "developer from india" 문자열이 반환됩니다.
SETNX
SETNX는 해당 키가 존재하지 않을 때만 값을 설정합니다. 이미 존재하는 키의 값을 실수로 덮어쓰는 상황을 방지할 수 있어 다양한 용도로 활용됩니다.
SET key1 value1
SETNX key1 value2
SETNX key2 value2
위 예제를 실행하면 key1은 value1, key2는 value2를 갖게 됩니다. 두 번째 명령은 key1이 이미 존재하기 때문에 아무 효과가 없기 때문입니다.
MSET
MSET은 SET과 비슷하지만 한 번의 명령으로 여러 키를 동시에 설정할 수 있습니다.
MSET key1 "value1" key2 "value2" key3 "value3"
지금은 편의상 키와 값에 key, value라는 접두사를 사용했지만, 실제로 이렇게 긴 명령어를 작성하다 보면 어디까지가 키이고 어디부터가 값인지 헷갈리기 쉽습니다.
그래서 좋은 습관 하나를 소개합니다. 값(value)은 항상 큰따옴표로 감싸고, 키(key)는 따옴표 없이 작성하세요(키 이름이 유효하다면). 이렇게 하면 가독성이 크게 향상됩니다.
MGET
MGET은 GET과 유사하지만 여러 값을 한 번에 반환할 수 있습니다.
MGET key1 key2 key3 key4
이 명령은 네 개의 값을 배열 형태로 반환합니다: value1, value2, value3, 그리고 null. key4는 한 번도 설정하지 않았기 때문에 null이 반환됩니다.
DEL
DEL은 키를 삭제하는 명령어입니다. 아주 간단하죠?
SET key value
GET key # "value" 반환
DEL key
GET key # null 반환
INCR과 DECR
이 두 명령어는 숫자 타입의 키 값을 증가시키거나 감소시킵니다. 매우 유용해서 실무에서도 자주 사용됩니다. Redis가 내부적으로 '키 조회(GET)'와 '키+1 저장(SET)'이라는 두 연산을 한 번에 처리해 주기 때문입니다.
덕분에 부모 애플리케이션과의 불필요한 왕복 통신(roundtrip)을 줄일 수 있고, 트랜잭션을 사용하지 않고도 안전하게 연산을 수행할 수 있습니다(트랜잭션은 뒤에서 다룹니다).
SET favNum 10
INCR favNum # 11
INCR favNum # 12
DECR favNum # 11
EXPIRE
EXPIRE 명령어는 키에 만료 시간을 설정합니다. 엄밀히 말하면 타이머라기보다는 '만료 시점(timestamp)'을 지정하는 것이며, 그 시점이 지나면 키는 다시 설정되기 전까지 항상 null을 반환합니다.
SET bitcoin 100
EXPIRE bitcoin 10
GET bitcoin # 100
# 10초 후
GET bitcoin # null
EXPIRE를 사용하면 만료 시각 정보까지 함께 저장해야 하므로 키 하나당 약간 더 많은 메모리를 사용합니다. 하지만 이 정도 오버헤드는 걱정할 필요가 없습니다.
TTL
TTL 명령어는 키가 만료되기까지 남은 시간을 확인할 때 사용합니다.
SET bitcoin 100
TTL bitcoin # -1
TTL somethingelse # -2
EXPIRE bitcoin 5
# 2초 대기
TTL bitcoin # 3 반환
# 1초 후
GET bitcoin # null
TTL bitcoin # -2
이 코드에서 알 수 있는 TTL의 반환 규칙은 다음과 같습니다.
- 키가 존재하지만 만료 시간이 설정되지 않은 경우
-1을 반환합니다. - 키가 존재하지 않는 경우
-2를 반환합니다. - 키가 존재하고 만료 시간이 설정된 경우 남은 시간(초)을 반환합니다.
SETEX
SETEX를 사용하면 SET과 EXPIRE를 한 번에 수행할 수 있습니다.
SETEX key 10 value
여기서 키는 "key", 값은 "value", 만료 시간(TTL)은 10초입니다. 이 키는 10초 후 자동으로 삭제됩니다.
이제 Redis 기본 명령어와 CLI 사용법에 대한 기초 지식을 갖추었으니, 실제 프로젝트 두 개를 만들며 배운 내용을 적용해 보겠습니다.
프로젝트 1 – Redis로 API 캐싱 시스템 구축하기
이 프로젝트는 Redis를 활용해 API 캐싱 시스템을 구축하는 것입니다. 외부(3rd party) 서버에서 받아온 결과를 일정 시간 동안 캐시에 저장해 두고 재사용하는 방식입니다.
이렇게 하면 외부 API의 호출 횟수 제한(rate limit)에 걸리지 않을 수 있습니다. 또한 캐싱은 사이트 응답 속도를 향상시키므로, 올바르게 구현하면 모두에게 윈윈(win-win)입니다.
Node.js를 사용하면 브라우저 안에서 인터랙티브하게 이 프로젝트를 직접 만들어 볼 수 있습니다. 직접 구현하지 않고 핵심 로직만 확인하고 싶다면, Node.js에서 동작하는 핵심 코드는 다음과 같습니다.
app.post('/data', async (req, res) => {
const repo = req.body.repo
const value = await redis.get(repo)
if (value) {
// 캐시 히트(cache hit) 발생
res.json({
status: 'ok',
stars: value
})
return
}
const response = await fetch(`https://api.github.com/repos/${repo}`).then((t) => t.json())
if (response.stargazers_count != undefined) {
await redis.setex(repo, 60, response.stargazers_count)
}
res.json({
status: 'ok',
stars: response.stargazers_count
})
})
이 코드의 동작 흐름을 살펴보겠습니다.
- 먼저 요청으로 전달된
repo(예:facebook/react형식)를 Redis 캐시에서 조회합니다. 값이 있다면 GitHub 서버에 다시 요청할 필요 없이 캐시된 스타 수를 바로 반환합니다. - 캐시에 값이 없다면 GitHub 서버에 요청을 보내 스타 수를 가져옵니다. 이때 스타 수가 undefined가 아닌지 확인합니다(레포지토리가 존재하지 않거나 비공개일 수 있기 때문). 값이 있다면
setex로 60초의 만료 시간을 두고 캐시에 저장합니다. - 만료 시간을 설정하는 이유는 오래된(stale) 데이터를 계속 제공하지 않기 위함입니다. 덕분에 최소 1분에 한 번은 최신 스타 수로 갱신됩니다.
프로젝트 2 – Redis로 API 요청 제한(Rate Limiting) 구현하기
이 프로젝트는 특정 엔드포인트에 대한 요청 횟수를 제한하여 악의적인 사용자로부터 API를 보호하는 방법을 다룹니다.
로그인이나 민감한 정보를 다루는 API 엔드포인트에서 특히 유용합니다. 한 사람이 수천 건의 요청을 몰아서 보내는 상황을 막을 수 있기 때문입니다.
이번 실습에서는 IP 주소 기준으로 요청을 제한합니다. 핵심 로직은 다음과 같습니다.
app.post('/api/route', async (req, res) => {
// 여기에 데이터 추가
const ip = req.headers['x-forwarded-for'] || req.ip
const reqs = await redis.incr(ip)
await redis.expire(ip, 2)
if (reqs > 15) {
return res.json({
status: 'rate-limited'
})
} else if (reqs > 10) {
return res.json({
status: 'about-to-rate-limit'
})
} else {
res.json({
status: 'ok'
})
}
})
이 코드 블록을 단계별로 이해해 보겠습니다.
x-forwarded-for헤더에서 클라이언트 IP를 추출합니다(express를 사용 중이므로req.ip를 사용해도 됩니다).- IP 주소를 키로 하는 값을
INCR로 증가시킵니다. Redis에 해당 키가 존재하지 않으면 INCR은 자동으로 0으로 초기화한 뒤 증가시키므로, 결국 1이 됩니다. - 키의 만료 시간을 2초로 설정합니다. 실무에서는 더 긴 값을 사용하는 것이 일반적이지만, 여기서는 실습 조건에 맞춘 것입니다.
- 마지막으로 요청 횟수를 확인하여 임계치를 초과하면 요청이 메인 함수 본문에 도달하지 못하도록 차단합니다.
Redis에 대해 더 알아야 할 것들
Redis는 지금까지 배운 내용보다 훨씬 방대한 기능을 갖춘 도구입니다. 다행히 지금 배운 것만으로도 충분히 실무에 바로 적용할 수 있습니다! 이 섹션에서는 Redis의 몇 가지 추가 기본 개념을 짚어 보겠습니다.
Redis는 싱글 스레드(single-threaded)
Redis는 멀티 코어, 멀티 스레드를 지원하는 시스템에서도 싱글 스레드 프로세스로 동작합니다. 이는 성능상의 약점이 아니라, 멀티 스레드 환경에서 발생할 수 있는 비일관성 있는 읽기/쓰기를 방지하기 위한 안전장치입니다.
만약 Redis가 멀티 스레드였다면, 단일 키에 대한 접근 시 스레드 안전성을 보장하기 위해 결국 어떤 형태의 락(locking) 메커니즘을 도입해야 했을 것입니다. 그런데 그런 락 기반 접근은 오히려 싱글 스레드의 순차 처리보다 성능이 떨어질 가능성이 높습니다.
Redis 트랜잭션
물론 Redis의 모든 작업을 단일 명령어로 처리할 수는 없습니다. 하지만 여러 명령어로 이루어진 블록을 한 번에 실행하도록 요청할 수는 있습니다(즉, 해당 블록이 실행되는 동안 다른 클라이언트는 Redis에 접근할 수 없습니다). 이때 MULTI 명령어를 사용합니다.
MULTI
SET hello world
SET yo lo
SET number 1
INCR number
EXPIRE hello 10
EXPIRE yo 5
EXEC
이렇게 하면 모든 연산이 한 번에 수행됩니다. 즉, MULTI 이후의 명령들은 즉시 실행되지 않고, EXEC 키워드를 만나는 순간 전부 한꺼번에 실행됩니다.
또한 Redis는 고급 사용 사례를 위해 리스트(list)와 셋(set) 자료구조도 지원합니다. 채널에 publish(발행)하면 해당 채널을 subscribe(구독) 중인 클라이언트들이 알림을 받는 브로드캐스팅 서비스로도 활용할 수 있습니다. 멀티 클라이언트 아키텍처에서 매우 유용한 기능입니다.
마무리
Redis 입문 가이드가 도움이 되었기를 바랍니다. 이 글에서 다룬 개념들을 단순히 읽는 데 그치지 말고, 브라우저에서 직접 실습하며 체득해 보세요. Redis + Node.js 캐싱 관련 인터랙티브 강의도 많으니 참고해 보시길 추천합니다.
직접 강의를 경험해 보고 피드백을 남겨주세요. 여러분의 의견은 언제나 환영합니다 :)