해시 테이블은 메모리 안에서 키를 빠르게 찾는다. 하지만 프로그램을 종료하면 메모리는 사라진다. 작은 데이터베이스의 첫 문제는 속도가 아니라 프로그램을 다시 시작한 뒤에도 같은 데이터를 되찾는 방법이다.
여기서 생각해 볼 가장 작은 저장소는 이렇다.
데이터베이스는 파일에 변경을 기록하고, 시작할 때 그 기록을 읽어 현재 상태를 복원한다. 빠른 조회가 필요하면 메모리의 인덱스가 파일 속 위치를 가리킨다.
여기서 데이터베이스는 SQL, 여러 사용자, 트랜잭션을 모두 갖춘 제품을 뜻하지 않는다. put, get, delete를 파일에 남기고 다시 읽는 가장 작은 저장소를 뜻한다.
해시 테이블 다음에 파일이 필요한 이유
해시 테이블에 put("theme", "dark")를 하면 키와 값은 메모리에 저장된다. 조회는 빠르지만 프로그램이 끝나면 해시 테이블도 함께 사라진다. 다음 실행에서 get("theme")를 해도 이전 값을 알 방법이 없다.
파일은 프로그램의 메모리와 별도로 남는다. 따라서 값을 넣을 때마다 파일에도 기록하고, 프로그램을 시작할 때 파일을 다시 읽으면 이전 상태를 되살릴 수 있다. 이것이 영속성(persistence)의 출발점이다.
가장 단순한 설계: 변경을 파일 끝에 붙인다
처음에는 기존 값을 찾아 덮어쓰지 않고, 변경 기록을 파일의 끝에 추가한다. 이를 append-only, 즉 추가 전용 로그라고 생각하면 된다.
put("theme", "light")
put("language", "ko")
put("theme", "dark")
파일
0: SET theme light
18: SET language ko
38: SET theme dark
같은 키가 여러 번 나와도 오류가 아니다. 시간 순서대로 읽었을 때 가장 마지막의 완전한 기록이 현재 상태다. 위 파일에서 theme의 현재 값은 light가 아니라 마지막에 기록된 dark다.
이 방식의 장점은 쓰기 경로가 단순하다는 것이다. 파일 중간에서 빈 공간을 찾거나 기존 레코드 길이를 맞출 필요 없이 끝에 하나를 더 붙이면 된다.
반대로 파일에는 오래된 값도 계속 남는다. 그래서 파일이 커지면 나중에 현재 값만 남기는 정리 작업, 즉 컴팩션(compaction)이 필요해진다. 여기서는 정리 작업까지 구현하지 않는다.
재시작하면 현재 상태를 어떻게 되찾을까
프로그램 시작 직후에는 메모리의 인덱스가 비어 있다. 파일을 처음부터 끝까지 읽으며 각 키가 마지막으로 등장한 위치를 기억하면 된다.
파일을 앞에서부터 읽는다.
0: SET theme light → index["theme"] = 0
18: SET language ko → index["language"] = 18
38: SET theme dark → index["theme"] = 38
복원된 인덱스
theme → 38
language → 18
여기서 인덱스는 해시 테이블이다. 값 자체를 모두 다시 메모리에 올리는 대신, 키 → 파일의 바이트 위치를 저장한다. 이후 get("theme")은 인덱스에서 38을 찾고, 파일의 38번째 위치로 가서 dark를 읽는다.
해시 테이블을 배운 이유가 여기서 드러난다. 해시 테이블은 데이터베이스를 대체하지 않는다. 파일 속에서 필요한 기록을 빨리 찾아가게 하는 메모리 인덱스가 된다.
put, get, delete를 머릿속에서 실행해 보기
put(key, value)
- 파일 끝에 SET key value 기록을 추가한다.
- 새 기록이 시작한 파일 위치를 인덱스에 갱신한다.
- 필요한 내구성 수준이라면 기록을 저장 장치까지 밀어 넣는 규칙을 적용한다.
핵심은 “기존 값을 지웠다”가 아니라 “새로운 현재 값을 선언하는 기록을 하나 더 남겼다”는 점이다.
get(key)
- 메모리 인덱스에서 키의 파일 위치를 찾는다.
- 위치가 없으면 이 키는 현재 존재하지 않는다고 판단한다.
- 위치가 있으면 그 파일 기록을 읽어 값을 돌려준다.
인덱스가 고르게 동작한다는 전제에서는 1번이 평균적으로 빠르다. 파일 전체를 매번 다시 읽는 것이 아니다.
delete(key)
추가 전용 파일에서 삭제는 예전 기록을 실제로 지우는 일이 아니다. 보통은 DELETE key라는 삭제 표식(tombstone)을 새로 추가한다.
0: SET theme light
18: SET language ko
38: SET theme dark
57: DELETE theme
복원 과정에서 DELETE theme를 만나면 theme를 인덱스에서 제거한다. 그러면 get("theme")은 없는 키가 되고, language는 그대로 남는다. 삭제 표식은 나중의 컴팩션에서 이전 theme 기록과 함께 정리할 수 있다.
이 설계에서 놓치면 안 되는 규칙
작은 데이터베이스도 아래 규칙이 깨지면 신뢰할 수 없다.
- 같은 키의 현재 값은 가장 마지막 완전한 기록으로 결정한다.
- delete는 해당 키만 없애야 하며, 다른 키의 기록을 건드리면 안 된다.
- 재시작 뒤에 만든 인덱스는 종료 직전의 유효한 기록 순서와 일치한다.
- 파일 끝에 불완전한 기록이 남을 수 있음을 설계에 포함해야 한다.
4번 때문에 기록 형식에는 경계가 필요하다. 예를 들어 레코드 길이, 구분자, 체크섬을 두어 “여기까지는 완전한 기록이고, 그 뒤는 버린다”를 판단할 수 있어야 한다. 전원 차단과 디스크 오류를 제대로 다루는 방법은 다음 단계에서 더 깊게 배운다.
이해 확인 질문과 답변
해시 테이블만으로 데이터베이스가 될 수 없는 이유는 무엇일까?
해시 테이블은 메모리에서 빠른 조회를 제공하지만, 프로그램이 끝나면 그 상태도 사라진다. 데이터베이스가 되려면 메모리 밖의 파일 같은 저장소에 기록을 남기고, 다음 시작 때 그 기록으로 상태를 복원해야 한다.
기존 값을 덮어쓰지 않고 파일 끝에 계속 추가해도 되는 이유는 무엇일까?
파일을 순서대로 읽어 가장 마지막 기록을 현재 값으로 정하면 된다. 이 방식은 쓰기 동작을 단순하게 만들지만, 오래된 기록이 쌓인다는 비용이 있다. 따라서 단순한 쓰기와 미래의 정리 작업을 맞바꾸는 설계다.
재시작 뒤에도 get이 파일 전체를 읽지 않으려면 무엇이 필요할까?
시작할 때 한 번 파일을 훑어 키 → 가장 마지막 기록의 위치 인덱스를 만든다. 그 뒤의 get은 인덱스에서 위치를 찾고 해당 위치만 읽는다. 시작 비용을 내고 조회 비용을 줄이는 선택이다.
delete는 왜 파일에서 예전 값을 바로 지우지 않을까?
파일 중간을 즉시 지우고 빈 공간을 관리하는 것보다, 삭제 기록을 끝에 추가하는 편이 단순하고 기록 순서도 보존된다. 대신 읽을 때 삭제 표식을 해석해야 하고, 나중에 불필요해진 기록을 정리해야 한다.
기록 도중 프로그램이 종료되면 왜 문제가 될까?
파일 끝에 반쪽짜리 레코드가 남을 수 있다. 이를 정상 기록으로 읽으면 잘못된 값을 복원하거나 이후 기록의 경계를 잃는다. 따라서 완전한 레코드인지 판별하는 형식과, 불완전한 끝부분을 무시하거나 복구하는 규칙이 필요하다.
AI에게 이렇게 요청할 수 있다
다음에 AI에게 구현을 요청한다면 이렇게 요청할 수 있다.
문자열 키와 값을 파일에 저장하는 추가 전용 로그 구조를 설명해 줘. 재시작 때 인덱스를 복원하는 과정, 같은 키의 갱신, 삭제 표식, 파일 끝의 불완전한 기록을 어떻게 다룰지 순서대로 보여 줘.
이 정도로 요청할 수 있다면, 코드를 직접 작성하지 않더라도 저장 방식의 핵심 규칙을 어느 정도 통제하고 있는 것이다.
'배움과 성장 > 백엔드·데이터' 카테고리의 다른 글
| 데이터베이스 파일은 왜 정리할까? 컴팩션과 삭제 표식 (0) | 2026.08.23 |
|---|---|
| 프로그램이 꺼져도 데이터는 어떻게 복구할까? 완전한 기록의 원리 (0) | 2026.08.22 |
| MySQL 복합 인덱스 설계: 컬럼 순서·범위 조건·EXPLAIN ANALYZE (0) | 2026.05.01 |
| MySQL 단일 인덱스가 유효한 조건: 선택도·범위 검색·정렬 (0) | 2026.04.30 |
| MySQL 인덱스는 어떻게 찾는가: InnoDB B+Tree와 풀 스캔 (0) | 2026.04.29 |
댓글