kv 프로젝트: 간단한 키-값 저장소

kv 프로젝트: 간단한 키-값 저장소#

Note

시작하기 전에: 실습 튜토리얼을 읽어보세요. C 프로그래밍 환경에서 코딩할 때 유용한 팁들이 있습니다.

이 프로젝트는 운영체제 수업을 위한 간단하지 않은 준비 과제입니다. 또한 여러분이 앞으로 몇 달 동안 매우 익숙해질 C 프로그래머의 사고방식에 빠져들게 하는 역할도 합니다. 행운을 빕니다!

여러분은 kv라는 간단한 프로그램을 작성할 것입니다. 이것은 간단한 영구 키-값 저장소(key-value store)입니다. 페이스북의 RocksDB나 구글의 LevelDB와 같은 키-값 저장 시스템은 다양한 목적으로 산업계에서 널리 사용되고 있습니다. 여기서는 간단한(혹은 복잡한, 누가 알겠어요?) 것을 직접 작성하고 C와 시스템 프로그래밍의 기초를 되새겨 볼 것입니다.

이 프로그램은 몇 가지 옵션을 가질 것입니다. 첫 번째는 데이터베이스에 일부 (키, 값) 쌍을 삽입하는 것입니다. 이것은 다음과 같이 수행됩니다:

./kv
./kv p,10,remzi
./kv p,20,andrea p,40,someotherperson

위의 줄은 사용자가 키-값 프로그램 kv의 이름을 입력했다는 것을 의미합니다(./는 현재 작업 디렉토리(점이라고 하며 .로 참조됨)를 나타내고 슬래시(/)는 구분자입니다. 따라서 이 디렉토리에서 kv라는 프로그램을 찾으세요). 그리고 명령줄 인수를 전혀 주지 않았거나, 하나의 명령줄 인수(p,10,remzi)를 주었거나, 두 개의 명령줄 인수(p,20,andrea p,40,someotherperson)를 주었습니다.

첫 번째 호출은 인수가 없으므로 아무 일도 하지 않습니다. 별로 흥미롭지 않죠?

두 번째는 좀 더 흥미롭습니다. 적어도 명령줄 키-값 저장소만큼은요! 이것은 키-값 시스템에게 데이터베이스에 키-값 쌍을 넣으라고 알려줍니다(p가 이를 나타냅니다). 구체적으로 키는 10과 같고 값은 이 경우 remzi와 같습니다.

보시다시피, 우리의 간단한 키-값 저장소는 키가 정수이고 값은 임의의 문자열(단순화를 위해 쉼표를 포함할 수 없음)이라고 가정합니다.

세 번째 예제는 명령줄 인터페이스가 하나의 명령줄 호출에서 여러 개의 put 명령(또는 실제로 어떤 명령의 조합)을 지정할 수 있어야 한다는 것을 보여줍니다. 이 경우에는 키 2040을 값 andreasomeotherperson으로 삽입합니다.

지금까지는 좋습니다. 하지만 모든 좋은 데이터베이스가 그래야 하는 것처럼 값을 가져올 수 있을까요? 대답은 예입니다! 하지만 어떻게요? 대답은 다음과 같이 호출되는 get 명령입니다:

./kv g,10
10,remzi

여기서 우리가 키 10get할 때, 프로그램은 키 값을 출력하고 그 뒤에 쉼표, 그 뒤에 값(이 경우 remzi)을 출력하는 것을 볼 수 있습니다. 우리는 이 출력을 단순히 printf를 호출하고 표준 출력으로 결과를 출력하는 것으로 수행합니다.

여러분의 KV 저장소가 지원해야 하는 명령의 전체 목록은 다음과 같습니다:

  • put: 형식은 p,key,value이며, 여기서 key는 정수이고 value는 임의의 문자열(쉼표가 없음)입니다.

  • get: 형식은 g,key이며, 여기서 key는 정수입니다. 키가 있으면 시스템은 키를 출력하고, 그 뒤에 쉼표, 그 뒤에 값, 그 뒤에 줄 바꿈(\n)을 출력해야 합니다. 키가 없으면 K not found의 형식으로 한 줄에 오류 메시지를 출력합니다. 여기서 K는 키의 실제 값, 즉 어떤 정수입니다.

  • delete: 형식은 d,key이며, 이는 관련 키-값 쌍을 삭제하고(아무것도 출력하지 않음) 그렇게 하지 못하면(키의 실제 값 K, 즉 어떤 정수로 K not found를 출력함) 실패합니다.

  • clear: 형식은 c입니다. 이 명령은 단순히 데이터베이스에서 모든 키-값 쌍을 제거합니다.

  • all: 형식은 a입니다. 이 명령은 데이터베이스의 모든 키-값 쌍을 임의의 순서로 출력하며, 한 줄에 하나의 키-값 쌍이 있고 각 키와 값은 쉼표로 구분됩니다.

세부사항#

다음은 프로젝트 완료에 도움이 될 수 있는 몇 가지 세부사항입니다.

지속성#

이 프로젝트에서 여러분이 알아내야 할 한 가지는 키와 값을 어떻게 “지속”시켜서 kv 명령의 이후 호출에서 검색할 수 있도록 하는가 하는 점입니다. 지속성은 우리가 나중에 강의에서 다루는 내용이지만, 여기서의 아이디어는 간단합니다: kv는 키와 값을 파일(또는 여러 파일)에 기록해야 하고, 그러면 다음에 실행될 때 요청을 수행하기 위해 다시 읽어들일 수 있어야 한다는 것입니다.

예를 들어, 다음과 같이 실행한다고 가정해 봅시다:

./kv p,1,first

kv 프로그램은 이제 키 1과 값 first를 데이터베이스에 저장해야 합니다. 따라서 나중에 kv를 실행하고 1 키를 가져오려고 하면 값이 반환됩니다:

./kv g,1
1,first

이러한 기능을 구현하는 방법은 매우 많습니다. 여기서는 매우 간단한 것을 제안합니다. 모든 정보를 일반 텍스트 형식으로 저장하는 단일 파일(예: database.txt라고 함)을 사용하는 것입니다.

예를 들어, 데이터베이스에 몇 개의 키와 값이 있는 경우, 일반 텍스트 파일에 한 줄에 하나의 항목씩 정보를 저장할 수 있습니다. 그러면 파일의 내용은 다음과 같을 수 있습니다:

cat database.txt
1,first
2,second

그렇다면 kv는 이 파일을 어떻게 사용해야 할까요? 한 가지 간단한 접근 방식은 시작 시 파일 전체를 연결 리스트나 해시 테이블과 같은 일종의 데이터 구조로 메모리에 읽어들이는 것입니다. 그런 다음 put, get, delete 또는 기타 명령을 처리할 때 kv가 하는 일은 메모리 내 데이터 구조를 업데이트하는 것뿐입니다. 그런 다음 종료하기 전에 프로그램은 파일을 다시 기록하여 향후 사용을 위해 모든 키/값 쌍을 저장해야 합니다.

물론 성능을 향상시키고 매우 큰 데이터베이스에 효율적으로 액세스할 수 있도록 하며 충돌을 허용하기 위해 더 정교한 기술을 사용할 수 있습니다. 이 프로젝트에서는 이러한 것들이 필요하지 않습니다.

가정 및 오류#

  • 잘못된 명령: 명령줄이 p, g, a, c 또는 d가 아닌 잘못된 명령을 지정하는 경우, 경고 bad command를 한 줄에 출력하고 명령줄의 나머지 부분을 계속 처리합니다. 중요한 점은 종료하지 않는다는 것입니다.

  • 예기치 않은 오류: malloc() 실패 또는 파일 열기 실패와 같은 예기치 않은 오류 조건에서는 유용한 오류 메시지를 출력하고 종료합니다. 이것은 테스트되지 않겠지만 개발 중에 유용할 수 있습니다.

유용한 루틴#

입력 파일을 읽어들이기 위해 fopen(), getline(), fclose() 루틴을 사용하면 작업이 쉬워질 것입니다.

출력(화면 또는 파일)을 위해서는 printf()를 사용하세요. 파일에서 읽거나 쓰기 위해서는 fread(), fwrite() 또는 fprintf(), 심지어 getline()을 사용할 수 있습니다.

malloc() 루틴은 메모리 할당에 유용합니다.

strsep() 루틴은 파싱에 유용합니다. 예를 들어 p,10,remzi와 같은 문자열을 받으면 strsep()을 사용하여 서로 다른 부분으로 분리할 수 있습니다.

또한 문자열을 정수로 변환하는 데 atoi()도 유용합니다.

이러한 함수들의 사용법을 모른다면 맨 페이지를 이용하세요. 예를 들어 명령줄에서 man malloc을 입력하면 malloc에 대한 많은 정보를 얻을 수 있습니다.

#

다음은 몇 가지 팁입니다:

  • 작게 시작하고 점진적으로 작동하게 만드세요. 예를 들어 먼저 하나의 명령에 대해 명령줄을 성공적으로 파싱하는 프로그램을 만드세요. 그런 다음 루프를 추가하고 하나의 명령줄에서 여러 명령을 파싱하세요. 그런 다음 메모리 내 데이터 구조에 요소를 추가하는 기능을 추가하되 지속성은 걱정하지 마세요. 그런 다음 지속성을 추가하세요. 또는 그런 식으로요.

  • 테스팅이 중요합니다. 우리가 알고 있던 훌륭한 프로그래머는 작성하는 코드 한 줄마다 5~10줄의 테스트 코드를 작성해야 한다고 말했습니다. 코드가 작동하는지 확인하기 위해 테스트하는 것이 중요합니다. 코드가 처리해야 한다고 생각하는 모든 경우를 처리하는지 확인하는 테스트를 작성하세요. 가능한 한 포괄적으로 하세요. 물론 우리가 여러분의 프로젝트를 채점할 때도 그럴 것입니다. 따라서 우리가 발견하기 전에 먼저 버그를 찾는 것이 좋습니다.

  • 이전 버전을 보관하세요. 버그를 도입하고 쉽게 되돌릴 수 없을 수 있으므로 프로그램의 이전 버전 사본을 보관하세요. 이렇게 하는 간단한 방법은 개발 중 다양한 시점에서 파일의 사본을 명시적으로 만들어 사본을 보관하는 것입니다. 예를 들어 kv.c의 간단한 버전(예: 파일을 읽기만 하는)이 작동한다고 가정해 봅시다. cp kv.c kv.v1.c를 입력하여 kv.v1.c 파일에 사본을 만드세요. 보다 정교한 개발자는 git과 같은 버전 제어 시스템을 사용합니다. 이러한 도구는 배울 만한 가치가 충분히 있으므로 배우세요!

용어 설명#

  • 키-값 저장소(Key-Value Store): 키-값 쌍의 콜렉션을 관리하는 데이터 저장소 유형입니다. 각 키는 값과 연결되며 키를 사용하여 값을 검색할 수 있습니다. 키-값 저장소의 예로는 Redis, Memcached, etcd 등이 있습니다.

  • 영구 저장(Persistence): 프로그램 실행 간에 데이터가 유지되도록 하는 것입니다. 데이터를 디스크에 저장하면 프로그램이 종료되거나 시스템이 충돌하더라도 나중에 데이터를 검색할 수 있습니다.

  • 표준 입력(Standard Input)과 표준 출력(Standard Output): 프로그램의 기본 입력 및 출력 스트림입니다. 일반적으로 표준 입력은 키보드이고 표준 출력은 터미널 화면입니다. 그러나 파일 또는 다른 프로그램으로 리디렉션할 수 있습니다.

  • 명령줄 인수(Command-Line Arguments): 프로그램이 실행될 때 프로그램에 전달되는 값입니다. 이를 통해 사용자는 프로그램의 동작을 사용자 정의할 수 있습니다. C에서 main 함수의 argc(인수 개수)와 argv(인수 벡터) 매개변수를 통해 액세스할 수 있습니다.

  • 파일 디스크립터(File Descriptor): 프로세스가 열려 있는 파일을 참조하는 데 사용하는 정수 값입니다. 파일 디스크립터 0, 1, 2는 각각 표준 입력, 표준 출력, 표준 오류에 대해 예약되어 있습니다.

  • 구문 분석(Parsing): 구조화된 형식으로 데이터를 분석하고 처리하는 과정입니다. 문자열 조작 함수를 사용하여 명령줄 인수나 입력 파일의 내용과 같은 텍스트 입력을 구문 분석할 수 있습니다.

코드 예제#

다음은 이 프로젝트에 도움이 될 수 있는 몇 가지 코드 예제입니다.

파일에서 한 줄씩 읽는 방법:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LINE_LENGTH 1024

int main() {
    FILE *file = fopen("database.txt", "r");
    if (file == NULL) {
        perror("파일을 열 수 없습니다");
        exit(1);
    }

    char line[MAX_LINE_LENGTH];
    while (fgets(line, sizeof(line), file)) {
        // 여기서 한 줄씩 line 변수로 읽어들입니다
        printf("%s", line);
    }

    fclose(file);
    return 0;
}

문자열을 파싱하는 방법:

#include <stdio.h>
#include <string.h>

int main() {
    char input[] = "p,10,remzi";
    char *command, *key, *value;

    command = strtok(input, ",");
    key = strtok(NULL, ",");
    value = strtok(NULL, ",");

    printf("Command: %s\n", command);
    printf("Key: %s\n", key);
    printf("Value: %s\n", value);

    return 0;
}

명령줄 인수를 처리하는 방법:

#include <stdio.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("사용법: %s <인수>\n", argv[0]);
        return 1;
    }

    for (int i = 1; i < argc; i++) {
        printf("인수 %d: %s\n", i, argv[i]);
    }

    return 0;
}

이러한 코드 조각을 프로젝트의 시작점으로 사용하고 필요에 따라 수정 및 확장할 수 있습니다.

결론#

이 프로젝트는 C 프로그래밍 언어와 시스템 프로그래밍 개념을 연습할 수 있는 훌륭한 기회입니다. 영구 키-값 저장소를 구현함으로써 파일 I/O, 메모리 관리, 데이터 구조 및 오류 처리와 같은 중요한 주제를 탐색할 수 있습니다.

프로젝트를 진행할 때는 증분 방식을 사용하고, 코드를 철저히 테스트하고, 진행 상황을 자주 저장하는 것을 잊지 마세요. 어려움에 부딪히면 맨 페이지, 온라인 리소스 및 동료들을 활용하세요.

무엇보다 프로그래밍을 즐기세요! 시스템 프로그래밍은 도전적일 수 있지만 보람 있는 분야이며, 이 프로젝트는 여러분이 그 분야에서 첫 걸음을 내딛는 데 도움이 될 것입니다. 행운을 빕니다!