최근 Valkey 강의를 보면서 Redis와 Valkey의 구조, 자료구조, 그리고 내부 프로토콜에 대해 정리해보고 있다.
이전에는 Redis를 사용할 때 대부분 클라이언트 라이브러리를 통해 set, get, zadd 같은 명령어만 호출하며 사용했는데,
그러다 보니 실제로 클라이언트가 서버에 어떤 데이터를 보내고, 서버가 어떤 형태로 응답하는지는 깊게 생각해본 적이 없었다.
명령어는 익숙하지만, 그 명령어가 네트워크 위에서 어떻게 동작하는지 잘 모르는 상태였던 것…
초심으로 돌아가서 Valkey에 telnet으로 직접 접속해서 RESP를 보내보고 테스트를 해보려고 한다.
일반 Valkey 명령어와 RESP 형식을 번갈아 실행해보면서, 우리가 평소에 쓰던 명령어가 실제로는 어떤 프로토콜 위에서 동작하는지 확인해보자.
이 글의 예시는 Valkey를 직접 컴파일해서 실행한 뒤 확인했다.
테스트 환경은 6380 포트로 Valkey를 띄우고, 일반 명령어와 RESP 요청을 번갈아 보내며 응답을 확인했다.
Valkey는 Redis에서 파생된 프로젝트다
먼저 Valkey가 무엇인지부터 간단히 정리해보자.
Redis는 Remote Dictionary Server의 줄임말로, 메모리 기반 key-value NoSQL 저장소다. 단순히 문자열만 저장하는 것이 아니라 string, set, sorted set, hash, list, stream 같은 여러 자료구조를 제공한다. 인메모리 DB이기 때문에, 일반 디스크 위에서 동작하는 DB 보다는 성능이 좋다. 그래서 cache, session store, distributed lock, rate limiter, leaderboard 같은 용도로 많이 사용된다.
그리고 Valkey는 Redis에서 파생된 오픈소스 프로젝트다. 2024년 3월 Redis의 라이선스 정책이 바뀌면서, 기존 Redis OSS 7.2.4를 기반으로 Linux Foundation 아래에서 Valkey 프로젝트가 시작되었다. Valkey는 Redis와 비슷한 사용 경험을 유지하면서도 BSD 3-Clause 라이선스 기반의 오픈소스 인메모리 데이터 저장소로 계속 개발되고 있다.
그래서 Valkey를 처음 접할 때 완전히 새로운 저장소라고 생각하기보다는, Redis에서 사용하던 명령어와 동작 방식을 이어받은 프로젝트라고 이해하면 편하다. 이번 글에서 다룰 RESP 역시 원래 Redis Serialization Protocol이라는 이름을 가지고 있지만, Valkey에서도 클라이언트와 서버가 통신할 때 사용하는 프로토콜이다.
RESP는 Valkey가 명령어를 주고받는 방식이다
우리가 보통 Valkey에 값을 저장할 때는 아래처럼 명령어를 입력한다.
SET A Hello
GET A
이렇게 한 줄로 쓰는 명령어는 사람이 보기에는 편하다. 하지만 어쨋든 사람만 이해할 수 있는 명령어고 Valkey가 동작하기 위해서는 클라이언트 라이브러리가 Valkey 서버와 통신을 위해서 RESP 형식으로 바꿔서 보낸다.
RESP는 Redis Serialization Protocol의 약자다. 이름 그대로 명령어와 응답을 직렬화하는 프로토콜이라서 HTTP처럼 header, method, path, body를 가진 문서형 프로토콜이라기보다는, 명령어와 인자를 빠르게 파싱하기 위한 단순한 형식에 가깝다고 느꼈다.
HTTP 요청을 생각해보면 이런 모양이다.
GET /users/1 HTTP/1.1
Host: example.com
Accept: application/json
반면 RESP는 명령어를 배열과 문자열 길이로 표현한다.
예를 들어 GET A는 아래처럼 보낼 수 있다.
*2
$3
GET
$1
A
처음 보면 조금 낯설지않을까? 싶다. 나도 그랬으니.. 하지만 생각보다 규칙은 단순하다.
*2: 배열에 들어있는 요소가 2개라는 뜻$3: 다음 문자열의 길이가 3이라는 뜻GET: 첫 번째 요소인 명령어$1: 다음 문자열의 길이가 1이라는 뜻A: 두 번째 요소인 key
즉 GET A라는 명령어를 ["GET", "A"]라는 배열로 표현한 것이다.
문자열 앞에 길이를 먼저 적기 때문에 공백이나 특수문자가 들어간 값도 안전하게 주고받을 수 있다. 이런 패턴은 프로토콜중에서도 많아서 익숙하기도하고 이런 구조 덕분에 구현이 단순하고, 파싱이 빠르며, 사람이 직접 읽어보는 것도 어느 정도 가능하다.
telnet으로 Valkey에 접속해보자
이제 실제로 Valkey에 접속해보자. Valkey가 실행 중인 서버 IP와 port를 알고 있다면 아래처럼 접속할 수 있다.
telnet <ip> <port>
로컬에서 실행 중이라면 보통 아래처럼 접속한다.
telnet 127.0.0.1 6379
접속이 되면 먼저 일반 명령어를 입력해보자.
PING
정상이라면 아래와 같이 응답이 온다.
+PONG
여기서 +는 RESP에서 simple string 응답을 의미한다.
즉 서버가 특수문자가 없는 간단한 문자열 PONG을 반환한 것이다.
Valkey는 RESP 형식뿐 아니라 사람이 직접 입력하기 쉬운 inline command도 처리할 수 있다.
그래서 PING, SET A Hello, GET A 같은 한 줄 명령어도 실행되는 것이다.
일반 명령어로 SET하고 RESP로 GET 해보기
먼저 일반 명령어로 값을 저장해보자.
SET A Hello
응답은 보통 아래처럼 온다.
+OK
이제 같은 값을 RESP로 조회해보자.
GET A를 RESP로 표현하면 다음과 같다.
*2
$3
GET
$1
A
telnet에서는 위 내용을 한 줄씩 입력하면 된다.
서버는 모든 줄을 받은 뒤 GET A 명령어로 해석하고 아래처럼 응답한다.
$5
Hello
여기서 $5는 bulk string의 길이가 5라는 뜻이고 그 다음 줄의 Hello가 실제 값이다.
확인했듯이 우리가 일반 명령어로 저장한 값도 RESP로 조회할 수 있고 반대로 RESP로 저장하고 일반 명령어로 조회하는 것도 가능하다. 둘은 서로 다른 명령어가 아니라, 같은 명령을 표현하는 방식만 다른 것임을 알 수 있다.
RESP로 SET하고 일반 명령어로 GET 해보기
이번에는 반대로 해보자.
SET B World를 RESP로 보내면 아래와 같다.
*3
$3
SET
$1
B
$5
World
응답은 아래처럼 온다.
+OK
이제 일반 명령어로 값을 조회해보자.
GET B
응답은 아래처럼 올 것이다.
$5
World
여기서 중요한 점은 Valkey가 내부적으로 RESP로 들어온 명령과 일반 명령어로 들어온 명령을 완전히 다른 기능으로 취급하는 것이 아니라는 점이다.
inline command는 사람이 입력하기 쉽게 한 줄로 쓴 형태이고, RESP는 클라이언트가 서버와 통신하기 좋은 표현에 가깝다.
sorted set도 RESP로 실행해보자
이번에는 sorted set을 사용해보자. 리더보드와 같은 기능을 만들 때 자주 사용하는 자료구조로 중복을 허용하지 않고, 정렬된 상태를 유지한다는 특징이 있다. score와 member를 함께 저장하고, score 기준으로 정렬된 결과를 가져올 수 있다.
먼저 RESP로 데이터를 넣어보자.
ZADD leaderboard 100 user:1을 RESP로 표현하면 아래와 같다.
*4
$4
ZADD
$11
leaderboard
$3
100
$6
user:1
응답은 아래처럼 온다.
:1
여기서 :는 RESP에서 integer 응답을 의미한다.
새 member가 하나 추가되었기 때문에 1이 반환된 것이다.
두 번째 데이터도 RESP로 넣어보자.
*4
$4
ZADD
$11
leaderboard
$3
120
$6
user:2
응답은 동일하게 integer 형태로 온다.
:1
이제 ZRANGE를 RESP로 실행해보자.
전체 leaderboard를 score와 함께 가져오려면 일반 명령어로는 아래와 같다.
ZRANGE leaderboard 0 -1 WITHSCORES
이를 RESP로 표현하면 아래와 같다.
*5
$6
ZRANGE
$11
leaderboard
$1
0
$2
-1
$10
WITHSCORES
응답은 RESP 배열 형태로 온다.
*4
$6
user:1
$3
100
$6
user:2
$3
120
*4는 배열 요소가 4개라는 뜻이다.
그 뒤로 member와 score가 번갈아 나온다.
즉 실제 값만 보면 아래와 같은 결과라고 이해하면 된다.
user:1 100
user:2 120
클라이언트 라이브러리는 이런 RESP 응답을 받아서 우리가 쓰기 좋은 배열, 객체, 리스트 같은 형태로 바꿔준다. 평소에는 이 변환 과정을 직접 볼 일이 없을 뿐이다.
RESP2와 RESP3
RESP에는 RESP2와 RESP3가 있다.
기본 연결은 보통 RESP2로 시작한다.
RESP3를 사용하고 싶다면 연결에서 HELLO 3 명령을 보내 프로토콜 버전을 바꿀 수 있다.
HELLO 3
RESP2도 대부분의 명령어를 사용하는 데 충분하지만 RESP3는 map, set, null, boolean, double 같은 타입을 더 의미 있게 표현할 수 있다.
정리하자면
Valkey를 사용할 때 우리는 보통 SET A Hello, GET A 같은 명령어만 사용한다.
하지만 실제 클라이언트와 서버 사이에서는 이 명령어가 RESP라는 형식으로 오간다.
RESP는 HTTP처럼 범용 문서를 주고받기 위한 형식이라기보다는, Valkey 같은 인메모리 데이터 저장소가 명령어와 응답을 빠르게 주고받기 위한 단순한 직렬화 프로토콜이다. 명령어는 배열로 표현하고, 문자열은 길이를 먼저 적어서 binary-safe하게 전달한다.
직접 telnet으로 접속해보면 이 구조가 조금 더 명확해진다.
일반 명령어로 SET A Hello를 실행한 뒤 RESP로 GET A를 보내도 같은 값을 가져올 수 있고, RESP로 ZADD, ZRANGE를 실행해도 실제 sorted set 결과를 확인할 수 있다.
처음에는 *2, $3, \r\n 같은 표현이 낯설지만, 결국은 ["GET", "A"]처럼 명령어와 인자를 배열로 보낸다고 생각하면 된다.
이렇게 한 번 직접 프로토콜을 만져보고 나니, Valkey 클라이언트가 내부에서 어떤 일을 해주는지도 조금 더 선명하게 이해할 수 있었다.