넓고 얕은 데이터베이스 지식/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