351 lines
12 KiB
Markdown
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` 으로만 업로드 세션 접근 |
|
|
|
|
--- |