넓고 얕은 데이터베이스 지식/NoSQL
Valkey에 추가한 나만의 echo 명령어 분석하기
팡펑퐁
2025. 5. 27. 05:29
728x90
💡 지난 글에 추가한 echoX3 명령어에서 사용된 구조체와 함수를 간단히 분석해 보면서 C와 Valkey에 한 발 더 다가가보자.
🤖 echoX3 명령어
127.0.0.1:6379> echoX3 helloworld!
# 출력
"helloworld! helloworld! helloworld!"
- 지난 글에서 valkey의 기존 echo 명령어를 이용해 세 번 반복하는 echoX3 명령어를 추가해 보았다.
- 자세한 내용은 맨 위 링크를 참고하자.
🚀 C 핵심 개념
🏛️ 구조체(Struct)
# 구조체 예시
struct Person {
int age;
char *name;
};
# 사용 예시
struct Person p;
p.age = 30;
p.name = "Alice";
- C에서 구조체는 여러 개의 변수(데이터)를 하나로 묶은 사용자 정의 타입이다.
- 다양한 타입의 값을 묶어 하나의 객체처럼 활용할 수 있다.
- 자바와 비교하면 메서드나 접근 제한자 같은 건 없다.
- 자바에서 public으로 선언된 DTO 필드를 직접 접근하는 것과 비슷하다고 보면 된다.
📍포인터
# 포인터 예시
int x = 42;
int *p = &x; // p는 x의 주소를 저장
# 출력 예시
printf("%p\n", p); // 주소값 출력 (예: 0x7ffee123)
printf("%d\n", *p); // p가 가리키는 값 (42)
- 포인터는 값을 저장하는 변수와 달리 메모리 주소를 저장하는 변수이다.
- 즉, 값을 직접 저장하지 않고, 그 값이 어디 있는지(주소)를 가리킨다.
- &x : 변수 x가 저장된 메모리 주소 (즉, x의 주소값)
- p : 주소값을 저장한 포인터 변수(예시에서는 int형 포인터)
- *p : 변수 p가 가리키는 주소에 저장된 값(= x)
☂️ malloc & free
함수 | 역할 | 설명 |
malloc(size) | 메모리 할당 | 지정한 바이트만큼 동적 메모리(heap)를 요청해서 사용 |
free(ptr) | 메모리 해제 | malloc으로 받은 메모리를 반납(해제)함 |
- C 언어는 실행 중에 메모리 크기를 유연하게 조절할 수 있는 기능이 없기 때문에 런타임 중 필요한 만큼 메모리를 할당할 때 malloc()을 사용한다.
- 자바의 new와 비슷한 역할을 하지만, 자동으로 메모리를 해제해 주는 GC가 없기 때문에 반드시 사용이 끝나면 free()로 직접 메모리를 반납해야 한다.
- 그렇지 않으면 메모리 누수(memory leak)가 발생할 수 있다.
🌀 Valkey(Redis) 내부 구조체와 함수
🔸 client
typedef struct client {
/* Basic client information and connection. */
uint64_t id; /* Client incremental unique ID. */
connection *conn;
...
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) */
...
}
- Valkey(Redis)의 클라이언트 연결을 처리하는 구조체이다.
- 클라이언트가 서버로 명령을 보낼 때 이 구조체가 모든 정보를 가지고 있다.
- argv는 현재 명령어의 인자 배열을 뜻한다.
- robj 포인터 배열로 "echo abc"의 경우 argv[1] -> ptr은 "abc"이다.
- argc는 인자의 개수이다.
- argv_len은 argv 배열의 길이이다.
- 이외에도 버퍼, 인증, 사용자 정보 등의 필드를 가지고 있다.
🔸 robj(serverObject)
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; // 실제값
};
- 구조체 이름이 Redis에서는 redisObject이고, Valkey에서는 serverObject이다.
- Valkey(Redis)에서 모든 값은 단순 자료형이 아니라 robj라는 구조체로 감싸져 있다.
- 사용자가 echo 명령어를 통해 "abc"를 입력했다고 생각해 보자.
- 이때 "abc"는 robj로 감싸져 있다.
- refcount는 현재 객체가 몇 곳에서 참조되고 있는지를 나타내는 값이다.
- 자바처럼 GC(Garbage Collector)가 없기 때문에 참조 횟수를 통해 관리한다.
🔹 sds(Simple Dynamic String)
😵💫 C의 기본 문자열
char *str = "hello";
주소 값
0x1000 → 'h'
0x1001 → 'e'
0x1002 → 'l'
0x1003 → 'l'
0x1004 → 'o'
0x1005 → '\0' ← 문자열 끝 표시 (널 문자)
- "hello" 문자열은 메모리에 위와 같이 저장된다.
- C는 문자열의 길이를 따로 저장하지 않기 때문에, strlen() 함수를 사용해서 문자열 길이를 매번 처음부터 \0이 나올 때까지 하나씩 세야 한다.
- 문자열에 할당된 버퍼의 전체 크기를 알 수 있는 방법도 없다.
- 문자열 조작 시 배열의 범위를 벗어나는 것을 막아주는 장치가 없기 때문에 버퍼 오버플로우가 발생할 위험이 있다.
🎨 sds(Simple Dynamic String)
sds str = sdsnew("hello");
[헤더(길이 정보 등)] + [실제 문자열 데이터(buf)] + ['\0']
헤더 문자열 데이터
┌──────┬───────┬───────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ len │ alloc │ flags │ 'h' │ 'e' │ 'l' │ 'l' │ 'o' │ \0 │
└──────┴───────┴───────┴─────┴─────┴─────┴─────┴─────┴─────┘
▲
sds 포인터는 여기(buf)를 가리킴
- Valkey(Redis)에서는 위와 같은 문제를 해결하기 위해 sds라는 별도의 구조체를 만들어 사용한다.
- sds는 실제로 char * 형태처럼 보이지만 문자열 앞쪽에 헤더 구조체가 붙어있다.
- 헤더는 len, alloc, flags가 있다.
- len은 sds에 실제로 들어있는 문자열의 길이이다.
- alloc은 sds가 메모리 안에 확보해 놓은 총 버퍼 크기이다.(몇 글자까지 사용가능한지)
- flags는 sds가 어떤 헤더 타입인지를 알려주는 표시값이다.
📦 sds 헤더 구조체
# sds.c
typedef char *sds;
typedef const char *const_sds;
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
- Valkey(Redis)는 수백만 개의 문자열을 다루기 때문에 문자열을 저장하는 메모리를 아끼는 것이 좋다.
- 예를 들어 "hi"와 같은 짧은 문자열에 메모리를 아끼고, 길어진 만큼 공간을 할당하는 것이 성능에 유리할 것이다.
- 따라서 문자열 길이에 맞는 적절한 sds 헤더 구조체를 자동으로 선택한다.
🔹 sdsempty()
- 빈 문자열을 만들고 sds 타입으로 리턴하는 함수이다.
- 내부적으로는 malloc()으로 메모리 잡는다.
- 길이(len) = 0, 할당 크기(alloc) = 최소 크기로 초기화된다.
🔹 sdscatfmt()
- 기존 sds 문자열에 포맷 문자열을 붙여서 리턴한다.
- 자동으로 버퍼 크기 늘려준다.(안전하게 append 된다)
- 순서
- 기존 sds의 alloc 확인한다.
- 부족하면 sdsMakeRoomFor()로 메모리 확장한다.
- 문자열 끝에 포맷 결과 붙인다.
- 새로운 sds 포인터 리턴한다.
🔹 addReplyBulkSds(client *c, sds s)
- 클라이언트에게 sds 문자열을 응답으로 전송한다.
- 자동으로 RESP 포맷을 처리한다.
- 이 함수에 넘긴 sds는 Valkey(Redis)가 해제하므로 sdsfree()를 하지 말아야 한다.
- 그렇지 않으면 double free(이중 해제) 오류 발생한다.
👨🏻🔬 분석해 보기
void echoX3Command(client *c) {
if (c->argc != 2) {
addReplyError(c, "Wrong number of arguments for 'echoX3' command");
return;
}
sds input = c->argv[1]->ptr;
// "input input input" 형식의 문자열 생성
sds result = sdscatfmt(sdsempty(), "%S %S %S", input, input, input);
// addReplyBulkSds()는 sds 메모리를 직접 해제함
addReplyBulkSds(c, result);
}
- 위 설명을 토대로 분석해 보자.
- c -> argv[1] -> ptr
- c -> : 클라이언트 구조체 안에서,
- argv -> :argv 인자의 명령어 배열(argv[1]) 안에 있는
- ptr : 의 값을 가져온다.
- sdscatfmt()로 echoX3 명령어를 통해 받은 명령문을 3 번 반복하는 sds를 생성한다.
- addReplyBulkSds()로 리턴한다.
참고
2025 오픈소스 컨트리뷰션 아카데미 Git & Redis 수업 자료
ChatGPT
728x90