Files
cloud-sharp-docs/설계/db/개념적 설계.md
2026-03-16 16:17:17 +09:00

351 lines
12 KiB
Markdown

# 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` 으로만 업로드 세션 접근 |
---