파일 스토리지 아키텍처¶
이 문서는 PR #125에서 도입된 영속적 메타데이터 스토리지를 포함하여 OpenAI Files API 호환 레이어를 위한 파일 스토리지 시스템 아키텍처를 설명합니다.
목차¶
개요¶
파일 스토리지 시스템은 영속적 메타데이터 스토리지와 함께 OpenAI Files API 호환 파일 관리를 제공합니다. 사용자가 파인튜닝, 배치 처리 및 기타 목적을 위해 파일을 업로드할 수 있게 하면서 서버 재시작 후에도 데이터 내구성을 보장합니다.
주요 기능¶
- OpenAI Files API 호환:
/v1/files엔드포인트 완전 지원 - 영속적 메타데이터: 파일 메타데이터가 서버 재시작 후에도 유지
- 자동 복구: 시작 시 사이드카 파일에서 메타데이터 인덱스 재구축
- 고아 관리: 불일치 파일 상태 감지 및 정리
- 플러그인 가능한 백엔드: 메모리 및 영속 스토리지 백엔드 지원
문제 정의¶
이전 (인메모리 스토리지)¶
이전에는 파일 메타데이터가 인메모리 DashMap에 저장되었습니다:
┌─────────────────────────────────────────────────────┐
│ 서버 │
│ ┌───────────────────────────────────────────────┐ │
│ │ DashMap<FileId, Metadata> │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ file-1 │ │ file-2 │ │ file-3 │ ... │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ ↓ │
│ 서버 재시작 │
│ ↓ │
│ ┌───────────────────────────────────────────────┐ │
│ │ DashMap<FileId, Metadata> │ │
│ │ (비어 있음) │ │ ← 데이터 손실!
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
문제점:
- 서버 재시작 시 메타데이터 완전 손실
- API 접근 없이 디스크에 고아 파일 생성
- 파일과 메타데이터 간 불일치 상태
- 업로드된 파일 복구 방법 없음
이후 (영속 스토리지)¶
영속적 메타데이터 스토리지 적용:
┌─────────────────────────────────────────────────────┐
│ 서버 │
│ ┌───────────────────────────────────────────────┐ │
│ │ 인메모리 캐시 (DashMap) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ file-1 │ │ file-2 │ │ file-3 │ ... │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ └───────┼───────────┼───────────┼───────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ 파일 시스템 │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ subdir1/ │ │ │
│ │ │ file-abc123.bin ← 데이터 │ │ │
│ │ │ file-abc123.meta.json ← 메타데이터 │ │ │
│ │ │ subdir2/ │ │ │
│ │ │ file-def456.bin │ │ │
│ │ │ file-def456.meta.json │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
↓
서버 재시작
↓
┌─────────────────────────────────────────────────────┐
│ 서버 │
│ ┌───────────────────────────────────────────────┐ │
│ │ .meta.json 파일 스캔 → 캐시 재구축 │ │ ← 자동 복구!
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
아키텍처¶
컴포넌트 다이어그램¶
┌─────────────────────────────────────────────────────────────────┐
│ HTTP Layer │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ /v1/files Handlers ││
│ │ POST /v1/files GET /v1/files GET /v1/files/:id DELETE ││
│ └──────────────────────────┬──────────────────────────────────┘│
└─────────────────────────────┼───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Services Layer │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ FileService ││
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ ││
│ │ │ Upload │ │ Delete │ │ List / Retrieve │ ││
│ │ │ Handler │ │ Handler │ │ Handler │ ││
│ │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ ││
│ │ │ │ │ ││
│ │ ▼ ▼ ▼ ││
│ │ ┌───────────────────────────────────────────────────────┐ ││
│ │ │ Arc<dyn MetadataBackend> │ ││
│ │ └───────────────────────────────────────────────────────┘ ││
│ └──────────────────────────┬──────────────────────────────────┘│
└─────────────────────────────┼───────────────────────────────────┘
│
┌─────────────────┴─────────────────┐
▼ ▼
┌───────────────────────┐ ┌───────────────────────────┐
│ MetadataStore │ │ PersistentMetadataStore │
│ (인메모리) │ │ (사이드카 JSON) │
├───────────────────────┤ ├───────────────────────────┤
│ • DashMap 스토리지 │ │ • DashMap 캐시 │
│ • 빠른 연산 │ │ • JSON 파일 영속성 │
│ • 내구성 없음 │ │ • 시작 복구 │
│ │ │ • 고아 감지 │
└───────────────────────┘ └───────────────────────────┘
레이어 책임¶
| 레이어 | 컴포넌트 | 책임 |
|---|---|---|
| HTTP | Handlers | 요청 파싱, 유효성 검사, 응답 포맷팅 |
| Services | FileService | 비즈니스 로직, 조정 |
| Services | MetadataBackend | 메타데이터 스토리지 추상화 |
| Infrastructure | LocalFileStorage | 물리적 파일 I/O |
스토리지 구조¶
디렉토리 레이아웃¶
storage/
├── a1b2c/ # 하위 디렉토리 (파일 ID 첫 5자)
│ ├── file-a1b2c3d4e5f6.bin # 바이너리 데이터 파일
│ └── file-a1b2c3d4e5f6.meta.json # 메타데이터 사이드카 파일
├── x9y8z/
│ ├── file-x9y8z7w6v5u4.bin
│ └── file-x9y8z7w6v5u4.meta.json
└── ...
파일 명명 규칙¶
| 파일 유형 | 확장자 | 패턴 | 설명 |
|---|---|---|---|
| 데이터 | .bin | file-{id}.bin | 원시 파일 콘텐츠 |
| 메타데이터 | .meta.json | file-{id}.meta.json | JSON 메타데이터 사이드카 |
사이드카 패턴의 이점¶
- 공동 배치: 데이터와 메타데이터가 함께 저장됨
- 원자적 연산: 메타데이터 쓰기는 원자적 이름 변경 패턴 사용
- 쉬운 백업: 간단한 디렉토리 복사로 모든 것 보존
- 디버그 친화적: 사람이 읽을 수 있는 JSON 메타데이터
- 외부 의존성 없음: 데이터베이스 불필요
메타데이터 스키마¶
FileMetadata 구조¶
{
"id": "file-abc123def456",
"object": "file",
"filename": "training_data.jsonl",
"bytes": 1048576,
"purpose": "fine-tune",
"created_at": 1699574400,
"content_type": "application/jsonl",
"storage_path": "a1b2c/file-abc123def456.bin"
}
필드 설명¶
| 필드 | 타입 | 설명 |
|---|---|---|
id | string | 고유 파일 식별자 (OpenAI 형식: file-{random}) |
object | string | API 호환성을 위해 항상 "file" |
filename | string | 업로드된 원본 파일명 |
bytes | integer | 바이트 단위 파일 크기 |
purpose | string | 파일 목적: fine-tune, batch, assistants 등 |
created_at | integer | 생성 Unix 타임스탬프 |
content_type | string | 파일의 MIME 타입 |
storage_path | string | 데이터 파일의 상대 경로 |
지원되는 목적¶
| 목적 | 설명 |
|---|---|
fine-tune | 파인튜닝용 훈련 데이터 |
batch | Batch API 입력 파일 |
assistants | Assistants API용 파일 |
vision | 비전 모델용 이미지 파일 |
user_data | 일반 사용자 업로드 |
스토리지 백엔드¶
MetadataBackend 트레이트¶
#[async_trait]
pub trait MetadataBackend: Send + Sync {
async fn insert(&self, metadata: FileMetadata) -> Result<(), FileError>;
async fn get(&self, id: &str) -> Option<FileMetadata>;
async fn remove(&self, id: &str) -> Option<FileMetadata>;
async fn list(&self, query: &FileListQuery) -> Vec<FileMetadata>;
async fn len(&self) -> usize;
async fn is_empty(&self) -> bool;
}
백엔드 비교¶
| 기능 | MetadataStore (메모리) | PersistentMetadataStore |
|---|---|---|
| 영속성 | 아니오 | 예 |
| 시작 복구 | 아니오 | 예 |
| 성능 | 가장 빠름 | 빠름 (캐시됨) |
| 고아 감지 | 아니오 | 예 |
| 사용 사례 | 개발/테스트 | 프로덕션 |
쓰기 경로 (영속)¶
1. 파일 ID 생성
2. 데이터 파일 저장: storage/{subdir}/file-{id}.bin
3. 메타데이터 JSON 생성
4. 임시 파일에 쓰기: file-{id}.meta.json.tmp
5. 원자적 이름 변경: file-{id}.meta.json.tmp → file-{id}.meta.json
6. 인메모리 캐시 업데이트
읽기 경로 (영속)¶
1. 인메모리 캐시 확인 (DashMap)
2. 캐시 히트 → 캐시된 메타데이터 반환
3. 캐시 미스 → (시작 복구 시에만)
a. .meta.json 파일 디렉토리 스캔
b. 각 파일 파싱 및 유효성 검사
c. 캐시 채우기
인증 및 권한 부여¶
Files API는 파일 작업을 보호하기 위한 포괄적인 인증 및 권한 부여를 포함합니다.
인증 방법¶
| 방법 | 설명 | 사용 사례 |
|---|---|---|
api_key (기본) | Bearer 토큰 인증 | 프로덕션 환경 |
none | 인증 없음 | 개발/테스트 전용 |
권한 부여 모델¶
┌─────────────────────────────────────────────────────────────────┐
│ Files API 요청 │
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 인증 레이어 │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Bearer 토큰 추출 → API 키 유효성 검사 → 스코프 확인 ││
│ └─────────────────────────────────────────────────────────────┘│
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 권한 부여 레이어 │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ 파일 소유권 확인 → 관리자 오버라이드 → 허용/거부 ││
│ └─────────────────────────────────────────────────────────────┘│
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 파일 작업 │
└─────────────────────────────────────────────────────────────────┘
파일 소유권¶
enforce_ownership이 활성화된 경우 (기본값):
| 연산 | 소유자 | 관리자 | 다른 사용자 |
|---|---|---|---|
| 업로드 | 소유 파일 생성 | 소유 파일 생성 | 소유 파일 생성 |
| 목록 | 본인 파일만 | 모든 파일 | 본인 파일만 |
| 조회 | 본인 파일만 | 모든 파일 | 403 Forbidden |
| 다운로드 | 본인 파일만 | 모든 파일 | 403 Forbidden |
| 삭제 | 본인 파일만 | 모든 파일 | 403 Forbidden |
권한 부여를 위한 메타데이터 필드¶
FileMetadata 구조에는 소유권 필드가 포함됩니다:
{
"id": "file-abc123def456",
"owner_id": "user-xyz789",
"organization_id": "org-abc123",
"source_ip": "192.168.1.100",
"created_at": 1699574400
}
| 필드 | 설명 |
|---|---|
owner_id | 파일을 업로드한 사용자 ID |
organization_id | 사용자가 속한 조직 |
source_ip | 업로드 요청의 IP 주소 (감사용) |
감사 로깅¶
모든 파일 작업은 인증 컨텍스트와 함께 로깅됩니다:
INFO file_uploaded file_id="file-abc123" user_id="user-xyz" org_id="org-abc" client_ip="192.168.1.1"
INFO file_downloaded file_id="file-abc123" user_id="user-xyz"
INFO file_deleted file_id="file-abc123" user_id="user-xyz" client_ip="192.168.1.1"
WARN file_access_denied file_id="file-abc123" user_id="user-xyz" file_owner="user-other"
보안 고려사항¶
- 개발 키:
CONTINUUM_DEV_MODE가 설정되거나 디버그 빌드에서만 사용 가능 - 스코프 요구사항: API 키는 설정된 스코프가 있어야 함 (기본: "files")
- 레거시 파일:
owner_id가 없는 파일은 모든 인증된 사용자가 접근 가능 - 관리자 오버라이드: "admin" 스코프가 있는 사용자는 설정된 경우 소유권 검사 우회
시작 복구¶
복구 프로세스¶
┌─────────────────────────────────────────────────────┐
│ 서버 시작 │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 스토리지 디렉토리 재귀적 스캔 │
│ 모든 *.meta.json 파일 찾기 │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 각 .meta.json 파일에 대해: │
│ 1. JSON 콘텐츠 파싱 │
│ 2. 스키마 유효성 검사 │
│ 3. 해당 .bin 파일 존재 확인 │
│ 4. 인메모리 캐시에 추가 │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 복구 통계 로깅: │
│ - 복구된 파일: N │
│ - 감지된 고아: M │
└─────────────────────────────────────────────────────┘
복구 보장¶
- 멱등성: 여러 번 실행해도 안전
- 비파괴적: 복구 중 파일 삭제하지 않음
- 부분 성공: 일부 파일이 손상되어도 계속 진행
- 로깅: 모든 복구 작업이 디버깅을 위해 로깅됨
고아 감지 및 정리¶
고아 유형¶
| 유형 | 설명 | 원인 |
|---|---|---|
| 고아 데이터 | .meta.json 없는 .bin 파일 | 업로드 중 크래시, 수동 삭제 |
| 고아 메타데이터 | .bin 없는 .meta.json | 삭제 중 크래시, 디스크 손상 |
감지 알고리즘¶
pub async fn detect_orphans(&self) -> Result<(Vec<PathBuf>, Vec<PathBuf>), FileError> {
// 스토리지 디렉토리의 모든 파일 스캔
for file in storage_directory {
if file.ends_with(".bin") {
// 해당 .meta.json 존재 확인
let meta_path = file.replace(".bin", ".meta.json");
if !meta_path.exists() {
orphaned_data.push(file);
}
} else if file.ends_with(".meta.json") {
// 해당 .bin 존재 확인
let data_path = file.replace(".meta.json", ".bin");
if !data_path.exists() {
orphaned_metadata.push(file);
}
}
}
Ok((orphaned_data, orphaned_metadata))
}
정리 옵션¶
| 옵션 | cleanup_orphans_on_startup | 효과 |
|---|---|---|
| 비활성화 (기본) | false | 고아 감지 및 로깅만 |
| 활성화 | true | 고아 메타데이터 파일 자동 삭제 |
경고: 데이터 파일 정리는 우발적 데이터 손실 방지를 위해 수동 개입 필요.
TOCTOU 안전 주의사항¶
고아 정리는 TOCTOU(Time-of-Check-Time-of-Use) 경쟁 조건으로 인해 활성 파일 작업 중에는 안전하지 않습니다:
스레드 A: detect_orphans() → file-X를 고아로 발견
스레드 B: upload() → file-X에 대한 메타데이터 생성
스레드 A: cleanup() → "고아" 삭제 (이제 유효함!)
권장사항: 서버 시작 또는 유지보수 기간에만 정리 실행.
설정¶
YAML 설정¶
files:
enabled: true
max_file_size: 536870912 # 512MB
storage_path: "./data/files" # ~ 확장 지원
retention_days: 0 # 0 = 영구 보관
metadata_storage: persistent # "memory" 또는 "persistent"
cleanup_orphans_on_startup: false
환경 변수¶
| 변수 | 기본값 | 설명 |
|---|---|---|
CONTINUUM_FILES_ENABLED | true | Files API 활성화/비활성화 |
CONTINUUM_FILES_MAX_SIZE | 536870912 | 최대 파일 크기 (바이트) |
CONTINUUM_FILES_STORAGE_PATH | ./data/files | 스토리지 디렉토리 |
CONTINUUM_FILES_RETENTION_DAYS | 0 | N일 후 자동 삭제 |
CONTINUUM_FILES_METADATA_STORAGE | persistent | 백엔드 유형 |
CONTINUUM_FILES_CLEANUP_ORPHANS | false | 시작 시 자동 정리 |
스토리지 백엔드 선택¶
| 백엔드 | 사용 시기 |
|---|---|
memory | 개발, 테스트, 일시적 워크로드 |
persistent | 프로덕션, 데이터 내구성 필요 시 |
API 엔드포인트¶
POST /v1/files¶
새 파일 업로드.
curl -X POST http://localhost:8080/v1/files \
-H "Content-Type: multipart/form-data" \
-F "file=@training.jsonl" \
-F "purpose=fine-tune"
GET /v1/files¶
모든 파일 목록.
GET /v1/files/:id¶
파일 메타데이터 조회.
GET /v1/files/:id/content¶
파일 콘텐츠 다운로드.
DELETE /v1/files/:id¶
파일 삭제.
설계 결정¶
왜 사이드카 JSON 파일인가?¶
고려된 대안:
| 옵션 | 장점 | 단점 |
|---|---|---|
| SQLite | ACID, 쿼리 | 추가 의존성, 복잡성 |
| 단일 JSON 파일 | 단순함 | 동시성 문제, 대용량 파일 문제 |
| RocksDB/LevelDB | 빠름, 내구성 | 무거운 의존성 |
| 사이드카 JSON | 단순, 의존성 없음, 공동 배치 | 많은 작은 파일 |
결정: 다음 이유로 사이드카 JSON 파일 선택:
- 기존 파일 기반 아키텍처에 적합
- 추가 의존성 없음 (기존
serde_json사용) - 파일과 메타데이터가 함께 위치해 백업/복원 용이
- 이름 변경 패턴으로 원자적 쓰기 가능
- 디버깅을 위한 사람이 읽을 수 있는 형식
왜 인메모리 캐시 + 디스크인가?¶
패턴: 디스크 영속성을 가진 Write-through 캐시
이점:
- 밀리초 미만의 읽기 지연 시간
- 내구성 있는 쓰기
- 재시작 시 자동 복구
왜 데이터베이스가 아닌가?¶
Files API 사용 사례의 경우:
- 일반적으로 수백에서 수천 개의 파일, 수백만 개가 아님
- 단순한 키-값 접근 패턴
- 복잡한 쿼리 불필요
- 파일 시스템이 이미 원자성 보장 제공
데이터베이스는 다음을 추가할 것:
- 운영 복잡성
- 추가 의존성
- 잠재적 단일 장애 지점