파일 스토리지 아키텍처¶
이 문서는 영속적 메타데이터 스토리지를 포함하여 OpenAI Files API 호환 레이어를 위한 파일 스토리지 시스템 아키텍처를 설명합니다.
목차¶
- 개요
- 스토리지 모드
- 아키텍처
- 스토리지 구조
- 메타데이터 스키마
- 스토리지 백엔드
- 콘텐츠 스트리밍과 메모리 모델
- 인증 및 권한 부여
- 시작 복구
- 고아 감지 및 정리
- 설정
- API 엔드포인트
- 설계 결정
개요¶
파일 스토리지 시스템은 영속적 메타데이터 스토리지와 함께 OpenAI Files API 호환 파일 관리를 제공합니다. 사용자가 파인튜닝, 배치 처리 및 기타 목적을 위해 파일을 업로드할 수 있게 하면서 서버 재시작 후에도 데이터 내구성을 보장합니다.
주요 기능¶
- OpenAI Files API 호환:
/v1/files엔드포인트 완전 지원 - 영속적 메타데이터: 파일 메타데이터가 서버 재시작 후에도 유지
- 자동 복구: 시작 시 사이드카 파일에서 메타데이터 인덱스 재구축
- 고아 관리: 불일치 파일 상태 감지 및 정리
- 플러그인 가능한 백엔드: 메모리 및 영속 스토리지 백엔드 지원
- 콘텐츠 스트리밍: 업로드와 다운로드가 청크 단위로 오가므로, 전송 1건의 최대 메모리는 파일 크기가 아니라 버퍼 크기
스토리지 모드¶
files.metadata_storage로 선택할 수 있는 메타데이터 스토리지 백엔드는 memory와 persistent(기본값) 두 가지입니다. 아래 다이어그램을 보면 persistent가 기본값인 이유를 알 수 있습니다.
인메모리 메타데이터 (metadata_storage: memory)¶
memory 백엔드에서는 파일 메타데이터가 인메모리 DashMap에만 존재합니다:
memory 백엔드의 제약:
- 서버 재시작 시 메타데이터 완전 손실
- API 접근 없이 디스크에 고아 파일 생성
- 파일과 메타데이터 간 불일치 상태
- 업로드된 파일 복구 방법 없음
영속 메타데이터 (metadata_storage: persistent, 기본값)¶
영속적 메타데이터 스토리지 적용:
아키텍처¶
컴포넌트 다이어그램¶
레이어 책임¶
| 레이어 | 컴포넌트 | 책임 |
|---|---|---|
| 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 메타데이터 사이드카 |
| 진행 중 업로드 | .tmp |
.file-{id}.{pid}.{seq}.tmp |
아직 게시되지 않은 업로드의 콘텐츠 |
.tmp 경로의 유일성은 {id}에서 온다. file- 뒤에 v4 UUID가 붙은 값이며, 목적지 .bin 경로를 유일하게 만드는 것도 같은 값이다. 프로세스 ID와 프로세스별 일련번호는 진단용이다. 남겨진 임시 파일을 어느 프로세스의 어느 업로드가 버린 것인지 추적하는 데 쓴다. 유일성 장치가 아니며 그렇게 취급해서도 안 된다. 컨테이너 두 개는 각자 pid 1을 보고 일련번호도 0부터 시작하기 때문이다.
.tmp 접미사는 고아 스캐너가 찾는 .bin / .meta.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 |
일반 사용자 업로드 |
evals |
평가 데이터셋 |
스토리지 백엔드¶
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}.{pid}.{seq}.tmp 로 콘텐츠 스트리밍
3. 목적이 확정된 뒤 검증 (크기, 유형, 콘텐츠)
4. fsync + 원자적 이름 변경: .file-{id}.{pid}.{seq}.tmp → file-{id}.bin
5. 메타데이터 JSON 생성
6. 임시 파일에 쓰기: file-{id}.meta.json.tmp
7. 원자적 이름 변경: file-{id}.meta.json.tmp → file-{id}.meta.json
8. 인메모리 캐시 업데이트
2~4단계가 데이터 파일의 원자성을 만든다. 첫 바이트가 도착하는 시점에는 전체 크기를 알 수 없으므로 콘텐츠를 최종 경로에 바로 쓸 수 없다. 덕분에 읽는 쪽은 절반만 쓰인 .bin을 볼 일이 없고, 검증에 실패했거나 오류가 났거나 도중에 취소된 업로드는 아무것도 남기지 않는다. 콘텐츠 스트리밍과 메모리 모델을 참고하라.
읽기 경로 (영속)¶
1. 인메모리 캐시 확인 (DashMap)
2. 캐시 히트 → 캐시된 메타데이터 반환
3. 캐시 미스 → (시작 복구 시에만)
a. .meta.json 파일 디렉토리 스캔
b. 각 파일 파싱 및 유효성 검사
c. 캐시 채우기
콘텐츠 스트리밍과 메모리 모델¶
파일 콘텐츠는 처음부터 끝까지 스트리밍된다. 업로드는 네트워크에서 도착하는 대로 청크 단위로 디스크에 기록되고, 다운로드는 클라이언트로 내보내는 대로 청크 단위로 읽힌다. 따라서 전송 1건의 최대 상주 메모리는 64KB 버퍼에 HTTP 계층이 들고 있는 양을 더한 정도이며, 파일 크기와 무관하다.
용량 산정에서 이 차이가 중요하다. files.max_file_size는 메모리 결정이 아니라 정책과 디스크 결정이다. 메모리 한도가 512Mi인 라우터도 512MB 업로드를 처리할 수 있고, 동시 전송이 늘어나도 파일 하나씩이 아니라 청크 버퍼 하나씩만 늘어난다.
업로드 경로¶
- 크기 검사. 청크를 받아들이기 전에 매번 누적 크기를 센다.
files.max_file_size를 넘는 순간 본문의 나머지를 읽지 않고 바로413을 반환한다. - 점진적 검증. 앞쪽 13바이트를 매직 바이트 검사용으로 보관하고(
vision은 이미지 시그니처,assistants/user_data는 실행 파일 시그니처), JSONL 계열 목적에는 할당 없는 UTF-8 검증기가 본문 전체를 추적한다. 이 검증기는 청크 경계를 가로지르는 부분 멀티바이트 시퀀스를 이어서 판정하므로, 네트워크 청크 두 개로 쪼개진 문자도 단일 버퍼일 때와 똑같이 판정되고 시퀀스 도중에 끝나는 본문은 거부된다. - 판정. multipart 클라이언트는
purpose파트보다file파트를 먼저 보낼 수 있고 어떤 검사를 적용할지는 목적이 정하므로, 판정은 본문이 끝난 뒤에 한 번 내린다. 통과하기 전에는 아무것도 게시되지 않는다. - 게시. 임시 파일을 fsync한 뒤 제자리로 rename한다. 메타데이터 사이드카는 그다음에 쓴다.
다운로드 경로¶
권한 판정은 메타데이터만으로, 콘텐츠를 읽기 전에 내린다. 파일 ID는 알지만 소유자가 아닌 호출자는 라우터가 파일을 열어보지도 않은 채 403을 받는다. 이후 응답 본문은 열린 파일 위의 스트림이므로, 파일을 다 읽기 전에 응답이 시작되고 전체가 메모리에 올라오는 일도 없다.
여전히 버퍼링되는 곳¶
본질적으로 파일 전체를 들고 있어야 하는 소비자가 둘 있다. 둘 다 files.max_file_size와 무관하게 별도로 상한이 걸려 있다.
| 경로 | 버퍼링하는 이유 | 상한 |
|---|---|---|
| 채팅 요청으로의 파일 주입 | 콘텐츠를 base64로 인코딩해 JSON 요청 본문에 넣는다 | 이미지 10MB, PDF 32MB (DefaultTransformer::with_max_size) |
| 프로바이더 패스스루 업로드 | 바이트를 상위 프로바이더의 Files API로 다시 업로드한다 | 프로바이더 자체 한도 |
이 때문에 라우터는 files.max_file_size가 cgroup v2(/sys/fs/cgroup/memory.max)가 보고하는 컨테이너 메모리 한도의 4분의 1을 넘으면 시작 시 경고를 남긴다. cgroup v2 한도가 없거나, 한도가 max로 읽히거나, cgroup이 없는 플랫폼(macOS, 베어메탈)에서는 경고를 건너뛰므로 헛경보가 나지 않는다.
일시적 디스크 사용¶
목적은 뒤쪽 multipart 파트로 도착하고 어떤 검사를 적용할지는 목적이 정하므로, 판정은 본문 전체가 디스크에 올라온 뒤에 내린다. 따라서 거부될 업로드도 전송이 진행되는 동안에는 제 크기만큼 디스크를 차지했다가 버려진다. 동시 업로드가 N건이면 일시적 최고치는 max_file_size의 N배이고, Files 그룹에는 자체 레이트 리밋 마운트가 없다.
이는 의도한 교환이다. 디스크 압박은 요청 하나를 실패시키지만, 이전의 버퍼링 설계는 OOM으로 프로세스 전체를 내릴 수 있었다. 최고치를 묶고 싶은 운영자에게는 세 가지 수단이 있다.
server.max_concurrent_requests는 프로세스 전체의 동시 요청 수를 제한한다. 기본값은 설정되어 있지 않다.files.max_file_size를 낮추면 업로드 1건의 상한이 그대로 내려간다.files.storage_path를 전용 볼륨에 두면 업로드 디렉토리가 가득 차도 노드의 다른 것에 영향을 주지 않는다.
실패 양상¶
| 실패 | 결과 |
|---|---|
| 업로드 도중 클라이언트 연결 끊김 | 임시 파일 삭제, 메타데이터 미기록 |
| 검증이 콘텐츠를 거부 | 임시 파일 삭제, 400, 메타데이터 미기록 |
| 디스크 가득 참 또는 쓰기 오류 | 임시 파일 삭제, 500, 메타데이터 미기록 |
| 업로드 도중 프로세스 강제 종료 | .tmp 파일이 남는다. SIGKILL, OOM kill, 노드 축출, 정전에서는 프로세스 안의 어떤 정리 코드도 돌 수 없기 때문이다. API로는 절대 보이지 않는다. 라우터를 시작하면 24시간이 지난 임시 파일 중 라우터가 자기 것으로 인식하는 것(.file-{id}...tmp와 file-{id}.meta.json.tmp)을 회수하며, 그 전에도 FileService::detect_orphans가 개수를 보고하므로 누수를 관측할 수 있다. .tmp로 끝나는 모든 파일이 아니라 이렇게 좁게 맞추는 이유는, files.storage_path가 라우터 전용 디렉토리라는 보장이 없기 때문이다. |
24시간이라는 기준은, 아직 스트리밍 중인 업로드의 임시 파일을 청소가 지우지 못하게 하려고 둔 것이다. 스토리지 디렉토리를 공유하는 다른 라우터 프로세스의 업로드도 여기에 포함된다. 임시 파일의 mtime은 쓰기마다 갱신되므로, 진행 중인 전송은 아무리 오래 걸려도 스스로 기준 밖으로 계속 밀려난다. 이 회수는 일부러 cleanup_orphans_on_startup에 걸지 않았다. 그 플래그는 아직 복구 가능한 데이터일 수 있는 .bin 삭제를 막는 장치인 반면, 오래된 임시 파일은 어떤 API로도 닿을 수 없는 명백한 쓰레기이기 때문이다.
인증 및 권한 부여¶
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
max_file_size는 1KB부터 5GB까지 지원하며, 범위 전체가 실제로 적용된다. 업로드 라우트의 바깥쪽 HTTP 본문 상한이 이 값에서(여기에 multipart 봉투용 고정 1MB 여유를 더해) 파생되므로, 512MB보다 크게 올리면 한도도 실제로 올라간다.
retention_days가 0이 아니면 시작 시 저장된 메타데이터 레코드를 훑어 보존 기간 정리를 실행한다. 나이 기준은 파일시스템 mtime이 아니라 메타데이터 사이드카의 created_at 타임스탬프다. 설정한 일수 이상 지난 파일은 콘텐츠와 .meta.json 사이드카를 함께 삭제한다. retention_days: 0은 파일을 영구 보관한다. 이 정리는 콘텐츠보다 메타데이터를 먼저 제거하므로, 삭제 대상으로 선택된 뒤의 새 조회는 닫힌 쪽으로 실패한다. POSIX에서는 이미 콘텐츠 핸들을 연 다운로드가 계속 스트리밍되지만, 메타데이터를 해석한 뒤 아직 콘텐츠를 열지 않은 호출자는 unlink 경합에서 지면 404를 볼 수 있다. 0이 아닌 정리는 실행마다 삭제/실패 개수를 로그로 남기고, 회수한 파일 수만큼 file_retention_deletes_total{status="success"}를 증가시킨다.
범위를 벗어난 값은 시작 실패가 아니라 시작 경고로 보고하고, 파생된 본문 상한은 범위 안으로 클램프한다. 하드 에러가 아니라 경고인 것은 의도한 선택이다. max_file_size: 0은 지금도 받아들여지고, 작은 값도 현실적으로 있을 수 있으며, 치명적 검사로 만들면 잘 돌던 라우터가 아예 뜨지 않게 된다. 실제로 라우트를 지키는 것은 클램프이므로, 범위를 벗어난 값이 바깥쪽 상한을 없애거나 multipart 봉투 아래로 끌어내리는 일은 생기지 않는다.
크기를 넘긴 업로드가 걸릴 수 있는 지점은 두 곳이며, 둘 다 같은 응답을 준다.
| 요청 | 걸리는 지점 | 응답 |
|---|---|---|
파일이 max_file_size를 넘지만 본문 전체는 파생 상한 이내 |
핸들러, 스트리밍 도중 | 413, {"error": {"code": "file_too_large", ...}}, 메시지는 File too large: exceeds maximum N bytes |
본문이 파생 상한(대략 max_file_size + 1MB)을 넘음 |
전송 계층, 본문을 잘라낸다 | 동일한 413, 동일한 본문 |
두 번째 경우는 예전에 400 invalid_request를 돌려줬다. 핸들러가 multipart 실패를 전부 잘못된 입력으로 분류했기 때문이다. 전송 계층이 잘라낸 본문은 크기 문제이므로, 이제 상태 코드는 클라이언트가 요청을 어떻게 쪼개 보냈는지가 아니라 원인을 따른다.
환경 변수 오버라이드¶
files.auth의 네 필드에는 전용 직접 CONTINUUM_FILES_* 오버라이드가 있습니다. 그 외 Files API 필드에는 없으며, 이 네 변수 역시 ${VAR} 참조를 확장하지 않으므로 files:의 나머지 값은 검증 전에 YAML/TOML에 렌더링하거나 검증된 배포 파일을 따로 관리하십시오.
export CONTINUUM_FILES_AUTH_METHOD=none # 또는 api_key
export CONTINUUM_FILES_AUTH_SCOPE=files
export CONTINUUM_FILES_ENFORCE_OWNERSHIP=true # 엄격한 "true"/"false"만 허용
export CONTINUUM_FILES_ADMIN_ACCESS_ALL=true # 엄격한 "true"/"false"만 허용
각 변수는 설정에 files: 섹션이 전혀 없을 때도 적용됩니다. 이 경우 오버라이드는 FilesConfig::default() 위에 적용되며, 이는 라우터의 다른 부분이 files: 섹션 부재를 처리하는 방식과 동일합니다. 이 변수들에는 대응하는 CLI 플래그가 없으므로 우선순위는 단순히 설정된 환경 변수 > 파일의 files.auth 값 > 기본값 순입니다. 인식할 수 없는 CONTINUUM_FILES_AUTH_METHOD 값이나 정확히 true/false가 아닌 불리언 변수 값은 조용히 대체되지 않고 시작이 명확한 오류와 함께 실패합니다.
설정 파일 감시자는 재로드마다 프로세스 환경을 다시 읽으므로, 파일이 변경되어도 CONTINUUM_FILES_* 값이 파일 내용으로 조용히 대체되지 않습니다. 매번 새로 파싱된 파일 위에 다시 적용됩니다.
스토리지 백엔드 선택¶
| 백엔드 | 사용 시기 |
|---|---|
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 사용 사례의 경우:
- 일반적으로 수백에서 수천 개의 파일, 수백만 개가 아님
- 단순한 키-값 접근 패턴
- 복잡한 쿼리 불필요
- 파일 시스템이 이미 원자성 보장 제공
데이터베이스는 다음을 추가할 것:
- 운영 복잡성
- 추가 의존성
- 잠재적 단일 장애 지점