Valkey: Redis 라이선스 변경 후 나온 오픈소스 포크, 호환성, Redis에서 옮기는 방법
이 글의 핵심
Valkey는 2024년 3월 Redis가 BSD에서 SSPL/RSALv2로 라이선스를 변경한 직후 Linux Foundation 주도로 출범한 오픈소스 포크입니다. Redis 7.2.4에서 갈라져 나와 BSD 3-Clause 라이선스를 유지하며 AWS·Google·Oracle·Ericsson·Snap 등이 개발에 참여합니다. 이 글은 Valkey를 고르는 이유와 Redis와의 차이, 그리고 Redis 7.4 이후 버전의 RDB 파일을 그대로 옮길 수 없는 문제처럼 마이그레이션에서 실제로 부딪히는 함정을 정리합니다.
왜 Valkey가 생겼나
2024년 3월 20일, Redis Ltd는 7.2.4 이후 버전의 라이선스를 BSD 3-Clause → SSPL + RSALv2로 변경한다고 발표했습니다. 이는 상용 “관리형 서비스” 제공을 원천 차단하는 라이선스로, 주된 타깃은 AWS ElastiCache 같은 클라우드 벤더입니다.
며칠 뒤 Linux Foundation이 Valkey 프로젝트를 발표했고, 기존 Redis 오픈소스 핵심 기여자들과 AWS·Google·Oracle·Ericsson·Snap 등이 참여했습니다. 2024년 4월 첫 릴리스인 Valkey 7.2.5가 나왔고, 9월에는 I/O 스레드를 개선한 Valkey 8.0이 GA되었으며 이후에도 새 메이저 버전이 이어지고 있습니다.
이 배경이 실무에 주는 의미는 두 가지입니다. 첫째, “Redis를 쓰면 라이선스 위반인가”라는 질문의 답은 대부분 아니오입니다. SSPL과 RSALv2가 막으려는 것은 Redis를 관리형 서비스로 재판매하는 경우이고, 회사 내부 시스템의 캐시로 쓰는 것은 대상이 아닙니다. 다만 사내 오픈소스 정책이 SSPL이나 AGPL을 금지하는 조직이 많아서, 법무 검토 부담을 없애려고 BSD 라이선스인 Valkey로 옮기는 경우가 흔합니다. 둘째, 2024년 이후 두 프로젝트는 서로 다른 방향으로 발전하고 있습니다. 포크 시점의 기능은 같지만 이후 추가된 명령과 파일 포맷은 호환되지 않을 수 있으므로, “Redis 대신 Valkey”는 버전에 따라 단순 교체가 아닐 수 있습니다.
Valkey vs Redis: 기능 비교
| 항목 | Valkey 8 | Redis 7.4 (SSPL) | Redis 8 (AGPL/SSPL) |
|---|---|---|---|
| 라이선스 | BSD 3-Clause | SSPL + RSALv2 | AGPLv3 / SSPL / RSALv2 중 선택 |
| RESP 프로토콜 | 동일 | 동일 | 동일 |
| 클러스터 | 동일 | 동일 | 동일 |
| 멀티스레드 I/O | 강화 (async) | 기본 | 강화 |
| JSON 모듈 | valkey-json | RedisJSON (별도 모듈) | 코어에 포함 |
| Search 모듈 | valkey-search | RediSearch (별도 모듈) | 코어에 포함 |
| Bloom/TimeSeries | valkey-bloom 등 별도 | RedisBloom (별도 모듈) | 코어에 포함 |
| RDB 포맷 | 포크 시점(7.2) 계열 | 7.4에서 새 포맷 | 7.4 이후 포맷 |
| 클라우드 관리형 | AWS/GCP/Azure 지원 확장 | Redis Cloud 중심 | Redis Cloud 중심 |
설치
Docker
docker run --name valkey -p 6379:6379 -d valkey/valkey:8
docker exec -it valkey valkey-cli
Linux (Ubuntu)
sudo apt update
sudo apt install valkey-server valkey-tools
sudo systemctl enable --now valkey-server
valkey-cli PING
Homebrew
brew install valkey
brew services start valkey
패키지 이름과 설정 파일 위치는 배포판마다 다를 수 있습니다. Ubuntu·Debian 계열은 최근 릴리스부터 공식 저장소에 Valkey가 포함되었지만 오래된 LTS에는 없을 수 있고, 일부 배포판은 redis-server 패키지 이름으로 Valkey를 제공하기도 합니다. 설치 뒤 valkey-server --version으로 실제 버전을 확인하세요.
Kubernetes (Helm)
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install cache bitnami/valkey \
--set architecture=replication \
--set auth.password=strong-password \
--set replica.replicaCount=2 \
--set sentinel.enabled=true
Bitnami 차트는 이미지 배포 정책이 바뀐 적이 있으므로, 사용할 차트와 이미지가 계속 업데이트되는지 먼저 확인하는 것이 좋습니다. Sentinel을 켠 구성에서는 애플리케이션이 고정 주소가 아니라 Sentinel에 현재 마스터를 물어보는 클라이언트 설정(ioredis의 sentinels 옵션 등)을 써야 페일오버 후에도 새 마스터로 연결됩니다. 서비스 하나의 주소로만 연결하면 페일오버 뒤 읽기 전용 레플리카에 쓰기를 보내 READONLY You can't write against a read only replica. 오류가 납니다.
CLI: Redis와 동일
valkey-cli
> SET user:1 "JB"
OK
> GET user:1
"JB"
> INCR counter
(integer) 1
> EXPIRE user:1 3600
(integer) 1
> TTL user:1
(integer) 3600
> INFO server
# Server
valkey_version:8.0.1
...
redis-cli 대신 valkey-cli만 바꾸면 됩니다. redis-cli도 여전히 동작합니다(동일 프로토콜). 다만 INFO server의 필드가 valkey_version으로 바뀌었고, 호환성을 위해 redis_version 필드도 함께 보고합니다. 모니터링 스크립트나 운영 도구가 redis_version 값으로 기능을 판단한다면, Valkey가 호환용으로 보고하는 값(7.2.4)이 실제 Valkey 버전과 다르다는 점을 알아 두어야 합니다.
클라이언트 라이브러리
모든 기존 Redis 클라이언트가 그대로 동작합니다.
Node.js (ioredis)
import Redis from "ioredis"
const redis = new Redis({
host: "valkey.internal",
port: 6379,
password: process.env.VALKEY_PASSWORD,
})
await redis.set("key", "value", "EX", 60)
const val = await redis.get("key")
Python (redis-py)
import redis
r = redis.Redis(host="valkey.internal", port=6379, password="...", decode_responses=True)
r.set("key", "value", ex=60)
print(r.get("key"))
Go (go-redis)
rdb := redis.NewClient(&redis.Options{
Addr: "valkey.internal:6379",
})
rdb.Set(ctx, "key", "value", time.Minute)
네이티브 Valkey 클라이언트도 개발 중이지만 기존 Redis 클라이언트로 충분합니다.
주요 사용 패턴
세션 저장소
await redis.setex(`session:${token}`, 3600, JSON.stringify({ userId: 42 }))
const session = JSON.parse(await redis.get(`session:${token}`) ?? "null")
레이트 리미트
const key = `rate:${userId}`
const count = await redis.incr(key)
if (count === 1) await redis.expire(key, 60)
if (count > 100) throw new Error("Rate limit exceeded")
캐시 (Cache-Aside)
async function getUser(id: string) {
const cached = await redis.get(`user:${id}`)
if (cached) return JSON.parse(cached)
const user = await db.users.findById(id)
await redis.setex(`user:${id}`, 600, JSON.stringify(user))
return user
}
Pub/Sub
const sub = new Redis()
await sub.subscribe("events")
sub.on("message", (channel, msg) => console.log(channel, msg))
const pub = new Redis()
await pub.publish("events", JSON.stringify({ type: "hi" }))
Streams (메시지 큐)
await redis.xadd("orders", "*", "userId", "42", "amount", "100")
const entries = await redis.xread("COUNT", 10, "STREAMS", "orders", "0")
레이트 리미트 예제의 INCR 후 EXPIRE는 두 명령 사이에 프로세스가 죽으면 만료 시간이 없는 카운터가 남아, 그 사용자는 영원히 제한에 걸립니다. 실무에서는 두 명령을 MULTI/파이프라인이나 Lua 스크립트로 묶거나, SET key 0 EX 60 NX 후 INCR처럼 만료를 먼저 보장하는 순서로 작성합니다.
단일 인스턴스 락 (토큰 확인 해제)
const lockKey = "lock:resource"
const token = crypto.randomUUID()
const acquired = await redis.set(lockKey, token, "PX", 30000, "NX")
if (acquired) {
try {
// critical section
} finally {
await redis.eval(`
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end`, 1, lockKey, token)
}
}
토큰을 비교한 뒤 지우는 Lua 스크립트를 쓰는 이유는, 락이 만료되어 다른 프로세스가 새 락을 잡은 뒤에 원래 프로세스가 DEL로 남의 락을 지우는 사고를 막기 위해서입니다. 이 예제는 인스턴스 하나에서의 락이며, 여러 독립 인스턴스에 락을 거는 Redlock 알고리즘과는 다릅니다. 레플리카 페일오버 중에는 마스터에 잡힌 락이 복제되기 전에 사라질 수 있으므로, 락이 깨지면 데이터가 손상되는 작업이라면 DB의 행 잠금이나 버전 비교 같은 장치를 함께 둬야 합니다.
프로덕션 설정
valkey.conf 요약
bind 0.0.0.0
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
requirepass your-strong-password
# 메모리
maxmemory 8gb
maxmemory-policy allkeys-lru
# 영속성 (AOF + RDB 혼합 권장)
appendonly yes
appendfsync everysec
save 900 1
save 300 10
save 60 10000
# 멀티스레드 I/O (Valkey 8에서 구조가 바뀌어 읽기도 기본으로 I/O 스레드가 처리)
io-threads 4
# 복제
replica-read-only yes
replica-serve-stale-data yes
# 로그
loglevel notice
bind 0.0.0.0은 모든 네트워크 인터페이스에서 연결을 받겠다는 뜻이므로, 반드시 requirepass나 ACL, 방화벽과 함께 써야 합니다. protected-mode yes는 비밀번호 없이 외부 주소에 바인딩된 경우 외부 연결을 거부하는 안전장치인데, 비밀번호를 설정하면 이 보호는 사실상 비밀번호에 맡겨집니다. 영속성을 켠 인스턴스의 maxmemory는 물리 메모리보다 넉넉히 작게 잡아야 합니다. RDB 저장과 AOF 재작성이 fork()로 자식 프로세스를 만드는데, 쓰기가 많은 동안에는 copy-on-write 때문에 메모리가 순간적으로 크게 늘어 OOM이 날 수 있기 때문입니다.
복제 + 센티넬 (고가용성)
Master 1 + Replica 2 + Sentinel 3의 전형적 구조. Sentinel이 master 장애 시 자동 페일오버.
# Sentinel 설정
sentinel monitor mymaster 192.168.1.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
Cluster (샤딩)
valkey-cli --cluster create \
10.0.0.1:6379 10.0.0.2:6379 10.0.0.3:6379 \
10.0.0.4:6379 10.0.0.5:6379 10.0.0.6:6379 \
--cluster-replicas 1
16384개 슬롯이 3 마스터에 분산되고 각 마스터마다 1 레플리카.
Redis → Valkey 마이그레이션
시나리오 1: 인플레이스 교체
Valkey는 Redis 7.2와 바이너리 호환이므로 가장 간단:
# 기존 Redis 중지 후 Valkey로 교체
systemctl stop redis-server
apt install valkey-server
# 데이터 디렉터리(/var/lib/redis → /var/lib/valkey) 복사 또는 설정 조정
systemctl start valkey-server
Redis 7.2 이하에서 옮기는 경우 AOF/RDB 파일을 그대로 로드합니다.
여기서 가장 흔한 함정은 원본 Redis의 버전입니다. Redis 7.4 이상은 포크 이후 새 RDB 포맷을 쓰기 때문에, 그 RDB 파일을 Valkey로 가져가면 Valkey 버전에 따라 시작 로그에 Can't handle RDB format version 12 같은 오류가 나며 데이터를 읽지 못합니다. 같은 이유로 Redis 7.4 이상의 마스터에 Valkey를 레플리카로 붙이는 방식도 실패합니다. 이 경우에는 파일을 그대로 옮기는 대신, 애플리케이션 수준에서 키를 읽어 다시 쓰는 복사 도구(redis-shake 같은 마이그레이션 도구, 또는 SCAN+DUMP/RESTORE를 쓰는 스크립트)를 쓰거나, 캐시 용도라면 Valkey를 빈 상태로 시작해 자연스럽게 다시 채우는 것이 현실적입니다. 또 Redis 7.4 이후에 추가된 명령을 애플리케이션이 쓰고 있다면 Valkey에서는 ERR unknown command로 실패하므로, 전환 전에 사용 중인 명령 목록(COMMANDSTATS)을 확인해 두세요.
인플레이스 교체 시에는 설정 파일 경로(/etc/redis/redis.conf → /etc/valkey/valkey.conf)와 데이터 디렉터리, 파일 소유자(redis → valkey 사용자)가 바뀌는 경우가 많습니다. 데이터를 복사한 뒤 소유권을 바꾸지 않으면 서비스가 Permission denied로 시작하지 못하므로, systemd 로그(journalctl -u valkey-server)를 먼저 확인합니다.
시나리오 2: 블루그린 (다운타임 최소)
- Valkey 인스턴스를 기존 Redis의 복제본으로 연결 (Valkey에서
REPLICAOF old-redis 6379실행, 원본이 Redis 7.2 이하일 때) - 복제 완료 대기 (
INFO replication에서master_link_status:up, 오프셋 차이 확인) - 애플리케이션을 Valkey로 전환한 뒤 Valkey에서
REPLICAOF NO ONE으로 독립 마스터로 승격 - 기존 Redis 종료
전환 순간에는 짧게라도 두 곳에 쓰기가 섞이지 않도록 해야 합니다. 복제 중인 Valkey는 읽기 전용이므로 먼저 전환된 애플리케이션 인스턴스는 쓰기에 실패하고, 반대로 승격을 너무 빨리 하면 아직 Redis에 쓰고 있는 인스턴스의 변경이 유실됩니다. 트래픽이 적은 시간에 쓰기를 잠깐 멈추거나, 캐시 용도라면 짧은 불일치를 감수하는 식으로 전환 절차를 미리 정해 두는 것이 좋습니다.
시나리오 3: ElastiCache 교체 (AWS)
aws elasticache create-replication-group \
--replication-group-id myapp-valkey \
--replication-group-description "Valkey replacement" \
--engine valkey \
--engine-version 8.0 \
--cache-node-type cache.r7g.large \
--num-cache-clusters 2 \
--automatic-failover-enabled
기존 Redis ElastiCache와 동일한 API·VPC·보안그룹 설정이 가능합니다. Terraform에서는 aws_elasticache_replication_group의 engine을 "valkey"로 바꾸게 되는데, 엔진·버전과 함께 파라미터 그룹 계열(family)도 Valkey용으로 바꿔야 하고, 변경 방식에 따라 클러스터가 교체(재생성)되는 계획이 나올 수 있으므로 terraform plan 결과를 반드시 확인해야 합니다. AWS는 기존 Redis OSS 클러스터를 Valkey로 제자리 업그레이드하는 경로도 제공하므로, 새 클러스터를 만드는 방식과 업그레이드 방식 중 다운타임 요구에 맞는 쪽을 고릅니다.
모니터링
핵심 메트릭
used_memory/used_memory_rss: 메모리 사용량connected_clients: 활성 연결 수instantaneous_ops_per_sec: 초당 처리keyspace_hits/keyspace_misses: 캐시 히트율evicted_keys: 메모리 부족으로 제거된 키 (순수 캐시에서는 정상적인 동작이지만, 세션 등 원천 데이터가 섞인 인스턴스라면 0이어야 함)- 복제 지연: 마스터의
master_repl_offset과 레플리카의 오프셋 차이 (INFO replication) cluster_state(클러스터): ok/fail
Prometheus exporter
docker run -d --name valkey-exporter \
-p 9121:9121 \
oliver006/redis_exporter \
--redis.addr=redis://valkey.internal:6379 \
--redis.password=$VALKEY_PASSWORD
기존 redis_exporter가 Valkey도 완벽 지원합니다. Grafana 대시보드도 Redis 대시보드 재사용.
성능 튜닝
maxmemory-policy: 순수 캐시라면allkeys-lru(또는allkeys-lfu), 세션처럼 지워지면 안 되는 데이터가 섞여 있다면 TTL 있는 키만 희생시키는volatile-*나 인스턴스 분리를 검토- Pipelining: 여러 명령을 한 번에 보내 왕복 지연을 줄임. 네트워크 지연이 클수록 효과가 크지만, 한 번에 너무 많이 보내면 서버의 다른 요청이 밀림
- Lua scripting: 여러 명령을 원자적으로, 네트워크 왕복 최소화
- 큰 값 회피: 단일 키에 수 MB 저장 시 블로킹. 100KB 이상은 구조 변경 고려
SCAN사용:KEYS는 운영 중 금지 — 블로킹- Persistence 설정: 캐시 전용이면
appendonly no+save ""로 디스크 I/O 제거 가능
보안
requirepass강한 비밀번호- ACL:
ACL SETUSER reader on >readpass ~* &* +@read로 읽기 전용 계정 bind 127.0.0.1+ 내부 네트워크만 허용- TLS 활성:
tls-port 6379, 인증서 설정 - Redis 익스플로잇(SSRF 등)은 Valkey에도 동일 적용 → 절대 공개 노출 금지
트러블슈팅
”WRONGTYPE” 오류
같은 키에 다른 타입(예: STRING 위에 LPUSH) 작업. TYPE key로 확인.
메모리 급증
DEBUG OBJECT key로 큰 키 찾기valkey-cli --bigkeys로 전체 스캔MEMORY USAGE key로 개별 키 크기
복제 지연
- 네트워크 대역폭 확인
repl-backlog-size증가- Master의
client-output-buffer-limit확인
AOF 파일 거대화
BGREWRITEAOF로 수동 재작성auto-aof-rewrite-percentage 100·auto-aof-rewrite-min-size 64mb
클러스터 운영 팁
- 마스터는 최소 3개: 레플리카 승격은 마스터 과반의 동의로 이뤄지므로, 마스터가 3개 미만이면 하나만 죽어도 자동 페일오버가 되지 않음
- 물리 분리: 마스터·레플리카를 다른 AZ/rack에
- 재분산(reshard): 노드 추가 후
valkey-cli --cluster reshard - Hash tag: 특정 키들을 같은 슬롯에 두려면
{user:1}:profile,{user:1}:orders - 멀티 키 명령 주의:
MGET·MSET·트랜잭션처럼 여러 키를 다루는 명령은 모든 키가 같은 슬롯이어야 하며, 아니면CROSSSLOT오류. 파이프라인은 클러스터 지원 클라이언트가 노드별로 나눠 보내므로 사용 가능
마무리
Valkey는 Redis 라이선스 변경에 대한 오픈소스 커뮤니티의 대응으로 시작해, Linux Foundation 거버넌스와 주요 클라우드 벤더의 참여로 자리를 잡았습니다. Redis 7.2 이하를 캐시나 세션 저장소로 쓰고 있다면 클라이언트 코드를 거의 바꾸지 않고 옮길 수 있고, BSD 라이선스라 사내 법무 검토 부담도 적습니다. 반대로 Redis 7.4 이후의 새 명령이나 RDB 포맷, Redis의 JSON·Search 기능에 의존하고 있다면 단순 교체가 아니므로, 사용 중인 명령과 데이터 이전 방법을 먼저 확인한 뒤 전환 계획을 세우는 것이 좋습니다. 전환 자체는 설정 몇 줄로 끝나는 경우가 많고, 문제는 주로 “원본이 몇 버전이었는가”와 “모니터링 도구가 무엇을 기준으로 버전을 판단하는가” 같은 주변부에서 생깁니다.