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

```{note}
**시작하기 전에:** [실습 튜토리얼](https://os2024.halla.ai/projects/lab-tutorial.html)을 읽어보세요. C 프로그래밍 환경에서 코딩할 때 유용한 팁들이 있습니다.
```

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

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

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

```bash
./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 명령(또는 실제로 어떤 명령의 조합)을 지정할 수 있어야 한다는 것을 보여줍니다. 이 경우에는 키 `20`과 `40`을 값 `andrea`와 `someotherperson`으로 삽입합니다.

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

```sh
./kv g,10
```

```
10,remzi
```

여기서 우리가 키 `10`을 `get`할 때, 프로그램은 키 값을 출력하고 그 뒤에 쉼표, 그 뒤에 값(이 경우 `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`는 키와 값을 파일(또는 여러 파일)에 기록해야 하고, 그러면 다음에 실행될 때 요청을 수행하기 위해 다시 읽어들일 수 있어야 한다는 것입니다.

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

```sh
./kv p,1,first
```

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

```sh
./kv g,1
```

```
1,first
```

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

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

```sh
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)**: 구조화된 형식으로 데이터를 분석하고 처리하는 과정입니다. 문자열 조작 함수를 사용하여 명령줄 인수나 입력 파일의 내용과 같은 텍스트 입력을 구문 분석할 수 있습니다.

## 코드 예제

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

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

```c
#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;
}
```

문자열을 파싱하는 방법:

```c
#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;
}
```

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

```c
#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, 메모리 관리, 데이터 구조 및 오류 처리와 같은 중요한 주제를 탐색할 수 있습니다.

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

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