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 명령(또는 실제로 어떤 명령의 조합)을 지정할 수 있어야 한다는 것을 보여줍니다. 이 경우에는 키 20
과 40
을 값 andrea
와 someotherperson
으로 삽입합니다.
지금까지는 좋습니다. 하지만 모든 좋은 데이터베이스가 그래야 하는 것처럼 값을 가져올 수 있을까요? 대답은 예입니다! 하지만 어떻게요? 대답은 다음과 같이 호출되는 get
명령입니다:
./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
는 키와 값을 파일(또는 여러 파일)에 기록해야 하고, 그러면 다음에 실행될 때 요청을 수행하기 위해 다시 읽어들일 수 있어야 한다는 것입니다.
예를 들어, 다음과 같이 실행한다고 가정해 봅시다:
./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, 메모리 관리, 데이터 구조 및 오류 처리와 같은 중요한 주제를 탐색할 수 있습니다.
프로젝트를 진행할 때는 증분 방식을 사용하고, 코드를 철저히 테스트하고, 진행 상황을 자주 저장하는 것을 잊지 마세요. 어려움에 부딪히면 맨 페이지, 온라인 리소스 및 동료들을 활용하세요.
무엇보다 프로그래밍을 즐기세요! 시스템 프로그래밍은 도전적일 수 있지만 보람 있는 분야이며, 이 프로젝트는 여러분이 그 분야에서 첫 걸음을 내딛는 데 도움이 될 것입니다. 행운을 빕니다!