지난 과제에서는 Valkey에 ECHOPIXXLIX라는 커스텀 명령어를 추가했다.
동작은 기존 ECHO 명령어처럼.사용자가 abc를 입력하면 그대로 "abc"를 돌려주는 단순한 명령어였다.
이번 과제에서는 여기서 한 단계 더 나아가, 입력값 앞에 echo2_라는 prefix를 붙여 응답하도록 수정해보자.

겉으로 보면 단순히 문자열 앞에 echo2_만 붙이는 작업처럼 보인다.
하지만 Valkey 내부에서는 기존 입력값을 그대로 반환하는 것과, 새로운 문자열을 만들어 반환하는 것이 꽤 다르게 처리된다.
이번 글에서는 코드 흐름을 세세하게 따라가기보다는, 왜 새 문자열을 만들어야 했는지, Valkey는 문자열을 어떻게 다루는지, 응답을 만들 때 메모리 소유권은 어떻게 처리되는지를 중심으로 정리해보려고 한다.
1. 기존 방식: 입력값을 그대로 돌려주기
지난 과제에서 구현했던 echoPixxlixCommand는 이런 형태였다.
void echoPixxlixCommand(client *c) {
addReplyBulk(c, c->argv[1]);
}
기존 구현에서는 c->argv[1]을 그대로 addReplyBulk()에 넘겼다.
사용자가 보낸 값 → 그대로 bulk string 응답
이 방식은 이미 존재하는 object를 그대로 응답할 때는 적합하다.
하지만 이번 과제에서는 "abc"를 그대로 돌려주면 안 된다. 기존 입력값 앞에 새로운 문자열을 붙인 새 응답 문자열이 필요하다.
2. 새 문자열 만들기
이번 과제의 전체 로직은 다음과 같다.
사용자 입력: abc
↓
c->argv[1]에서 실제 문자열 값 꺼내기
↓
빈 sds 문자열 만들기
↓
echo2_ + 입력값 붙이기
↓
새로 만든 문자열을 bulk string 응답으로 반환
void echoPixxlixCommand(client *c) {
sds reply = sdsempty();
reply = sdscatfmt(reply, "echo2_%S", objectGetVal(c->argv[1]));
addReplyBulkSds(c, reply);
}
코드 줄 수만 보면 짧은 수정이다.
하지만 이 안에는 Valkey의 문자열 처리 방식과 응답 처리 방식이 함께 들어 있다.
2.1 c->argv[1]은 그냥 문자열이 아니다
처음에는 c->argv[1]을 그냥 char *처럼 생각하기 쉽다.
하지만 Valkey에서 command argument는 단순 C 문자열이 아니라 robj * 형태로 관리된다.
그래서 입력값 자체를 그대로 문자열처럼 이어 붙이는 것이 아니라, 먼저 object 안에 들어 있는 실제 문자열 값을 꺼내야 한다.
이 역할을 하는 함수가 objectGetVal()이다.
objectGetVal(c->argv[1])
즉, 이 부분은 이렇게 이해할 수 있다.
c->argv[1]이라는 Valkey object에서
실제 문자열 값을 꺼낸다
기존에는 object 자체를 그대로 addReplyBulk()에 넘겼지만, 이번에는 그 안의 실제 값을 꺼내서 새 문자열을 만드는 데 사용했다.
2.2 Valkey의 문자열 타입, sds
이번 과제에서 가장 중요하게 본 개념은 sds였다.
C에서는 보통 문자열을 char *로 다룬다.
하지만 일반 C 문자열은 길이를 알려면 strlen()처럼 문자열 끝까지 직접 훑어야 한다.
예를 들어 "hello"의 길이를 구하려면, 내부적으로는 \0을 만날 때까지 한 글자씩 확인한다.
h → e → l → l → o → \0
문자열이 짧을 때는 큰 문제가 없지만, Valkey처럼 문자열 데이터를 매우 자주 다루는 서버에서는 이 비용이 중요해질 수 있다.
그래서 Valkey는 문자열을 다룰 때 주로 sds라는 구조를 사용한다.
sds는 겉으로는 char *처럼 보이지만, 실제 메모리 구조는 조금 다르다.
[ len | alloc | flags ][ 실제 문자열 buf ][ \0 ]
↑
sds 포인터
sds의 앞쪽에는 header가 있고, 그 안에는 문자열 길이, 할당된 크기, 타입 정보 등이 들어 있다.
재밌는 점은 sds 포인터가 header를 가리키는 것이 아니라, 실제 문자열이 시작되는 buf를 가리킨다는 것이다.
처음에는 조금 이상하게 느껴졌다.
header가 중요하면 포인터가 header를 가리켜야 하는 것 아닌가?
이렇게 하면 장점이 있다.
1. C 문자열처럼 사용하기 쉽다. 바로 문자열 시작 위치를 가리키기 때문이다.
2. 길이나 할당 크기 같은 정보가 필요하면 문자열 전체를 훑지 않고, 포인터 앞쪽으로 이동해서 header를 읽으면 된다.
즉, 사용하기에는 C 문자열처럼 편하게 쓰고, 내부적으로는 header를 통해 길이와 메모리 정보를 빠르게 확인할 수 있는 구조다.
이 부분이 처음에는 낯설었지만, Valkey가 왜 자체 문자열 구조를 사용하는지 이해하는 데 도움이 됐다.
2.3 빈 문자열을 만들고, 뒤에 붙이기
이번 과제에서는 먼저 빈 sds 문자열을 만들고, 이후 sdscatfmt()를 사용해 echo2_와 입력값을 이어 붙였다.
sds reply = sdsempty();
reply = sdscatfmt(reply, "echo2_%S", objectGetVal(c->argv[1]));
중요한 점은 sdscatfmt()의 반환값을 다시 reply에 저장한다는 것이다.
문자열을 뒤에 붙이다 보면 기존 공간이 부족할 수 있다.
그 경우 내부적으로 더 큰 공간을 새로 할당하거나 realloc이 일어날 수 있다.
그러면 기존 reply 포인터가 바뀔 수도 있다.
그냥 아래처럼 반환값을 받지 않는 방식은 위험할 수 있다.
sdscatfmt(reply, "echo2_%S", objectGetVal(c->argv[1]));
3. addReplyBulkSds() 로 응답하기
기존에는 이렇게 응답했다.
addReplyBulk(c, c->argv[1]);
기존 방식은 이미 존재하는 robj를 그대로 bulk string 응답으로 보낼 때 사용하기 좋다.
하지만 이번에는 새로 만든 sds 문자열이 있으니 addReplyBulkSds()를 사용했다.
sds reply = sdsempty();
reply = sdscatfmt(reply, "echo2_%S", objectGetVal(c->argv[1]));
addReplyBulkSds(c, reply);
조심해야 할 점은 addReplyBulkSds()의 메모리 소유권이다.
addReplyBulkSds()는 단순히 응답만 추가하는 함수가 아니라, 전달받은 sds의 메모리 소유권까지 가져간다.
sdsfree(reply); // 하면 안 됨
왜냐하면 addReplyBulkSds() 내부에서 이미 해당 sds를 처리하고 해제하기 때문이다.
이걸 모르고 직접 sdsfree(reply)를 한 번 더 호출하면 double free 문제가 생길 수 있다.
또 호출 이후에 reply를 다시 사용하면 이미 해제된 메모리를 참조하는 use-after-free 문제가 생길 수 있다.
쉽게 말하면,
addReplyBulkSds()에 reply를 넘기는 순간
reply의 책임은 함수 쪽으로 넘어간다
테스트 결과
수정 후 valkey-cli에서 직접 확인했다.
127.0.0.1:6379> echoPixxlix abc
"echo2_abc"
127.0.0.1:6379> echoPixxlix hello
"echo2_hello"
인자 없이 실행했을 때는 command metadata의 arity 검증에 따라 에러가 발생한다.
127.0.0.1:6379> echoPixxlix
(error) ERR wrong number of arguments for 'echopixxlix' command
정리하며
처음에는 단순히 문자열 앞에 echo2_만 붙이면 되는 과제라고 생각했는데, Valkey 내부에서는 기존 object를 그대로 반환하는 것과, 새 문자열을 만들어 반환하는 것이 다르게 처리된다는 점을 알게 되었다.
Valkey의 문자열 처리 방식과 응답 생성 방식을 조금 더 구체적으로 이해할 수 있었다.
추가로 Valkey 네트워크 처리과정에 대해서도 배웠는데, 다음 포스팅에서 이어서 작성하겠다.(epoll과 멀티스레딩의 개념을 이해하고 있다고 생각했는데, 너무 어려워서 PPT 슬라이드를 이해하는데만 3일정도 걸렸다)
'CS STUDY' 카테고리의 다른 글
| Redis/Valkey는 Command를 어떻게 실행할까? - Valkey 커스텀 명령어를 만들어보자 (0) | 2026.05.21 |
|---|---|
| Redis, Valkey는 왜 빠를까? - 자료구조, 메모리 최적화, RESP 프로토콜 실습 (0) | 2026.05.10 |
| Git-X-Ray : 깃 구조 살펴보기 (0) | 2026.05.10 |
| io_uring 개념과 epoll과의 비교 + 실제 코드 테스트 (0) | 2025.11.08 |