# Cloud# — 데이터베이스 개념적 설계서 > 본 문서는 Cloud# 서비스의 핵심 엔티티에 대한 개념적 DB 설계를 정의한다. > 물리적 DDL이 아닌 **엔티티의 역할, 컬럼 의미, 설계 의도, 제약 조건**을 중심으로 기술한다. --- ## 목차 1. [엔티티 목록](#1. 엔티티 목록) 2. [ER 다이어그램](#2-er-다이어그램) 3. [엔티티 상세 설계](#3-엔티티-상세-설계) - [3.1 User](#31-user) - [3.2 Folder](#32-folder) - [3.3 FileItem](#33-fileitem) - [3.4 UploadSession](#34-uploadsession) 4. [관계 정의](#4-관계-정의) 5. [제약 조건 요약](#5-제약-조건-요약) 6. [설계 결정 사항 (Decision Log)](#6-설계-결정-사항-decision-log) --- ## 1. 엔티티 목록 | 엔티티 | 설명 | |--------|------| | `User` | 사용자 인증 정보 및 스토리지 한도 관리 | | `Folder` | 사용자별 폴더 트리 구조 표현 | | `FileItem` | 파일 메타데이터 및 저장소 객체 연결 | | `UploadSession` | tus 기반 업로드 진행 상태 추적 | --- ## 2. ER 다이어그램 ![[개념적설계_erd.svg]] --- ## 3. 엔티티 상세 설계 --- ### 3.1 User > 사용자의 인증 정보와 스토리지 한도를 관리하는 엔티티. #### 주요 역할 - 로그인 식별자 관리 - 권한(role) 관리 - 전체 사용 가능 용량 / 사용 중 용량 관리 #### 컬럼 정의 | 컬럼명 | 타입 | Null | 설명 | | ----------------------- | ------------- | -------- | ------------------------ | | `id` | UUID / BIGINT | NOT NULL | PK — 내부 식별자 | | `email` | VARCHAR | NOT NULL | 로그인 식별자. **UNIQUE** | | `password_hash` | VARCHAR | NOT NULL | 해시된 비밀번호 | | `display_name` | VARCHAR | NULL | 표시 이름 | | `role` | ENUM | NOT NULL | `ADMIN` \| `USER` 등 | | `storage_allowed_bytes` | BIGINT | NULL | 허용 용량. `NULL` 이면 **무제한** | | `storage_used_bytes` | BIGINT | NOT NULL | 현재 사용 중 용량. 기본값 `0` | | `created_at` | TIMESTAMP | NOT NULL | 계정 생성 일시 | | `updated_at` | TIMESTAMP | NOT NULL | 마지막 수정 일시 | | `deleted_at` | TIMESTAMP | NULL | 소프트 삭제 일시 | #### 설계 포인트 - `email` 은 **UNIQUE** 제약으로 중복 가입 방지 - `storage_allowed_bytes` 가 `NULL` 이면 무제한 처리 - 애플리케이션 레이어에서 `NULL` 여부를 먼저 확인 - `storage_used_bytes` 는 **집계 캐시 컬럼**으로 운영 - 즉시 조회 성능은 우수하나, 파일 업로드·삭제 시 **반드시 동기화** 필요 - 정합성 보장을 위해 트랜잭션 내에서 함께 갱신 --- ### 3.2 Folder > 사용자별 폴더 트리를 표현하는 엔티티. #### 주요 역할 - 폴더 계층 구조 표현 - 탐색기 경로 구성 - 파일의 논리적 위치 제공 #### 컬럼 정의 | 컬럼명 | 타입 | Null | 설명 | | ------------------ | ------------- | -------- | ----------------------------- | | `id` | UUID / BIGINT | NOT NULL | PK | | `owner_user_id` | FK → User | NOT NULL | 소유 사용자 | | `parent_folder_id` | FK → Folder | NULL | 부모 폴더. `NULL` 이면 **루트 폴더** | | `name` | VARCHAR | NOT NULL | 폴더 이름 (부모 기준 상대명) | | `full_path` | VARCHAR | NULL | 전체 경로 캐시 (예: `/문서/프로젝트/2025`) | | `created_at` | TIMESTAMP | NOT NULL | 생성 일시 | | `updated_at` | TIMESTAMP | NOT NULL | 수정 일시 | | `deleted_at` | TIMESTAMP | NULL | 소프트 삭제 일시 | #### 설계 포인트 **자기참조 트리 구조** ``` parent_folder_id IS NULL → 루트 폴더 parent_folder_id = X → X 폴더의 하위 폴더 ``` **`full_path` 컬럼의 위치** `full_path` 는 원본 진실(source of truth)이 아닌 **조회 최적화용 캐시 컬럼**이다. | 구분 | 컬럼 | 역할 | |------|------|------| | **원본 진실** | `id`, `parent_folder_id`, `name` | 실제 구조의 기준 | | **캐시** | `full_path` | 목록 조회·검색·WebDAV 경로 변환·breadcrumb 표시용 | - 폴더 이름 변경·이동 시 `full_path` 와 하위 폴더의 `full_path` 를 **함께 갱신**해야 함 - 불일치 발생 시 `id` / `parent_folder_id` 기준으로 재계산 가능 **유니크 제약** 같은 부모 폴더 아래에서 동일한 이름의 폴더는 허용하지 않는다. ``` UNIQUE (owner_user_id, parent_folder_id, name) -- 활성 데이터 기준 ``` - 소프트 삭제 구현 시 `deleted_at IS NULL` 조건부 유니크 인덱스 권장 **루트 폴더 정책** - 사용자당 루트 폴더(`parent_folder_id IS NULL`) 는 **1개** 권장 - 애플리케이션 레이어 또는 DB 제약으로 강제 --- ### 3.3 FileItem > 사용자 관점에서 보이는 파일 자체의 메타데이터를 관리하는 엔티티. #### 주요 역할 - 탐색기 파일 목록 조회 - 파일명, MIME, 크기, 미리보기 상태 관리 - 실제 저장소(LocalFS / S3)의 객체와 연결 #### 컬럼 정의 | 컬럼명 | 타입 | Null | 설명 | |--------|------|------|------| | `id` | UUID / BIGINT | NOT NULL | PK | | `owner_user_id` | FK → User | NOT NULL | 소유 사용자 | | `folder_id` | FK → Folder | NOT NULL | 소속 폴더 | | `display_name` | VARCHAR | NOT NULL | 사용자에게 보이는 파일 이름 | | `storage_key` | VARCHAR | NOT NULL | 실제 저장소 객체 키. **UNIQUE** | | `size_bytes` | BIGINT | NOT NULL | 파일 크기 (`>= 0`) | | `mime_type` | VARCHAR | NULL | MIME 타입 (예: `image/jpeg`) | | `checksum_sha256` | VARCHAR | NULL | 무결성 검증용 해시 | | `preview_status` | ENUM | NOT NULL | 미리보기 파이프라인 상태 | | `metadata_json` | JSON / TEXT | NULL | 파일 종류별 가변 메타데이터 | | `created_at` | TIMESTAMP | NOT NULL | 업로드 완료 일시 | | `updated_at` | TIMESTAMP | NOT NULL | 수정 일시 | | `deleted_at` | TIMESTAMP | NULL | 소프트 삭제 일시 | #### 설계 포인트 **`storage_key` 와 `display_name` 분리** ``` display_name : 사용자에게 보이는 이름 → "보고서 최종.pdf" storage_key : 저장소의 실제 키 → "user/12/ab/cd/f83e...uuid.bin" ``` - 파일 이름 변경 시 `display_name` 만 변경하고 저장소 객체는 그대로 유지 - `storage_key` 가 외부에 노출되지 않도록 주의 **`metadata_json` 활용 예시** ```json // 이미지 { "width": 1920, "height": 1080 } // 비디오 { "duration": 3600, "codec": "h264", "bitrate": 4000 } // 문서 { "page_count": 42 } ``` **`preview_status` 상태 흐름** ``` PENDING → PROCESSING → DONE → FAILED (미지원 포맷) → UNSUPPORTED ``` **파일명 중복 정책** 같은 폴더 내에서 동일한 파일 이름은 허용하지 않는다. ``` UNIQUE (owner_user_id, folder_id, display_name) -- 활성 데이터 기준 ``` - 소프트 삭제 구현 시 `deleted_at IS NULL` 조건부 유니크 인덱스 권장 --- ### 3.4 UploadSession > tus 기반 대용량 업로드의 진행 상태를 관리하는 엔티티. #### 주요 역할 - 업로드 시작 ~ 완료까지의 진행 상태 추적 - 실패·중단·재개 처리 - 임시 저장 위치와 최종 `FileItem` 생성 연결 #### 컬럼 정의 | 컬럼명 | 타입 | Null | 설명 | |--------|------|------|------| | `id` | UUID / BIGINT | NOT NULL | PK | | `owner_user_id` | FK → User | NOT NULL | 업로드 요청 사용자 | | `target_folder_id` | FK → Folder | NOT NULL | 완료 후 파일이 위치할 폴더 | | `token` | VARCHAR | NOT NULL | 외부 공개용 식별자. **UNIQUE** | | `tus_upload_id` | VARCHAR | NULL | tus 서버와의 매핑 ID. UNIQUE 가능 | | `status` | ENUM | NOT NULL | 업로드 상태 (아래 상태 흐름 참고) | | `expected_size` | BIGINT | NOT NULL | 전체 파일 크기 (bytes) | | `received_size` | BIGINT | NOT NULL | 현재까지 수신된 크기 (bytes) | | `original_name` | VARCHAR | NOT NULL | 업로드 원본 파일명 | | `mime_type` | VARCHAR | NULL | MIME 타입 | | `storage_key_temp` | VARCHAR | NULL | 임시 저장 경로 | | `file_item_id` | FK → FileItem | NULL | 완료 후 생성된 FileItem 참조 | | `created_at` | TIMESTAMP | NOT NULL | 업로드 세션 생성 일시 | | `expires_at` | TIMESTAMP | NULL | 세션 만료 일시 | | `completed_at` | TIMESTAMP | NULL | 업로드 완료 일시 | #### 설계 포인트 **`token` 과 내부 `id` 분리** ``` id : 내부 DB 식별자 → 외부 노출 금지 token : 클라이언트가 업로드 재개 시 사용하는 공개 식별자 ``` **`status` 상태 흐름** ``` CREATED → UPLOADING → COMPLETED → FAILED → CANCELLED → EXPIRED ``` **업로드 완료 처리 흐름** 업로드 완료 시 아래 세 작업을 **하나의 트랜잭션**으로 처리한다. ``` 1. UploadSession.status → COMPLETED 2. FileItem 신규 생성 3. User.storage_used_bytes += size_bytes ``` **`received_size` 제약** ``` received_size <= expected_size -- 항상 유지 ``` --- ## 4. 관계 정의 | 관계 | 카디널리티 | 설명 | |------|-----------|------| | `User` → `Folder` | 1 : N | 한 사용자는 여러 폴더를 소유 | | `Folder` → `Folder` | 1 : N | 폴더는 하위 폴더를 가짐 (자기참조) | | `User` → `FileItem` | 1 : N | 한 사용자는 여러 파일을 소유 | | `Folder` → `FileItem` | 1 : N | 한 폴더는 여러 파일을 포함 | | `User` → `UploadSession` | 1 : N | 한 사용자는 여러 업로드 세션을 가짐 | | `Folder` → `UploadSession` | 1 : N | 한 폴더는 여러 업로드 세션의 목적지가 됨 | | `UploadSession` → `FileItem` | 1 : 0..1 | 완료된 세션은 하나의 FileItem을 생성 | --- ## 5. 제약 조건 요약 ### User | 제약 | 내용 | |------|------| | PK | `id` | | UNIQUE | `email` | | CHECK | `storage_used_bytes >= 0` | ### Folder | 제약 | 내용 | |------|------| | PK | `id` | | FK | `owner_user_id` → `User.id` NOT NULL | | FK | `parent_folder_id` → `Folder.id` NULL 허용 | | UNIQUE | `(owner_user_id, parent_folder_id, name)` — 활성 데이터 기준 | | 정책 | 사용자당 루트 폴더(`parent_folder_id IS NULL`) 1개 권장 | ### FileItem | 제약 | 내용 | |------|------| | PK | `id` | | FK | `owner_user_id` → `User.id` NOT NULL | | FK | `folder_id` → `Folder.id` NOT NULL | | UNIQUE | `storage_key` | | UNIQUE | `(owner_user_id, folder_id, display_name)` — 활성 데이터 기준 | | CHECK | `size_bytes >= 0` | ### UploadSession | 제약 | 내용 | |------|------| | PK | `id` | | FK | `owner_user_id` → `User.id` NOT NULL | | FK | `target_folder_id` → `Folder.id` NOT NULL | | FK | `file_item_id` → `FileItem.id` NULL 허용 | | UNIQUE | `token` | | UNIQUE | `tus_upload_id` (NULL 제외) | | CHECK | `received_size <= expected_size` | | CHECK | `expected_size >= 0` | --- ## 6. 설계 결정 사항 (Decision Log) | # | 결정 사항 | 이유 | |---|----------|------| | 1 | `storage_used_bytes` 를 집계 캐시 컬럼으로 관리 | 매 조회 시 SUM 집계 대신 즉시 응답. 대신 업로드·삭제 트랜잭션에서 동기화 필수 | | 2 | `full_path` 를 캐시 컬럼으로 정의 | 폴더 이동·이름 변경 시 하위 경로 전체 갱신이 필요하지만, 조회·WebDAV·breadcrumb 성능 확보 | | 3 | `storage_key` 와 `display_name` 분리 | 파일 이름 변경이 저장소 객체 이동 없이 메타데이터 변경만으로 처리 가능 | | 4 | `UploadSession` 완료 처리를 단일 트랜잭션으로 | `FileItem` 생성과 `storage_used_bytes` 증가의 정합성 보장 | | 5 | 소프트 삭제 (`deleted_at`) 적용 | 실수 복구, 휴지통 기능 지원. 유니크 제약은 `deleted_at IS NULL` 조건부 인덱스로 처리 | | 6 | `token` (외부) / `id` (내부) 식별자 분리 | 내부 PK 노출 방지. 클라이언트는 `token` 으로만 업로드 세션 접근 | ---