FileStore 전면 재설계 — 저장소 · 메타데이터 · 데이터 플랫폼 공급
작성 배경: FileStore는 원래 "소비 포탈의 첨부파일 등 비정형데이터를 통합 관리"하는 저장 계층이다. 이 문서는 그 목적을 저장소 통합 → 메타데이터 원장 → 빅데이터 분석 파이프라인 공급으로 확장하기 위한 전면 재설계(메뉴·데이터 모델·API·아키텍처)를 정리한다.
상태: 설계(제안). 코드 구현은 이 문서 확정 후 단계별로 진행한다. 관련 문서:
docs/ARCHITECTURE.md,docs/ADMIN_CONSOLE.md,docs/STAGE1_API_CONTRACT.md,docs/STAGE2_DIRECTORY_SERVICE_CONTRACT.md,docs/ATTACHMENT_STORAGE_ROADMAP.md.
1. 목적
FileStore의 최종 목적은 세 가지다.
- 저장소 통합 관리 — 이기종 저장 백엔드(R2·S3·로컬 볼륨·네트워크 드라이브·구글드라이브·원드라이브)를 등록하고, 소비 포탈별로 어느 저장소(버킷·경로)를 쓸지 정책으로 결정한다.
- 메타데이터 원장 — 등록되는 파일의 출처(포털·메뉴·게시물)와 분류·무결성 메타데이터를 FileStore가 정본으로 보유한다. 원본 업무 데이터는 소비 포탈에 있어도, 분석에 필요한 메타는 FileStore가 갖는다.
- 데이터 파이프라인 공급 — 위 메타데이터와 변경 이벤트(CDC), 콘텐츠를 데이터 플랫폼(Databricks, Snowflake 등)에 제공한다. 제공 방식은 pull(추출 API·CDC) 과 push(DataSink 커넥터) 를 모두 지원한다.
핵심 원칙(기존 유지): 브라우저에 저장소 자격증명을 노출하지 않는다. FileStore가 저장 검증과 lifecycle의 최종 보안 경계다.
2. 현재 상태 (as-is)
2.1 관리자 메뉴
React admin-web(포탈쉘 @hohyun94/portal-foundation)의 현재 탭:
개요(overview) · 파일(assets) · 저장소(storage) · 감사 로그(audit)저장소(storage)탭은 백엔드 목록 + 소비정책 목록의 읽기 전용 테이블만 제공한다.- 백엔드·정책의 등록/수정/삭제 폼은 React 앱에 없고, 번들이 없을 때만 노출되는 서버 렌더 폴백
src/adminConsole.ts에만 있다. - 결과: 번들이 배포된 현재 상태에서는 화면으로 백엔드/정책을 등록할 수 없고 API/curl 로만 가능하다.
2.2 데이터 모델 (src/domain.ts)
| 모델 | 필드 |
|---|---|
StorageBackend | provider ∈ {CLOUDFLARE_R2, AMAZON_S3, LOCAL_VOLUME, S3_COMPATIBLE}, secretRef, endpoint, bucket, localRoot, region, enabled |
ConsumerStoragePolicy | consumerId(자유 문자열·FK 아님), backendId, pathPrefix, maxBytes, allowedContentTypes, encryptionRequired, ragPublishAllowed, readEnabled/writeEnabled/enabled |
AssetRecord | consumerId, mappingId, ownerType, ownerId, fileName, contentType, classification, organizationIds, sourceVersion, sourceMenuId, sourceEntityType, sourceEntityId + id, storageKey, bytes, checksum, status |
2.3 스토리지 계층 (src/storage.ts)
ObjectStorage인터페이스:put / getReadUrl / getWriteUrl / head / delete.S3CompatibleStorage(R2·S3·S3_COMPATIBLE):getWriteUrl→presigned PUT,getReadUrl→presigned GET.LocalVolumeStorage: 서명 HTTP 업로드/다운로드 URL(/local-uploads,/local-objects) 또는 불투명 locator +streamTo.- 업로드 흐름:
upload-session(PENDING_SCAN 파일 +uploadUrl) → 소비자 PUT →complete(bytes·checksum 검증 → ACTIVE). base64 호환 경로도 존재한다.
2.4 한계 (재설계가 필요한 지점)
- 저장소 종류가 R2·S3·로컬로 한정 — 네트워크 드라이브·구글드라이브·원드라이브 없음.
- 객체 위치가 항상 경로(key) 전제 — Drive/OneDrive 의
fileId/itemId개념 없음. - OAuth 기반 저장소를 위한 토큰 보관·갱신 구조 없음(현재는 secretRef 원칙만).
- 소비 포탈이 자유 문자열
consumerId— 등록 원장·오타/고아 정책 차단 없음. - 파일에 출처 메타(어느 메뉴·게시물) 없음 —
ownerType/ownerId로 일부만 표현,menu개념 부재. - 분석용 공급 계층 없음 — RAG 발행(
ragPublishAllowed→RAGCenter)만 존재. 데이터 플랫폼 연동·CDC·추출 API 부재. - 설정 CRUD 가 React 어드민이 아니라 폴백에 있음.
3. 목표 아키텍처 (to-be)
FileStore는 3중 역할을 갖는다.
┌──────────────────────────────────────────────┐
소비 포탈 ───▶│ FileStore │
(service token) │ │
│ (1) 저장소 통합 : provider별 backend + OAuth│
│ (2) 메타데이터 원장: 출처·분류·무결성 │
│ (3) 데이터 공급기 : pull API + CDC + DataSink │
└───────┬───────────────────────┬──────────────┘
│ push │ pull
▼ ▼
스테이징 버킷 / Kafka / REST 추출 API·CDC(cursor)
│ │
└────────┬───────────────┘
▼
Databricks(Delta) ──UniForm──▶ Snowflake(Iceberg)- 저장 계층: 이기종 backend 등록 + 소비 포탈별 라우팅 정책.
- 원장 계층: 파일 메타데이터의 정본(출처·분류·무결성). 분석의 조인키·거버넌스 근거.
- 공급 계층: 정형 메타 카탈로그 + 변경 이벤트(CDC) + 콘텐츠를 데이터 플랫폼에 제공.
4. 목표 메뉴 구조
설정
├─ 1. 저장소 백엔드 provider별 등록 + 연결테스트 + OAuth 연결
├─ 2. 소비 포털 FileStore를 쓰는 소비포탈 등록 원장(신규)
└─ 3. 포털 저장 정책 포털 × 저장소(버킷/경로) 매핑 + 용량·MIME·read/write
운영
├─ 4. 파일 관리 검색·필터(포털/메뉴/게시물/상태/MIME) + 상세
├─ 5. 감사 로그 기존 유지
└─ 6. 데이터 공급 Data Feed 상태·커서·sink 연결·전송 이력(신규)- CRUD 의 정본은 React
admin-web으로 이전한다. 인라인 폴백(adminConsole.ts)은 롤백 전용으로 남긴다.
5. 영역별 설계
5.1 저장소 백엔드 — provider 능력 모델
| provider | 상태 | 업로드 | 다운로드 | 객체 위치 | 자격증명 |
|---|---|---|---|---|---|
CLOUDFLARE_R2 | 기존 | presigned PUT | presigned GET | 키(path) | secretRef(AK/SK) |
AMAZON_S3 | 기존 | presigned PUT | presigned GET | 키(path) | secretRef |
S3_COMPATIBLE | 기존 | presigned PUT | presigned GET | 키(path) | secretRef |
LOCAL_VOLUME | 기존 | 서명 HTTP PUT | 서명/stream GET | 파일 경로 | 없음 |
NETWORK_DRIVE | 신규 | 로컬과 동일(마운트 경로) | 동일 | 마운트 경로 | 없음(마운트) |
GOOGLE_DRIVE | 신규 | FileStore 경유(프록시) | FileStore 경유(프록시) | fileId | OAuth refresh token |
ONEDRIVE | 신규 | FileStore 경유 | FileStore 경유 | itemId | OAuth refresh token |
NETWORK_DRIVE는 이번 범위에서 호스트에 이미 마운트된 경로(SMB/NFS) 로 한정한다(SMB 프로토콜 직접 접속은 범위 밖). 구현은LOCAL_VOLUME과 동일 어댑터를 재사용하고 provider 라벨·검증만 분리한다.- Drive/OneDrive 는 presigned URL 이 없고 위치가 경로가 아니므로 어댑터·업로드 흐름을 함께 바꾼다(5.3, 5.4).
5.2 OAuth · 토큰 보관 아키텍처
- 기존 원칙("secretRef 만 저장, credential 원문 미저장")은 유지하되, OAuth 는 refresh token 이 필요하므로 서버 측 암호화 저장소를 도입한다.
- 권장:
FILE_STORE_ENVELOPE_MASTER_KEY(32바이트 Base64)로 AES-256-GCM 봉투 암호화. PortalConsole 의envelopeEncryption과 동일 패턴을 따른다.- 테이블:
file_store_oauth_tokens(backend_id PK, token_ciphertext, key_version, updated_at) - 브라우저·설정 JSON·API 응답·로그에 토큰 원문을 절대 노출하지 않는다.
- 테이블:
- 연결 흐름(관리자 브라우저 리다이렉트):
- 관리자가 Google Cloud / Azure AD 에 앱 등록(clientId/secret) 후 FileStore 백엔드에 입력.
…/admin/backends/oauth-callback으로 consent → callback 에서 refresh token 을 봉투 암호화해 저장.- 어댑터가 refresh→access 자동 갱신. 만료/폐기는 감사 로그.
- 폴백: "refresh token 붙여넣기" 방식도 제공.
5.3 스토리지 어댑터 진화
ObjectStorage 인터페이스를 provider 능력(capability) 기반으로 확장한다.
export interface ObjectStorage {
put(key: string, body: Uint8Array, contentType: string): Promise<StoredObject>;
getReadUrl(key: string, expiresInSeconds?: number, context?: { assetId: string }): Promise<string>;
getWriteUrl(key: string, contentType: string, expiresInSeconds?: number, context?: { assetId: string }): Promise<string>;
head(key: string): Promise<StoredObject | undefined>;
delete(key: string): Promise<void>;
// 신규
streamTo?(key: string, target: NodeJS.WritableStream): Promise<void>; // 프록시 다운로드(Drive/OneDrive 등)
}
// provider 능력 선언
type StorageCapability = {
presignedUpload: boolean; // false → FileStore 경유 업로드
presignedRead: boolean; // false → FileStore 경유 다운로드
objectIdentity: "path" | "remoteId"; // Drive/OneDrive 는 remoteId(fileId/itemId)
oauth: boolean;
};path방식(R2/S3/로컬/네트워크): 현행storageKey= 객체 경로.remoteId방식(Drive/OneDrive): 생성 시 받은fileId/itemId를 파일의remote_ref에 저장하고,storageKey는 감사·일관성용 논리 키로 유지한다.
5.4 업로드/다운로드 흐름 (presigned vs 프록시)
- presigned 지원(S3/R2/로컬/네트워크): 현행 흐름 유지(
upload-session→ PUT →complete). - 프록시 업로드(Drive/OneDrive): presigned PUT 이 없으므로
upload-session이 presigned URL 대신 FileStore 경유 업로드 주소(LOCAL_VOLUME 의 서명 URL 패턴 재사용)를 반환하고, FileStore 가 받은 바이트를 Drive/OneDrive API 로 스트리밍한 뒤remote_ref를 기록한다.- 트레이드오프: 업로드 대역폭을 FileStore 가 부담(보안·흐름은 단순).
- 프록시 다운로드: read-url 은 Drive/OneDrive 의 단기 다운로드 링크를 그대로 노출하지 않고, FileStore 인증 endpoint 를 통한 locator(
streamTo)로 제공한다.
5.5 소비 포털 원장 (신규)
- 신규 테이블
file_store_consumers(id PK, label, enabled, notes, created_at, updated_at, portal_console_ref?). - 저장 정책의
consumerId를consumers.id로 참조(등록돼야 정책 생성 가능) → 오타·고아 정책 차단. - PortalConsole
portal_configurations와 선택 동기화(consumerId 일치). FileStore 는 자체 원장을 갖되, PortalConsole 을 정본으로 강제하지 않는다(독립 운영 가능).
5.6 포털 저장 정책 — 논리 구분
- 현행: "R2 안의 버킷"은 백엔드 속성(
backend.bucket) → 같은 R2 계정의 여러 버킷 = 백엔드 여러 개. - 변경: 정책에 선택
bucket오버라이드를 추가해, 하나의 백엔드(자격증명)로 여러 버킷에 라우팅할 수 있게 한다.- 정책:
backendId+bucket?+pathPrefix+ 용량·MIME·read/write·dataPublishAllowed.
- 정책:
- 객체 키 규칙(유지/확장):
{pathPrefix}/{consumerId}/{assetId}/{fileName}.
5.7 파일 메타데이터 — 출처 메타 확장
파일(AssetRecord)에 분석용 출처 메타를 추가한다.
| 신규 필드 | 의미 |
|---|---|
sourceMenuId | 소비 포탈 안에서 등록된 메뉴 식별자 |
sourceEntityType / sourceEntityId | 등록 출처 업무 객체(게시물 등) 종류/식별자 |
context (JSON) | 위 외 부가 분석 메타(스키마 유연) |
ownerType/ownerId는 하위호환 유지.consumerId는 어느 포털인지를 이미 담당.classification(PUBLIC/INTERNAL/CONFIDENTIAL/SECRET) 는 저장·추출·발행 전 구간에서 강제한다.- 트레이드오프: 정형 컬럼(menuId/entityType/entityId) + 부가
contextJSON 병행을 권장. 정형은 조인·필터·거버넌스에, JSON 은 스키마 변화 흡수에 쓴다. - 계약 영향:
upload-session요청 스키마 확장 +@hohyun94/portal-filestore어댑터가 신규 필드를 전달하도록 갱신 (소비 포탈이 메뉴·게시물 정보를 함께 넘겨야 한다).
6. 데이터 공급(Data Feed) 계층 (신규)
6.1 무엇을 제공하는가
| 대상 | 내용 | 1차 형식 |
|---|---|---|
| 메타데이터 카탈로그 | 파일 메타(포털·메뉴·게시물·분류·크기·체크섬·상태) | JSON/JSONL → Parquet 스냅샷 |
| 변경 이벤트(CDC) | CREATED/ACTIVATED/DELETED 등 lifecycle + 감사 | 단조증가 seq 기반 cursor 스트림 |
| 콘텐츠 | 원본 바이트(+ manifest) | 스테이징 버킷 배치 |
6.2 제공 방식 — pull + push (둘 다)
- pull(추출 API): ETL·Airflow·Spark 가 주기적으로 당겨감.
GET /api/file-store/v1/data-feed/assets?cursor=&limit=&since=&classification=(메타, 페이지네이션·필터)GET /api/file-store/v1/data-feed/events?cursor=(CDC — 파일별 단조증가seq, 재생·증분 적재 보장)- 서비스 토큰 인증 +
dataPublishAllowed정책 확인.
- push(DataSink 커넥터): FileStore 가 목적지로 직접 쓴다.
DataSink인터페이스로 추상화:S3/R2 스테이징(JSONL/Parquet),Kafka 토픽,REST 웹훅→ 이후Iceberg/Delta확장.- 소비 정책에
dataPublishAllowed+dataSinkId추가(ragPublishAllowed패턴의 일반화).
6.3 1차 연동 대상
Databricks(우선) → Snowflake. 데이터 플랫폼 저장소가 이 저장소(프로젝트)에 없으므로 외부 연동을 전제로 한다.
6.4 콘텐츠 제공 — 2단 전략 (확정)
| 층 | 제공 | 방식 | 이유 |
|---|---|---|---|
| 메타+CDC | 실시간/근실시간 | pull API + Delta CDF | 용량 작음, 분석 조인키·거버넌스 정본 |
| 콘텐츠 | 배치/온디맨드 | 스테이징 버킷 배치 export(+manifest) 또는 파일별 추출 API | 용량 큼, OCR/NLP/embedding 등 후속 배치에서만 소비 |
- 실시간 경로에 무거운 바이너리를 태우지 않아 비용·보안이 단순해진다.
- 콘텐츠 배치는
classification를 반영한다(CONFIDENTIAL/SECRET 제외·마스킹).
6.5 오픈 테이블 포맷 — 권장: Delta Lake (+ UniForm → Iceberg)
- 1순위 Databricks 이므로 Delta 가 네이티브(Unity Catalog·Auto Loader·
MERGE·time travel·schema evolution). - CDC 와 특히 잘 맞는다: Delta Change Data Feed(CDF) 로 증분 적재·변경 이벤트를 별도 로그 없이 충족.
- 2순위 Snowflake 는 Iceberg 를 네이티브로 읽음 → Databricks UniForm 이 Delta 를 Iceberg 로 프로젝션.
계층 구분(중요): FileStore 는 raw/bronze 생산자에 머물고 테이블은 데이터 플랫폼이 소유한다.
- FileStore 가 내보내는 것: 메타 Parquet 스냅샷 + Delta(append/CDF) 이벤트 + 콘텐츠 원본(+manifest).
- Databricks 가 그 위에 bronze/silver/gold Delta 를 구축. FileStore 가 Iceberg/Delta 라이터·metastore 를 직접 소유하지 않는다.
6.6 거버넌스 (필수 전제)
classification를 추출·발행 전 구간에서 강제.- 기본 제외:
QUARANTINED·PENDING_SCAN(승인된ACTIVE만). - 원본이 소비 포탈에 있어도 FileStore 가 메타 정본을 보유해야 거버넌스를 유지한 채 분석이 가능하다.
6.7 기존 RAG 발행과의 관계
RagPublisher(현재 console.info 플레이스홀더)를 일반화된 발행 레이어로 묶되 목적지·필터는 분리한다.- RAG:
ragPublishAllowed→ RAGCenter (기존) - Data Feed:
dataPublishAllowed+ sink → 데이터 플랫폼 (신규)
- RAG:
- 둘 다 동일한 audit/CDC 이벤트 로그를 원천으로 사용.
7. 데이터 모델 변경 요약
| 구분 | 변경 |
|---|---|
| 신규 테이블 | file_store_consumers, file_store_oauth_tokens, file_store_data_sinks, file_store_feed_cursors |
| 백엔드 | provider 확장(NETWORK_DRIVE/GOOGLE_DRIVE/ONEDRIVE), OAuth 메타(clientId/secretRef/oauth user) |
| 정책 | bucket? 오버라이드, dataPublishAllowed, dataSinkId? |
| 파일 | sourceMenuId, sourceEntityType, sourceEntityId, context, remote_ref, seq(CDC) |
| 이벤트 | 파일별 단조증가 seq (CDC cursor) |
8. 단계 로드맵
| 단계 | 내용 | 완료 기준 | 상태 |
|---|---|---|---|
| 0 | 설계 문서 확정(이 문서) | 리뷰 승인 | ✅ ae8405f |
| 1 | 설정 CRUD 를 React admin-web 으로 이전 + 소비포털 원장/정책 폼 | 화면에서 backend·consumer·policy 등록 가능 | ✅ 2e8a8d6 |
| 2 | NETWORK_DRIVE 추가(마운트 경로) + 어댑터 capability 모델 정리 | 로컬/네트워크 backend 등록·업로드 검증 | ✅ 691f077 |
| 3 | OAuth 토큰 보관 + GOOGLE_DRIVE/ONEDRIVE 어댑터(프록시 업/다운로드) | 실계정 1개로 업로드·다운로드·삭제 왕복 | ⚠️ 구조·단위검증 81476c2, 연결테스트 4a0c8fb, 실계정 검증은 자격증명 확보 후 |
| 4 | 메타 확장(sourceMenuId·sourceEntity*·context) + upload-session 계약/어댑터 갱신 | 소비 포탈이 출처 메타 전달, 파일 관리 필터 동작 | ✅ 365628f · 소비 어댑터 @hohyun94/portal-filestore 0.3.2 에서 source* 전송 |
| 5 | Data Feed pull API + CDC(seq) + 거버넌스 필터 | cursor 증분 추출 검증 | ✅ 67edead |
| 6 | DataSink push(S3/R2 스테이징 → Kafka → REST) + Delta/CDF landing | Databricks Auto Loader 적재 확인 | ⚠️ 오브젝트·웹훅 97d6816·커서 9d3b062·Kafka 9b90a2e, Databricks 실측은 이후 |
| 7 | Data Feed 운영 화면(커서·sink·이력) + 문서/감사 | 관리자 화면에서 상태 확인 | ✅ 58d8df6 |
남은 항목: 실계정 OAuth 검증(3, 자격증명 필요), Databricks Auto Loader 실측(6, 데이터 플랫폼 필요).
9. 검증·운영 규칙
- 로컬 선검증:
pnpm verify→pnpm verify:full→pnpm verify:all(admin-web 포함). - 원격 검증은 PR 에서 1회(
gh workflow run "Verify FileStore").mainpush 로는 원격 검증이 돌지 않는다(Coolify 배포만). - 커밋은 작업 단위 1회:
node scripts/agent-commit.mjs --message "..." --body "..." --push. - 자격증명·토큰 원문은 설정 파일·DB 평문·브라우저·로그에 저장하지 않는다(봉투 암호화·secretRef 유지).
10. 열린 질문
- NETWORK_DRIVE 를 "마운트 경로"로 한정 — 마운트 표준(NFS/SMB/CIFS)과 FileStore 컨테이너 마운트 방식 확정 필요.
- OAuth 앱 등록 주체(고객사별 vs 서비스 공용)와 redirect URI 정책.
- 콘텐츠 배치 export 의 주기·범위(전체 vs 증분)와 스테이징 버킷 소유.
sourceMenuId·sourceEntityId의 식별자 체계(소비 포탈 메뉴/게시물 ID 규약).- Data Feed 노출 범위: 소비 포탈별 자신의 데이터만 vs 분석 전용 광범위(거버넌스 규칙 확정).