Redis 문자열 내부 구조

Redis 문자열 내부 구조 (SDS)

Redis가 문자열 하나를 저장할 때 메모리에서 어떻게 생겼는지 궁금했던 적 있나요? Redis의 문자열은 사실 단순한 C 문자열이 아니라, 길이와 남은 공간을 함께 관리하는 구조체로 구현되어 있어요. 이 글은 Redis 창시자인 Salvatore Sanfilippo가 개발 초기(약 2010년)에 직접 쓴 원문을 바탕으로, sds.c에 담긴 그 구현을 따라가 볼게요.

출처: Redis 공식 문서 - String internals

문자열은 단순한 char 포인터가 아니에요

Redis 문자열의 구현은 sds.c 파일에 들어 있어요. 여기서 sds는 Simple Dynamic Strings의 줄임말인데, 이 구현은 독립된 라이브러리(antirez/sds)로도 배포되어 따로 빌려 쓸 수 있답니다.

sds.h에 선언된 sdshdr라는 C 구조체가 Redis 문자열 하나를 표현해요.

struct sdshdr {
    long len;
    long free;
    char buf[];
};
  • buf는 실제 문자열을 담는 문자 배열이고요.
  • lenbuf의 길이를 저장해요. 덕분에 문자열 길이를 구하는 일이 O(1)로 끝나요.
  • free는 앞으로 더 쓸 수 있는 추가 바이트 수를 알려줘요.

lenfree를 합쳐 생각하면, 이 둘이 buf 배열의 메타데이터 역할을 한다고 볼 수 있어요.

문자열 만들기: sdsnewlen

sds.h에는 sds라는 새 데이터 타입이 정의되어 있는데, 이건 사실 문자 포인터의 별칭이에요.

typedef char *sds;

sds.csdsnewlen 함수가 새 Redis 문자열을 만들어요.

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;
    sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
#ifdef SDS_ABORT_ON_OOM
    if (sh == NULL) sdsOomAbort();
#else
    if (sh == NULL) return NULL;
#endif
    sh->len = initlen;
    sh->free = 0;
    if (initlen) {
        if (init) memcpy(sh->buf, init, initlen);
        else memset(sh->buf,0,initlen);
    }
    sh->buf[initlen] = '\0';
    return (char*)sh->buf;
}

여기서 기억해야 할 점이 있어요. Redis 문자열은 struct sdshdr 타입인데, sdsnewlen문자 포인터를 반환한다는 거예요. 이게 그냥 트릭 같은데, 실제로는 꽤 영리한 설계예요.

redis라는 문자열을 만들면 메모리에는 아래처럼 len·free 필드와 buf 배열이 차례로 잡혀요.

-----------
|5|0|redis|
-----------
^         ^
sh      sh->buf

그런데 sdsnewlen은 호출한 쪽에 sh->buf를 돌려줘요. 그럼 나중에 문자열을 해제하려면 sh 포인터가 필요한데, 우리가 가진 건 sh->buf뿐이잖아요. shsh->buf로부터 다시 얻을 수 있을까요?

네, 가능해요. 포인터 연산이면 충분하죠. 위 ASCII 그림을 보면 sh->buf에서 두 개의 long 크기를 빼면 정확히 sh가 나와요. 그 두 long의 크기가 마침 struct sdshdr의 크기와 같거든요.

sdslen 함수를 보면 이 트릭이 그대로 쓰이고 있어요.

size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
    return sh->len;
}

이 트릭 하나만 알면 sds.c에 있는 나머지 함수들도 금방 따라 읽을 수 있어요.

즉, Redis 문자열 구현은 문자 포인터만 받는 인터페이스 뒤에 숨어 있어요. 사용하는 쪽은 내부가 어떻게 됐든 신경 쓸 필요 없이, Redis 문자열을 그냥 문자 포인터처럼 다루면 됩니다.

더 알아보기