Skip to content

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의 최종 목적은 세 가지다.

  1. 저장소 통합 관리 — 이기종 저장 백엔드(R2·S3·로컬 볼륨·네트워크 드라이브·구글드라이브·원드라이브)를 등록하고, 소비 포탈별로 어느 저장소(버킷·경로)를 쓸지 정책으로 결정한다.
  2. 메타데이터 원장 — 등록되는 파일의 출처(포털·메뉴·게시물)와 분류·무결성 메타데이터를 FileStore가 정본으로 보유한다. 원본 업무 데이터는 소비 포탈에 있어도, 분석에 필요한 메타는 FileStore가 갖는다.
  3. 데이터 파이프라인 공급 — 위 메타데이터와 변경 이벤트(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) ​

모델필드
StorageBackendprovider ∈ {CLOUDFLARE_R2, AMAZON_S3, LOCAL_VOLUME, S3_COMPATIBLE}, secretRef, endpoint, bucket, localRoot, region, enabled
ConsumerStoragePolicyconsumerId(자유 문자열·FK 아님), backendId, pathPrefix, maxBytes, allowedContentTypes, encryptionRequired, ragPublishAllowed, readEnabled/writeEnabled/enabled
AssetRecordconsumerId, 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 한계 (재설계가 필요한 지점) ​

  1. 저장소 종류가 R2·S3·로컬로 한정 — 네트워크 드라이브·구글드라이브·원드라이브 없음.
  2. 객체 위치가 항상 경로(key) 전제 — Drive/OneDrive 의 fileId/itemId 개념 없음.
  3. OAuth 기반 저장소를 위한 토큰 보관·갱신 구조 없음(현재는 secretRef 원칙만).
  4. 소비 포탈이 자유 문자열 consumerId — 등록 원장·오타/고아 정책 차단 없음.
  5. 파일에 출처 메타(어느 메뉴·게시물) 없음 — ownerType/ownerId 로 일부만 표현, menu 개념 부재.
  6. 분석용 공급 계층 없음 — RAG 발행(ragPublishAllowed→RAGCenter)만 존재. 데이터 플랫폼 연동·CDC·추출 API 부재.
  7. 설정 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 PUTpresigned GET키(path)secretRef(AK/SK)
AMAZON_S3기존presigned PUTpresigned GET키(path)secretRef
S3_COMPATIBLE기존presigned PUTpresigned GET키(path)secretRef
LOCAL_VOLUME기존서명 HTTP PUT서명/stream GET파일 경로없음
NETWORK_DRIVE신규로컬과 동일(마운트 경로)동일마운트 경로없음(마운트)
GOOGLE_DRIVE신규FileStore 경유(프록시)FileStore 경유(프록시)fileIdOAuth refresh token
ONEDRIVE신규FileStore 경유FileStore 경유itemIdOAuth 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 응답·로그에 토큰 원문을 절대 노출하지 않는다.
  • 연결 흐름(관리자 브라우저 리다이렉트):
    1. 관리자가 Google Cloud / Azure AD 에 앱 등록(clientId/secret) 후 FileStore 백엔드에 입력.
    2. …/admin/backends/oauth-callback 으로 consent → callback 에서 refresh token 을 봉투 암호화해 저장.
    3. 어댑터가 refresh→access 자동 갱신. 만료/폐기는 감사 로그.
    • 폴백: "refresh token 붙여넣기" 방식도 제공.

5.3 스토리지 어댑터 진화 ​

ObjectStorage 인터페이스를 provider 능력(capability) 기반으로 확장한다.

ts
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) + 부가 context JSON 병행을 권장. 정형은 조인·필터·거버넌스에, 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 → 데이터 플랫폼 (신규)
  • 둘 다 동일한 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
2NETWORK_DRIVE 추가(마운트 경로) + 어댑터 capability 모델 정리로컬/네트워크 backend 등록·업로드 검증✅ 691f077
3OAuth 토큰 보관 + GOOGLE_DRIVE/ONEDRIVE 어댑터(프록시 업/다운로드)실계정 1개로 업로드·다운로드·삭제 왕복⚠️ 구조·단위검증 81476c2, 연결테스트 4a0c8fb, 실계정 검증은 자격증명 확보 후
4메타 확장(sourceMenuId·sourceEntity*·context) + upload-session 계약/어댑터 갱신소비 포탈이 출처 메타 전달, 파일 관리 필터 동작✅ 365628f · 소비 어댑터 @hohyun94/portal-filestore 0.3.2 에서 source* 전송
5Data Feed pull API + CDC(seq) + 거버넌스 필터cursor 증분 추출 검증✅ 67edead
6DataSink push(S3/R2 스테이징 → Kafka → REST) + Delta/CDF landingDatabricks Auto Loader 적재 확인⚠️ 오브젝트·웹훅 97d6816·커서 9d3b062·Kafka 9b90a2e, Databricks 실측은 이후
7Data 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"). main push 로는 원격 검증이 돌지 않는다(Coolify 배포만).
  • 커밋은 작업 단위 1회: node scripts/agent-commit.mjs --message "..." --body "..." --push.
  • 자격증명·토큰 원문은 설정 파일·DB 평문·브라우저·로그에 저장하지 않는다(봉투 암호화·secretRef 유지).

10. 열린 질문 ​

  1. NETWORK_DRIVE 를 "마운트 경로"로 한정 — 마운트 표준(NFS/SMB/CIFS)과 FileStore 컨테이너 마운트 방식 확정 필요.
  2. OAuth 앱 등록 주체(고객사별 vs 서비스 공용)와 redirect URI 정책.
  3. 콘텐츠 배치 export 의 주기·범위(전체 vs 증분)와 스테이징 버킷 소유.
  4. sourceMenuId·sourceEntityId 의 식별자 체계(소비 포탈 메뉴/게시물 ID 규약).
  5. Data Feed 노출 범위: 소비 포탈별 자신의 데이터만 vs 분석 전용 광범위(거버넌스 규칙 확정).