Valkey 명령어 기능 추가해보기
이번 글을 통해 2025 OSSCA 5주차 때 배운 Valkey 명령어에서 기능을 추가해보는 실습과정을 정리해보려고 한다.
이전 글을 통해 valkey에 새로운 명령어를 추가해보는 실습을 진행해봤다.
이전에 추가한 명령어는 echo_min abc이라는 명령어를 입력하면 abc라는 것이 결과물로 출력이 되었다.
이번에는 echo_min abc 라고 보내면 응답이 abc 가 아니라 echo_min_ abc 으로 응답이 오도록 만드는 것이다.
1. 기존 동작 코드 파헤치기
일단은 기존에 동작했던 코드를 살펴보고 어떻게 수정을 해야할지 방향성을 잡아보려고 한다.
void echoMinCommand(client *c) {
addReplyBulk(c, c->argv[1]);
}
기존 코드를 보면 파라미터로 client라는 타입의 포인터를 받고 있고 addReplyBulk라는 함수를 호출하고 있다.
client구조체를 보면 아래와 같다.
typedef struct client {
/* Basic client information and connection. */
uint64_t id; /* Client incremental unique ID. */
connection *conn;
/* Input buffer and command parsing fields */
sds querybuf; /* Buffer we use to accumulate client queries. */
size_t qb_pos; /* The position we have read in querybuf. */
robj **argv; /* Arguments of current command. */
int argc; /* Num of arguments of current command. */
int argv_len; /* Size of argv array (may be more than argc) */
...
#ifdef LOG_REQ_RES
clientReqResInfo reqres;
#endif
} client;
모든 필드가 무엇을 의미하는지까지 파악할 필요는 없을 거같고 addReplyBulk에 넘기는 argv를 보면 현재 명령어의 argument들을 담고 있는 것 필드라고 알 수 있다.
즉, echo_min abc라고 주면 c->argv[0]은 echo_min에 대한 *robj, c->argv[1]은 abc에 대한 *robj를 의미한다고 유추해볼 수 있다.
그리고 여기서 robj 구조체를 들여다보면 아래와 같다.
struct serverObject {
unsigned type : 4;
unsigned encoding : 4;
unsigned lru : LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
unsigned hasexpire : 1;
unsigned hasembkey : 1;
unsigned refcount : OBJ_REFCOUNT_BITS;
void *ptr;
};
여기서 중요하게 볼 필드는 *ptr로 디버깅을 통해 해당 필드에 어떤 값이 저장되어있는지 보면 명령어로 온 argument의 실제 값이 저장된 주소값을 가지고 있다.
그래서 echo_min abc라고 입력했을 때 c->argv[0]->ptr을 출력해보면 echo_min이 나오고 c->argv[1]->ptr을 출력해보면 abc가 나오는 것을 확인해볼 수 있다.
void echoMinCommand(client *c) {
printf("첫번째 args: %s\n", c->argv[0]->ptr);
printf("두번째 args: %s\n", c->argv[1]->ptr);
}
다음으로 addReplyBulk함수를 보면 아래와 같다.
/* Add an Object as a bulk reply */
void addReplyBulk(client *c, robj *obj) {
addReplyBulkLen(c, obj);
addReply(c, obj);
addReplyProto(c, "\r\n", 2);
}
addRelyBulk함수에서 호출되는 각 함수의 역할을 보면 아래와 같다.
1. addReplyBulkLen(c, obj) 호출
- 객체의 길이를 계산하여 $<length> 형식으로 추가
2. addReply(c, obj) 호출
- 실제 데이터를 클라이언트의 출력 버퍼에 추가
- 객체의 내용을 RESP 형식에 맞게 직렬화
3. addReplyProto(c, "\r\n", 2) 호출
- RESP 프로토콜의 종료 구분자 \r\n 추가
여기서 RESP 프로토콜에 대한 설명은 이 글을 참고해보면 좋을 거같다.
2. 함수 변경해보기
이제 echoMinCommand를 변경해서 요구사항을 구현해보자.
일단은 간단하게 생각해보면 c->argv[0]->ptr과 '_'와 c->argv[1]->ptr의 값을 join하면 요구사항을 만족할 것으로 보인다.
java언어라면 간단하게 String.join()함수를 사용하면 되지만 valkey프로젝트에서는 sds를 사용해야한다.
sds는 redis에서 개발한 동적 문자열 라이브러리로 C 언어의 기본 문자열(char)의 한계를 보완하기 위해 설계되었다.
sds 자체는 sds.h에 정의된 것에 따르면 char *타입이다.
그러면 이제 echomin abc를 받았을 때 echomin_abc로 나타내기 위한 sds를 만들기 위한 함수가 필요한데 이때는 sdscatfmt함수를 사용하면 된다.
/* This function is similar to sdscatprintf, but much faster as it does
* not rely on sprintf() family functions implemented by the libc that
* are often very slow. Moreover directly handling the sds string as
* new data is concatenated provides a performance improvement.
*
* However this function only handles an incompatible subset of printf-alike
* format specifiers:
*
* %s - C String
* %S - SDS string
* %i - signed int
* %I - 64 bit signed integer (long long, int64_t)
* %u - unsigned int
* %U - 64 bit unsigned integer (unsigned long long, uint64_t)
* %% - Verbatim "%" character.
*/
sds sdscatfmt(sds s, char const *fmt, ...) {
sdscatfmt 함수는 SDS 문자열에 printf 스타일의 포맷팅을 수행하는 함수이다.
해당 함수의 첫번째 인자로 sds타입의 변수를 받아야하므로 비어있는 sds를 만들어서 넣어줘야하는데 이것도 sds.c에 sdsempty라는 함수로 구현이 되어있다.
그러면 아래와 같이 sdscatfmt함수를 사용하면 될 것이다.
void echoMinCommand(client *c) {
sds result = sdscatfmt(sdsempty(), "%s_%s", c->argv[0]->ptr, c->argv[1]->ptr);
}
sds타입의 문자열을 만들었으므로 이를 출력하는 함수가 필요하고 이는 networking.c에 addReplyBulkSds라는 함수를 사용하면 된다.
/* Add sds to reply (takes ownership of sds and frees it) */
void addReplyBulkSds(client *c, sds s) {
if (prepareClientToWrite(c) != C_OK) {
sdsfree(s);
return;
}
_addReplyLongLongWithPrefix(c, sdslen(s), '$');
_addReplyToBufferOrList(c, s, sdslen(s));
sdsfree(s);
_addReplyToBufferOrList(c, "\r\n", 2);
}
여기서 알아보고 넘어가면 좋은 점은 sdsfree(s)라는 함수이다.
sdsfree는 sds 문자열의 메모리를 해제하는 함수로 동적으로 할당된 sds 문자열의 메모리 누수를 방지하기 위해 사용된다.
echoMinCommand에서 sdscatfmt함수를 사용해서 새로운 sds변수를 생성하면서 메모리를 할당받았다.
그러므로 이를 다 사용했으면 메모리 해제를 명시적으로 해줘야하므로 sdsfree는 메모리 누수 방지를 위해 필수라는 것을 알고 넘어가면 좋을 거같다.
3. 결과보기
변경된 코드를 빌드 후에 실행해보면 아래와 같이 정상적으로 출력되는 것을 확인해볼 수 있다.