Redis 문자열 내부 구조
Redis 문자열 내부 구조 (SDS)
Redis가 문자열 하나를 저장할 때 메모리에서 어떻게 생겼는지 궁금했던 적 있나요? Redis의 문자열은 사실 단순한 C 문자열이 아니라, 길이와 남은 공간을 함께 관리하는 구조체로 구현되어 있어요. 이 글은 Redis 창시자인 Salvatore Sanfilippo가 개발 초기(약 2010년)에 직접 쓴 원문을 바탕으로, sds.c에 담긴 그 구현을 따라가 볼게요.
문자열은 단순한 char 포인터가 아니에요
Redis 문자열의 구현은 sds.c 파일에 들어 있어요. 여기서 sds는 Simple Dynamic Strings의 줄임말인데, 이 구현은 독립된 라이브러리(antirez/sds)로도 배포되어 따로 빌려 쓸 수 있답니다.
sds.h에 선언된 sdshdr라는 C 구조체가 Redis 문자열 하나를 표현해요.
struct sdshdr {
long len;
long free;
char buf[];
};
buf는 실제 문자열을 담는 문자 배열이고요.len은buf의 길이를 저장해요. 덕분에 문자열 길이를 구하는 일이 O(1)로 끝나요.free는 앞으로 더 쓸 수 있는 추가 바이트 수를 알려줘요.
len과 free를 합쳐 생각하면, 이 둘이 buf 배열의 메타데이터 역할을 한다고 볼 수 있어요.
문자열 만들기: sdsnewlen
sds.h에는 sds라는 새 데이터 타입이 정의되어 있는데, 이건 사실 문자 포인터의 별칭이에요.
typedef char *sds;
sds.c의 sdsnewlen 함수가 새 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뿐이잖아요. sh를 sh->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 문자열을 그냥 문자 포인터처럼 다루면 됩니다.