Files
irukseo/projects/CloudSharp-백엔드-포트폴리오-분석.md
2026-05-05 21:27:37 +09:00

1268 lines
54 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CloudSharp — 백엔드 + 인프라 + 설계 포트폴리오 분석
> **작성 기준일:** 2026-05-05
> **분석 범위:** `apps/backend/`, `infra/`, `docs/.llm/` — 프론트엔드 제외
> **주의:** 확인되는 내용만 코드 기반으로 작성. 미구현/미확인 항목은 "확인 필요" 또는 "예정"으로 표시.
> **구조:** "문제 → 설계 선택 → 구현 방식 → 결과" 지향. 단순 기능 나열이 아닌 설계 판단 기록.
---
## 1. 프로젝트 목적
### 1.1 해결하려는 문제
- 기존 클라우드 스토리지는 **개인 계정 중심**으로 설계되어 있다.
- 팀/프로젝트 단위로 **완전히 독립된 파일 저장 공간**이 필요한 경우, 폴더 공유만으로는 격리·권한·Quota를 충분히 통제할 수 없다.
- 셀프호스트 환경에서 **대용량 파일의 중단 재개 업로드**와 안전한 공유가 필요하다.
### 1.2 주요 사용자
- 셀프호스트를 선호하는 개인, 팀, 프로젝트 그룹, 소규모 조직
- Space 단위로 파일을 저장·공유·협업하려는 사용자
- MCP/AI 연계를 원하는 파워유저 (향후)
### 1.3 프로젝트 목표
**Space**라는 개념을 중심으로, 완전히 격리된 팀 저장 공간을 제공하는 셀프호스팅 파일 서비스 플랫폼을 구축한다. MVP는 Docker Compose로 간편하게 배포할 수 있어야 한다.
### 1.4 백엔드가 담당하는 핵심 책임
| 책임 | 설명 |
|------|------|
| **인증/인가** | opaque session token 기반 사용자 인증 + Space Role 기반 인가 |
| **메타데이터 관리** | Space, Folder, FileItem, ShareLink, UploadSession, DownloadSession의 진실 원천(PostgreSQL) |
| **업로드 파이프라인** | tusd와 연계한 업로드 세션 생성 → 전송 → Finalize까지 전체 생명주기 관리 |
| **Quota 정책** | Space 단위 `used + reserved + expected <= allowed` 판정 및 예약/확정/해제 정합성 |
| **파일 저장 추상화** | Local FS, MinIO, S3를 동일한 storage_key로 추상화 |
| **다운로드 보안** | 단명(5분 TTL) DownloadSession 기반 Zero Information Leak 정책 |
| **공유 제어** | 내부 협업(SpaceMember/SpaceInvite)과 외부 공유(ShareLink) 분리 |
| **파일명 충돌 정책** | 자동 리네임 없이 실패 반환 |
| **Finalize 동시성 제어** | CAS(Compare-And-Swap) 기반 중복 실행 방지 + Recovery Worker |
### 1.5 인프라 구성이 필요한 이유
- PostgreSQL: 메타데이터 진실 원천
- Redis: 사용자 세션 저장소(opaque session), 캐시, Redis Pub/Sub 이벤트 전달
- tusd: tus 프로토콜 기반 재개 가능한 대용량 청크 업로드 (Go 바이너리)
- Local FS: 물리 파일 저장
- nginx: Reverse Proxy — 단일 진입점, 스트리밍 버퍼링 해제, tus 헤더 포워딩
- ffmpeg/AI Worker (향후): 썸네일, 메타데이터 추출 등 비동기 후처리
### 1.6 프로젝트 한 줄
> **Cloud#**은 Space 단위의 완전 격리형 저장 공간을 제공하는, tus 기반 재개 가능 업로드와 단명 다운로드 세션을 갖춘 셀프호스트 파일 서비스다.
---
## 2. 전체 아키텍처
### 2.1 아키텍처 유형: **모듈러 모놀리스 (Simplified Clean Architecture)**
- MVP에서는 하나의 ASP.NET Core 백엔드로 시작하고, 서비스 분리는 실제 병목이 생긴 뒤 결정한다.
- 다만 프로젝트 내부는 Clean Architecture로 레이어를 엄격히 분리한다.
### 2.2 전체 구성 요소 관계
```mermaid
flowchart TB
subgraph External["외부"]
CLIENT["Browser / Client"]
end
subgraph DockerHost["Docker Compose Host"]
NGINX["nginx :80"]
API["ASP.NET Core API<br/>:8080"]
TUSD["tusd :1080"]
PG[("PostgreSQL :5432")]
REDIS[("Redis :6379")]
STORAGE[("Local FS<br/>/data/storage")]
end
CLIENT -->|"HTTP :8080"| NGINX
NGINX -->|"/api/*"| API
NGINX -->|"/files/*"| TUSD
API --> PG
API --> REDIS
API --> STORAGE
API -.->|"internal hook"| TUSD
TUSD --> STORAGE
TUSD -->|"hook callback"| API
```
### 2.3 외부 요청 → 내부 서비스 흐름
```
Browser Request
→ nginx (Reverse Proxy)
→ /api/* → ASP.NET Core API
→ PostgreSQL (메타데이터)
→ Redis (세션 검증)
→ Local FS (파일 읽기/쓰기)
→ /files/* → tusd (청크 업로드/다운로드)
→ API hook callback (pre-create, post-finish 등)
→ Local FS (임시 저장 → 최종 이동)
```
### 2.4 서비스 간 책임 분리
| 컴포넌트 | 책임 | 알고 있는 것 |
|----------|------|-------------|
| **nginx** | 리버스 프록시, 라우팅, tus 헤더 포워딩, 버퍼링 해제 | API, tusd 위치 |
| **ASP.NET API** | 권한·정책·메타데이터·finalize·공유·검색·세션 관리 | PostgreSQL, Redis, Local FS, tusd hook |
| **tusd** | tus 프로토콜 청크 업로드 수신, 파일 저장, hook 호출 | Local FS, API hook endpoint |
| **PostgreSQL** | 서비스의 진실 원천 (모든 메타데이터) | 없음 |
| **Redis** | 세션 저장소, Pub/Sub 이벤트 버스 | 없음 |
| **Local FS** | 물리 파일 바이트 저장 | 없음 |
### 2.5 외부 공개 vs 내부 서비스 구분
| 구분 | 경로 | 인증 |
|------|------|------|
| **내부 API** | `/api/v1/*` | `Authorization: Bearer {opaque_session_token}` |
| **외부 공개 API** | `/public/v1/*` | share_token + 비밀번호 |
| **내부 전용** | `/api/internal/*` | `X-CloudSharp-Internal-Token` (서버 간) |
| **tusd hook** | `/internal/tusd/hooks` | tusd 전용 |
### 2.6 아키텍처 설계 의도
- **모듈러 모놀리스 선택 이유**: MVP에서 마이크로서비스의 운영 복잡도(분산 트랜잭션, 서비스 디스커버리, 로그 수집)를 피하면서도, Clean Architecture로 도메인 경계를 코드 레벨에서 명확히 분리한다.
- **nginx를 앞에 둔 이유**: 단일 진입점으로 CORS/HTTPS 관리 단순화, tusd 스트리밍에 필요한 버퍼링 해제 및 장시간 타임아웃 설정.
- **tusd를 분리한 이유**: 업로드 전송(tus 프로토콜)은 ASP.NET보다 Go 기반 tusd가 효율적이며, 업로드 plane과 API plane을 분리해 장애 격리 및 독립 확장이 가능하다.
---
## 3. API 설계
### 3.1 기본 계약
| 항목 | 값 |
|------|-----|
| 내부 API base path | `/api/v1` |
| 외부 공개 base path | `/public/v1` |
| 인증 방식 | `Authorization: Bearer {cs_sess_...}` (opaque session token) |
| 기본 데이터 형식 | `application/json` |
| API 버전 관리 | URL path 기반 (`v1`) |
| 문서화 | Swagger UI (`/swagger`), OpenAPI 3.x (`/openapi/v1/openapi.yaml`), ReDoc |
| OpenAPI bearer format | `bearerFormat: opaque` (JWT가 아님) |
### 3.2 구현 상태 범례
| 상태 | 의미 |
|------|------|
| `구현됨` | 현재 API 앱에 매핑 완료 |
| `내부 구현됨` | tusd hook / 내부 인증 전용 |
| `예정` | 계약 초안만 존재, 엔드포인트 미구현 |
### 3.3 구현된 API 목록
#### 인증 및 사용자
| Method | Path | 설명 | 상태 |
|--------|------|------|------|
| `GET` | `/api/v1/health` | Docker/API health check | `구현됨` |
| `POST` | `/api/v1/auth/register` | 회원가입 + 세션 토큰 발급 | `구현됨` |
| `POST` | `/api/v1/auth/login` | 로그인 (→ 미구현) | `예정` |
| `POST` | `/api/v1/auth/logout` | 로그아웃 | `예정` |
| `POST` | `/api/v1/me` | 내 프로필 조회 | `예정` |
**구현된 Request DTO (`RegisterRequest`):**
```csharp
// 실제 코드
public sealed record RegisterRequest(
[property: Required] string Email,
[property: Required] string Username,
[property: Required] string DisplayName,
[property: Required] string Password
);
```
**구현된 Response (`AuthResponse`):**
```csharp
// 실제 코드
public sealed record AuthResponse(
string AccessToken, // cs_sess_... opaque token
string TokenType, // "Bearer"
long ExpiresInSeconds,
UserProfileResponse User
);
```
#### Space
| Method | Path | 설명 | 최소 Role | 상태 |
|--------|------|------|-----------|------|
| `GET` | `/api/v1/spaces` | 내 Space 목록 | `VIEWER` | `예정` |
| `POST` | `/api/v1/spaces` | Space 생성 | 로그인 | `예정` |
| `GET` | `/api/v1/spaces/{spaceSlug}` | Space 상세 | `VIEWER` | `예정` |
| `PATCH` | `/api/v1/spaces/{spaceSlug}` | 이름/설명 변경 | `ADMIN` | `예정` |
| `DELETE` | `/api/v1/spaces/{spaceSlug}` | 소프트 삭제 | `OWNER` | `예정` |
| `GET` | `/api/v1/spaces/{spaceSlug}/quota` | Quota 조회 | `ADMIN` | `예정` |
| `PATCH` | `/api/v1/spaces/{spaceSlug}/quota` | Quota 변경 | `OWNER` | `예정` |
#### 폴더
| Method | Path | 설명 | 최소 Role | 상태 |
|--------|------|------|-----------|------|
| `GET` | `/api/v1/spaces/{spaceSlug}/folders/{folderId}/children` | 자식 목록 | `VIEWER` | `구현됨` |
| `POST` | `/api/v1/spaces/{spaceSlug}/folders` | 폴더 생성 | `MEMBER` | `구현됨` |
| `PATCH` | `/api/v1/spaces/{spaceSlug}/folders/{folderId}` | 이름 변경/이동 | `MEMBER` | `구현됨` |
| `DELETE` | `/api/v1/spaces/{spaceSlug}/folders/{folderId}` | 폴더 삭제 | `MEMBER` | `구현됨` |
**실제 정렬 쿼리 (코드 기반):**
```
sortBy = Name | Size | UpdatedAt
sortDirection = Asc | Desc
```
#### 파일
| Method | Path | 설명 | 상태 |
|--------|------|------|------|
| `PATCH` | `/api/v1/spaces/{spaceSlug}/files/{fileId}` | 파일명 변경/이동 | `예정` |
| `DELETE` | `/api/v1/spaces/{spaceSlug}/files/{fileId}` | 파일 삭제 | `예정` |
#### 업로드
| Method | Path | 설명 | 최소 Role | 상태 |
|--------|------|------|-----------|------|
| `POST` | `/api/v1/spaces/{spaceSlug}/upload-sessions` | 업로드 세션 생성 | `MEMBER` | `구현됨` |
| `GET` | `/api/v1/spaces/{spaceSlug}/upload-sessions/{token}` | 세션 상태 조회 | `MEMBER` | `구현됨` |
| `POST` | `/api/internal/uploads/uploading` | tus 전송 시작 반영 | 내부 인증 | `내부 구현됨` |
| `POST` | `/api/internal/uploads/finalize` | Finalize 수행 | 내부 인증 | `내부 구현됨` |
| `POST` | `/internal/tusd/hooks` | tusd HTTP hook 수신 | tusd | `내부 구현됨` |
**업로드 세션 생성 Request (실제 코드):**
```csharp
public sealed record CreateUploadSessionRequest(
[property: Required] long TargetFolderId,
[property: Required] [property: MaxLength(255)] string OriginalName,
[property: Required] [property: Range(1, long.MaxValue)] long ExpectedSize,
[property: MaxLength(255)] string? ClientMimeType,
[property: MaxLength(64)] string? Checksum
);
```
#### 다운로드
| Method | Path | 설명 | 상태 |
|--------|------|------|------|
| `POST` | `/api/v1/spaces/{spaceSlug}/files/{fileId}/download-sessions` | 다운로드 세션 발급 | `예정` |
| `GET` | `/public/v1/download-sessions/{sessionToken}/stream` | 실제 스트리밍 | `예정` |
#### 공유 링크
| Method | Path | 설명 | 상태 |
|--------|------|------|------|
| `POST` | `/api/v1/share-links` | 공유 링크 생성 | `예정` |
| `PATCH` | `/api/v1/share-links/{shareLinkId}` | 옵션 수정 | `예정` |
| `DELETE` | `/api/v1/share-links/{shareLinkId}` | 비활성화 | `예정` |
| `POST` | `/public/v1/share-links/{shareToken}/verify` | 유효성 검증 | `예정` |
| `POST` | `/public/v1/share-links/{shareToken}/browse` | 열람 | `예정` |
| `POST` | `/public/v1/share-links/{shareToken}/download-sessions` | 공유 기반 다운로드 | `예정` |
### 3.4 공통 응답 구조
**성공 응답** — 리소스 중심 JSON, 공통 `data` envelope 없음.
**실패 응답 구조 (실제 `ErrorResponse` 코드):**
```json
{
"requestId": "req_01JXYZ...",
"error": {
"code": "FILE_NAME_CONFLICT",
"message": "같은 이름의 파일이 이미 존재합니다.",
"details": [
{
"field": "displayName",
"reason": "conflict"
}
]
}
}
```
### 3.5 대표 비즈니스 에러 코드
| 코드 | 설명 | HTTP 상태 |
|------|------|-----------|
| `AUTH_REQUIRED` | 인증 토큰 누락/만료 | 401 |
| `INVALID_CREDENTIALS` | 로그인 실패 | 401 |
| `SPACE_ACCESS_DENIED` | Space 접근 권한 없음 | 403 |
| `FILE_NAME_CONFLICT` | 동일 폴더 내 파일명 충돌 | 409 |
| `FOLDER_NAME_CONFLICT` | 동일 부모 내 폴더명 충돌 | 409 |
| `QUOTA_EXCEEDED` | 업로드 시작 전 quota 초과 | 413/422 |
| `QUOTA_EXCEEDED_FINALIZE` | finalize 직전 quota 재검사 실패 | 422 |
| `UPLOAD_SESSION_EXPIRED` | 업로드 세션 만료 | 410 |
| `DOWNLOAD_NOT_AVAILABLE` | 삭제·손상·격리로 다운로드 불가 | 404 |
| `SHARE_LINK_EXPIRED` | 공유 링크 만료 | 404 |
| `SHARE_LINK_PASSWORD_REQUIRED` | 비밀번호 필요한 링크 | 401 |
| `UNSUPPORTED_PREVIEW_TYPE` | 미리보기 미지원 | 415 |
---
## 4. 인증/인가 설계
### 4.1 설계 결정: **Opaque Session Token (≠ JWT)**
```
Authorization: Bearer cs_sess_{256-bit CSPRNG random}
```
### 4.2 왜 JWT가 아니라 opaque session token인가
CloudSharp의 권한 모델 특성상 JWT의 self-contained claim이 오히려 단점이 된다:
| 전제 | Opaque token이 더 적합한 이유 |
|------|-------------------------------|
| 같은 사용자가 여러 Space에서 서로 다른 Role을 가진다 | JWT에 모든 Role을 담으면 토큰 크기 증가, Role 변경 시 stale |
| Space Role 변경이 즉시 반영되어야 한다 | opaque token은 매 요청마다 최신 DB 권한을 조회하므로 stale 문제 없음 |
| 계정 정지, 세션 강제 만료가 필요하다 | opaque는 Redis key 삭제만으로 즉시 revoke 가능 |
| 다운로드 세션은 별도 단명 토큰이 필요하다 | 목적별 토큰 분리가 자연스럽다 |
### 4.3 토큰 종류 구분
| 토큰 | 용도 | 형식 | Revoke |
|------|------|------|--------|
| **사용자 세션 토큰** | 내부 API 로그인 상태 | `cs_sess_{random}` | 즉시 (Redis) |
| **다운로드 세션 토큰** | 실제 파일 스트리밍 | 단명 opaque | TTL 내 미지원 |
| **공유 링크 토큰** | 외부 공유 접근 | opaque | 링크 상태 조회 시 |
| **초대 토큰** | Space 초대 수락 | opaque | 초대 상태 조회 시 |
### 4.4 토큰 생성 규칙 (실제 코드 기반)
```text
1. CSPRNG.GetBytes(32) → 256-bit 난수
2. accessToken = "cs_sess_" + base64url(randomBytes)
3. tokenHash = HMAC-SHA-256(accessToken, serverSecret)
4. Redis key = "auth:session:{tokenHash}"
5. Redis에 원문 저장 금지, token_hash만 저장
```
### 4.5 Redis 세션 모델
| Redis Key | Type | 설명 |
|-----------|------|------|
| `auth:session:{token_hash}` | Hash | 단일 세션 본문 (user_id, status, idle/absolute 만료 등) |
| `auth:user_sessions:{user_id}` | Set | 특정 사용자의 활성 세션 인덱스 |
### 4.6 세션 수명 정책
| 항목 | 값 |
|------|-----|
| Idle timeout | 12시간 (미사용 시 만료) |
| Absolute timeout | 30일 (사용 중이어도 최대) |
| Sliding renewal | idle 만료만 연장, absolute는 연장 안 함 |
### 4.7 ASP.NET Core 인증 아키텍처
```
요청 진입
→ CloudSharpSessionAuthenticationHandler
1. Authorization 헤더 존재 확인
2. "Bearer " prefix 확인
3. opaque token hash 계산
4. Redis UserSession 조회
5. 세션 상태 + 만료 확인
6. DB 사용자 상태 및 system_role 확인
7. 최소 claim(sub, sid, system_role)만 담은 ClaimsPrincipal 생성
→ RequireSpacePermissionFilter (Endpoint Filter)
1. route의 spaceSlug 추출
2. ISpacePermissionService.FindAuthorizedSpaceAsync()
- Space.status = ACTIVE 확인
- SpaceMember.user_id + status + Role 확인
- 요청 permission 충족 확인
3. 성공 → HttpContext에 AuthorizedSpaceContext 저장
4. 실패 → 403 Forbidden
→ UseCase
리소스의 space_id 검증 (IDOR 방어)
```
### 4.8 Role 매트릭스
| 작업 | OWNER | ADMIN | MEMBER | VIEWER |
|------|:-----:|:-----:|:------:|:------:|
| Space 조회 | O | O | O | O |
| 파일/폴더 조회 | O | O | O | O |
| 다운로드 세션 발급 | O | O | O | O |
| 업로드 세션 생성 | O | O | O | X |
| 파일/폴더 수정 | O | O | O | X |
| 공유 링크 생성 | O | O | O | X |
| 멤버 초대/변경 | O | O | X | X |
| Quota 변경 | O | X | X | X |
| Space 삭제 | O | X | X | X |
### 4.9 예외 규칙
- 마지막 OWNER 제거 금지
- ADMIN이 OWNER를 강등/제거할 수 없음
- 자기 자신을 마지막 OWNER에서 제거할 수 없음
- Space가 ARCHIVED/DELETED이면 Role과 무관하게 쓰기 금지
### 4.10 외부 공개 API 보안: Zero Information Leak
외부 `/public/v1/*`에서는 권한 없음, 리소스 없음, 비활성, 격리 상태를 **모두 `404 Not Found`로 통일**하여 리소스 존재 여부를 외부에 노출하지 않는다.
### 4.11 인증/인가 설계 판단
> **JWT가 아니라 opaque session token을 선택한 것은 CloudSharp의 "한 사용자 = 여러 Space × 여러 Role"이라는 멀티 테넌트 권한 모델을 고려한 의도적 설계다.** 인증과 인가를 분리하고, 권한 변경을 즉시 반영하며, 토큰에 정보를 담지 않아 토큰 탈취 시 노출을 최소화한다.
---
## 5. 데이터베이스 설계
### 5.1 주요 Entity 목록 (12개)
| Entity | 설명 | 주요 특징 |
|--------|------|-----------|
| `User` | 계정 주체, 인증 정보 | `system_role` (ADMIN/USER), 소프트 딜리트 |
| `Space` | 파일 저장 최상위 단위 | `slug` (UUID, URL 식별자), `storage_*_bytes` |
| `SpaceMember` | User ↔ Space 소속 관계 | Role(OWNER/ADMIN/MEMBER/VIEWER), Status |
| `SpaceInvite` | 멤버 초대 워크플로 | `token_hash`, 만료일, Role |
| `Folder` | Space 내 폴더 트리 | `parent_folder_id` self-join, `full_path` 캐시 |
| `FileItem` | 최종 저장 완료 파일 | `storage_key`, 후처리 3종 상태, metadata_json |
| `UploadSession` | tus 업로드 전송/확정 추적 | 7상태 머신, `finalize_attempts`, `tus_upload_id` |
| `FileReservation` | 파일명 + quota 선점 | UploadSession과 1:1, 6상태 머신 |
| `ShareLink` | 외부 공유 링크 정책 | `password_hash`, `allow_download`, 다운로드 카운트 |
| `ShareLinkTarget` | ShareLink → File/Folder 연결 | `target_type` (FILE/FOLDER) |
| `DownloadSession` | 실제 파일 스트리밍 단명 세션 | `subject_type` (USER/SHARE_LINK), 5분 TTL |
### 5.2 ERD (Mermaid)
```mermaid
erDiagram
USER ||--o{ SPACE : "creates"
USER ||--o{ SPACE_MEMBER : "joins"
SPACE ||--o{ SPACE_MEMBER : "has"
SPACE ||--o{ FOLDER : "owns"
FOLDER ||--o{ FOLDER : "parent_of"
SPACE ||--o{ FILE_ITEM : "owns"
FOLDER ||--o{ FILE_ITEM : "contains"
SPACE ||--o{ UPLOAD_SESSION : "scopes"
UPLOAD_SESSION ||--|| FILE_RESERVATION : "1:1 paired"
UPLOAD_SESSION ||--o| FILE_ITEM : "creates"
FILE_RESERVATION ||--o| FILE_ITEM : "consumed_to"
SPACE ||--o{ SHARE_LINK : "owns"
SHARE_LINK ||--o{ SHARE_LINK_TARGET : "maps"
FILE_ITEM ||--o{ SHARE_LINK_TARGET : "shared_file"
FOLDER ||--o{ SHARE_LINK_TARGET : "shared_folder"
FILE_ITEM ||--o{ DOWNLOAD_SESSION : "streamed_by"
```
### 5.3 EF Core Entity Configuration (실제 코드 — 11개 Config 클래스)
```
Configurations/
├── UserEntityConfiguration.cs
├── SpaceEntityConfiguration.cs
├── SpaceMemberEntityConfiguration.cs
├── SpaceInviteEntityConfiguration.cs
├── FolderEntityConfiguration.cs
├── FileItemEntityConfiguration.cs
├── UploadSessionEntityConfiguration.cs
├── FileReservationEntityConfiguration.cs
├── ShareLinkEntityConfiguration.cs
├── ShareLinkTargetEntityConfiguration.cs
└── DownloadSessionEntityConfiguration.cs
```
### 5.4 설계 특징
| 원칙 | 적용 |
|------|------|
| **Space 중심 소유** | 파일/폴더/공유의 FK 기준은 User가 아닌 Space |
| **행위자와 소유자 분리** | 생성자는 User(crated_by_user_id), 소유자는 Space(space_id) |
| **모든 Timestamp 관리** | `created_at`, `updated_at` 모든 테이블 공통 |
| **Soft Delete** | `deleted_at TIMESTAMP NULL` — User, Space, Folder, FileItem, SpaceMember, ShareLinkTarget 등 대부분 |
| **Unique 제약** | slug, email, storage_key, token, (space_id, parent_folder_id, name) 등 |
| **CHECK 제약** | `storage_used_bytes >= 0`, `size_bytes >= 0`, `received_size <= expected_size` |
| **Migration 자동 적용** | `RunDatabaseMigrationsOnStartup = true` — EF Core가 시작 시점에 자동 마이그레이션 |
### 5.5 제약 조건 예시 (FileItem)
| 제약 | 내용 |
|------|------|
| PK | `id` |
| UNIQUE | `storage_key` |
| UNIQUE | `(space_id, folder_id, normalized_name)` on active rows |
| CHECK | `size_bytes >= 0` |
| FK | `space_id → Space`, `folder_id → Folder`, `created_by_user_id → User` |
### 5.6 데이터 모델링 설계 이유
- **UploadSession과 FileReservation 분리**: 전송 상태 추적(tus I/O)과 비즈니스 자원 선점(quota·파일명)은 변경 주기와 실패 원인이 다르다. 전자는 네트워크, 후자는 도메인 정책. 1:1 연결로 두었다.
- **ShareLinkTarget 다대다**: 하나의 링크가 여러 파일/폴더를 가리킬 수 있고, 하나의 파일이 여러 링크로 공유될 수 있다.
- **DownloadSession 별도 테이블**: 로그인 세션과 다운로드 스트리밍 토큰은 TTL, revoke 정책, subject_type이 전혀 다르다.
---
## 6. 비즈니스 로직 / UseCase 구조
### 6.1 UseCase 목록 (실제 구현 코드 기준)
| Feature | 인터페이스 | 구현 클래스 | 주요 메서드 |
|---------|-----------|-------------|-------------|
| Auth | `IUserUseCases` | `UserUseCases` | `RegisterAsync` |
| Spaces | `ISpaceUseCases` | `SpaceUseCases` | `CreateAsync` + 생성 정책 |
| Members | `ISpaceMemberUseCases` | `SpaceMemberUseCases` | `FindSpacesAsync`, `AddMemberAsync` |
| Members | `ISpacePermissionService` | `SpacePermissionService` | `FindAuthorizedSpaceAsync` |
| Folders | `IFolderUseCases` | `FolderUseCases` | `CreateAsync`, `ListChildrenAsync`, `UpdateMetadataAsync`, `DeleteAsync`, `ProvisionAsync` |
| Files | `IFileUseCases` | `FileUseCases` | `UpdateMetadataAsync`, `DeleteAsync` |
| Uploads | `IUploadUseCases` | `UploadUseCases` | `CompleteUploadAsync`, `MarkUploadSessionUploadingAsync` |
| Uploads | `IUploadSessionUseCases` | `UploadSessionUseCases` | `AddUploadSessionAsync`, `ReserveUploadSessionAsync` |
| Downloads | `IDownloadUseCases` | `DownloadUseCases` | `CreateSessionAsync`, `GetDownloadStreamAsync` |
### 6.2 Command/Query/Result 패턴
각 Feature 디렉토리 구조 (실제 코드 기반):
```
UseCases/{Feature}/
├── I{Feature}UseCases.cs
├── {Feature}UseCases.cs
├── Commands/ ← CQRS Command (입력 모델)
├── Queries/ ← CQRS Query (조회 입력)
├── Validators/ ← FluentValidation validator
├── Results/ ← UseCase 반환 Result
├── Dtos/ ← 내부 DTO
└── Extensions/ ← 변환 헬퍼
```
### 6.3 요청 처리 순서 (4단계)
```csharp
// 표준 UseCase 패턴:
1. Command/Query (DTO )
2. Validation (FluentValidation .ValidateAsync)
3. (Space , , quota, )
4. Result<T> ( Result DTO, CloudSharpError)
```
### 6.4 Bind 파이프라인 패턴 (업로드)
```csharp
// 설계 기준 패턴:
return EnsureSpaceExists(spaceId)
.Bind(_ => EnsureCanUpload(spaceId, userId))
.Bind(_ => EnsureQuotaAvailable(spaceId, expectedSize))
.Bind(_ => ReserveFileName(command))
.Bind(reservation => CreateUploadSession(command, reservation));
```
→ 각 단계 실패 시 뒤 단계 실행되지 않음. 실패 이유 보존.
### 6.5 API ↔ UseCase 책임 분리
| 계층 | 하는 일 | 하지 않는 일 |
|------|---------|-------------|
| **Endpoint** | 라우팅, Request DTO → Command 변환, UseCase 호출, Result → HTTP 변환, OpenAPI metadata | DB 직접 조회, 여러 UseCase 혼합, 비즈니스 판단 |
| **UseCase** | 비즈니스 흐름, 권한/상태/정책 검증, 트랜잭션 경계, Result<T> 반환 | HTTP 상태 코드 결정, Response DTO 생성, `IResult` 반환 |
| **Domain Policy** | 상태 전이 검증, 값 객체 생성, 불변조건 확인 | 인프라 의존성 참조 |
---
## 7. 트랜잭션과 동시성
### 7.1 트랜잭션이 필요한 기능
Space 생성, 업로드 세션 생성, Finalize 등 **여러 repository가 하나의 원자적 흐름을 이룰 때** UseCase에서 트랜잭션을 시작한다.
### 7.2 트랜잭션 패턴: `IAppDbTransactionFactory`
```csharp
await using var tx = await transactionFactory.BeginAsync(ct);
var saveResult = await repository.SaveAsync(entity, ct);
if (saveResult.IsFailed)
{
await tx.RollbackAsync(ct);
return saveResult.ToResult<MyResult>();
}
await tx.CommitAsync(ct);
```
### 7.3 Finalize 트랜잭션 경계 분리
Finalize는 파일 I/O(time-consuming)와 DB 변경을 분리한다:
```
🔓 트랜잭션 밖:
1. 임시 파일 검증 (크기, 해시, MIME, quota)
2. 파일 이동 (rename or copy+delete)
🔒 DB 트랜잭션:
3. FileItem INSERT
4. Space.storage_used_bytes += final_size
5. Space.storage_reserved_bytes -= reserved_size
6. FileReservation.status = CONSUMED
7. UploadSession.status = COMPLETED
```
> **설계 판단**: 파일 이동 같은 느린 I/O 동안 DB 락을 잡지 않는다. 단, 이동 성공 후 DB 트랜잭션 실패 시 고아 파일이 생길 수 있으므로 Recovery Worker와 정리 배치가 필요하다.
### 7.4 동시 요청 경쟁 방지
#### Finalize 중복 방지: CAS (Compare-And-Swap)
```sql
-- 동시 finalize 요청 차단: 단 하나의 프로세스만 점유
UPDATE upload_session
SET status = 'FINALIZING',
finalizing_started_at = now(),
finalize_attempts = finalize_attempts + 1
WHERE id = :session_id
AND status = 'UPLOADING';
-- affected_rows = 1 → 점유 성공
-- affected_rows = 0 → 점유 실패 (이미 다른 프로세스가 처리 중)
```
#### Quota 경쟁 방지: DB row-level lock
```sql
-- 동일 Space 동시 업로드 판정: FOR UPDATE로 단일 트랜잭션 원자화
BEGIN;
SELECT * FROM space WHERE id = :id FOR UPDATE;
-- quota 판정 + reserved += expected_size
-- FileReservation 생성
COMMIT;
```
### 7.5 중복 생성 방지 (이중 안전장치)
| 단계 | 방식 | 위치 |
|------|------|------|
| 1차 | UseCase 사전 검증 (파일명 충돌 확인) | Core |
| 2차 | DB UNIQUE 제약 `(space_id, folder_id, normalized_name)` | PostgreSQL |
| 3차 | `TrySaveChangesAsync()` — conflict 감지 시 `Result<int>` 실패 | Infra |
### 7.6 Idempotency
- 동일 UploadSession에 대한 Finalize 재호출은 CAS로 차단된다.
- `affected_rows = 0` → 요청 무시, 기존 상태 반환.
- `file_item_id`가 이미 존재하면 `COMPLETED`로 간주하고 별도 처리.
### 7.7 외부 파일 ↔ DB 정합성 처리
Recovery Worker(매 5분)가 `FINALIZING` 상태에 10분 이상 머문 세션을 발견하면:
- `FileItem` 이미 존재 → `COMPLETED`로 보정
- 임시 파일 소실 → `FAILED`로 보정
- 임시 파일 존재 + 재시도 가능 → Finalize 재시도
- 최대 재시도 초과 → `FAILED` + 운영 알림
---
## 8. 예외 처리와 응답 구조
### 8.1 3계층 실패 전략
| 실패 유형 | 예시 | 처리 | 로그 레벨 |
|-----------|------|------|-----------|
| **Validation 실패** | `Required` 누락, `MaxLength` 초과 | ASP.NET Core `400 Bad Request` (자동) | 없음 |
| **비즈니스 실패** | `QUOTA_EXCEEDED`, `FILE_NAME_CONFLICT`, `SPACE_ACCESS_DENIED` | `Result.Fail(CloudSharpError)``ResultHttpMapper`가 HTTP 변환 | 대부분 없음 (Debug). 보안 실패만 Warning |
| **시스템 예외** | DB 연결 장애, 버그, OOM | Exception 전파 → `ExceptionHandlingMiddleware`에서 `500` + `ProblemDetails` | Error (1회), 운영에선 stack trace 금지 |
### 8.2 FluentResults + CloudSharpError 패턴
```csharp
// 공통 베이스
public abstract class CloudSharpError : Error
{
protected CloudSharpError(string errorCode, string message)
: base(message)
{
Metadata.Add("ErrorCode", errorCode);
}
}
// 도메인별 에러 (예시)
public sealed class DuplicateEmailError : CloudSharpError
{
public DuplicateEmailError(string email)
: base("USER_DUPLICATE_EMAIL", "이미 사용 중인 이메일입니다.") { }
}
```
### 8.3 Result → HTTP 변환 (`ResultHttpMapper`)
`ResultHttpMapper`는 모든 UseCase 실패를 4가지 HTTP 상태로 매핑한다:
| ErrorCode 패턴 | HTTP 상태 |
|----------------|-----------|
| `*_NOT_FOUND` | 404 |
| `*_FORBIDDEN`, `*_UNAUTHORIZED` | 403 |
| `*_CONFLICT`, `*_DUPLICATE` | 409 |
| 나머지 | 400 |
### 8.4 Global Exception Middleware
- 처리되지 않은 시스템 예외만 `Error` 레벨로 1회 기록
- 운영 환경에서는 stack trace 출력 금지 (`exception.GetType().Name`만)
- Development 환경에서만 디버깅을 위해 stack trace 허용
- 클라이언트에는 `ProblemDetails`로 응답하고 내부 exception message 노출 금지
### 8.5 로그 레벨 구분 기준
| 분류 | 로그 레벨 | 기준 |
|------|-----------|------|
| Validation 실패 | 없음 | 정상적 사용자 입력 실패 |
| `*_NOT_FOUND` | 없음 | 예상 가능한 비즈니스 결과 |
| `*_CONFLICT`, `*_ALREADY_*` | 없음 | 예상 가능한 충돌 |
| `*_FORBIDDEN`, 소유권 위반 | **Warning** | 보안 관련 |
| `AUTH_PASSWORD_VERIFY_FAILED` | **Warning** | 인증 보안 |
| 처리되지 않은 시스템 예외 | **Error** | 예상 못한 장애 |
### 8.6 프론트엔드-백엔드 에러 일관성
- 모든 실패 응답은 `ErrorResponse` 형식(`requestId`, `error.code`, `error.message`, `error.details[]`)
- `requestId` = `HttpContext.TraceIdentifier` → 요청 추적 가능
- ErrorCode는 API 문서화된 고정 문자열 → 클라이언트가 분기 처리 가능
- 외부 공개 API는 404로 마스킹 → 정보 누출 최소화
---
## 9. 인프라 구성
### 9.1 Docker Compose 서비스 구성
| 서비스 | 이미지 | 역할 | 포트 |
|--------|--------|------|------|
| **postgres** | `postgres:16-alpine` | 메타데이터 진실 원천 | 내부:5432 |
| **redis** | `redis:7-alpine` | 세션 저장소, Pub/Sub | 내부:6379 |
| **tusd** | `tusproject/tusd:latest` | tus 프로토콜 청크 업로드 | 내부:1080 |
| **api** | `mcr.microsoft.com/dotnet/aspnet:10.0` (직접 빌드) | ASP.NET Core 백엔드 | 내부:8080, 8081 |
| **nginx** | `nginx:alpine` (직접 빌드) | Reverse Proxy 단일 진입점 | **외부:8080→80** |
### 9.2 Volume 마운트 구조
| Volume | 마운트 | 용도 |
|--------|--------|------|
| `postgres_data` | `/var/lib/postgresql/data` | DB 데이터 영속화 |
| `redis_data` | `/data` | AOF 기반 Redis 영속화 |
| `./storage` (호스트) | `/data/storage` (컨테이너) | API + tusd 공유 파일 저장소 |
파일 저장 디렉토리:
```
/data/storage/
├── objects/ ← 최종 저장 파일 (shard 기반)
│ └── spaces/{shard}/{spaceId}/objects/{s1}/{s2}/{fileKey}.bin
└── tmp/tusd/ ← tusd 임시 업로드 파일
```
### 9.3 Healthcheck 구성
| 서비스 | Healthcheck | 간격 |
|--------|-------------|------|
| postgres | `pg_isready` | 10s × 5 retry |
| redis | `redis-cli ping` | 10s × 5 retry |
| tusd | `wget localhost:1080/health` | 10s × 5 retry |
| api (컨테이너) | `curl localhost:8080/api/v1/health` | 30s × 3 retry |
| api (앱) | `GET /api/v1/health` | - |
### 9.4 depends_on 조건
```
api는 postgres(healthy), redis(healthy), tusd(healthy) 이후 시작
nginx는 api, tusd 이후 시작
```
`condition: service_healthy`로 단순 실행 순서가 아닌 실제 준비 완료를 기다린다.
### 9.5 포트 노출 정책
| 정책 | 적용 |
|------|------|
| **외부 노출** | nginx:8080 → host:8080 (단일 진입점) |
| **내부 only** | postgres, redis, tusd, api (모두 `expose`만, `ports` 없음) |
| **보안 원칙** | DB, Redis, tusd, API는 외부에서 직접 접근 불가. nginx만 유일한 외부 진입점 |
### 9.6 인프라 구성 설계 의도
- **단일 외부 포트(8080)**: 셀프호스트 사용자가 하나의 포트만 열면 된다. 복잡한 포트 매핑 불필요.
- **Dockerfile 멀티 스테이지 빌드**: SDK 이미지 → 빌드 → publish → runtime(`aspnet:10.0`). 최종 이미지 크기 최소화.
- **HEALTHCHECK `curl`을 위해 apt-get install curl**: ASP.NET 기본 이미지에 curl이 없으므로 런타임 이미지에만 curl을 추가했다.
- **tusd hook**: `pre-create`, `post-create`, `post-finish` 이벤트를 API로 전달하여 업로드 생명주기를 백엔드가 통제한다.
---
## 10. Reverse Proxy / 도메인 / HTTPS
### 10.1 nginx 선택
nginx를 리버스 프록시로 사용. alpine 기반 경량 이미지, 단일 설정 파일(`cloudsharp.conf`).
### 10.2 도메인 라우팅 구조
```
server_name: localhost (→ 운영 시 실제 도메인)
listener: 80
/api/* → http://api:8080 (ASP.NET Core API)
/files/* → http://tusd:1080/files/ (tusd 업로드)
/swagger/* → http://api:8080 (Swagger UI)
/openapi/* → http://api:8080 (OpenAPI 문서)
```
### 10.3 tusd 스트리밍 특수 설정
nginx가 tusd 업로드를 프록시할 때 필요한 특수 설정:
```nginx
# tus 관련 헤더 전달
proxy_set_header Tus-Resumable $http_tus_resumable;
proxy_set_header Upload-Length $http_upload_length;
proxy_set_header Upload-Offset $http_upload_offset;
proxy_set_header Upload-Metadata $http_upload_metadata;
# 대용량 업로드
client_max_body_size 0; # 무제한
# 스트리밍 버퍼링 해제
proxy_buffering off;
proxy_request_buffering off;
# 장시간 업로드 타임아웃
proxy_read_timeout 600s;
proxy_send_timeout 600s;
```
### 10.4 HTTPS 적용
- MVP: `ASPNETCORE_ENVIRONMENT=Production`, `UseHttpsRedirection=false` (nginx 뒤에서 HTTP로 통신)
- 운영: nginx에 Let's Encrypt/Caddy 적용하거나, Cloudflare Tunnel로 HTTPS 처리 (확인 필요)
### 10.5 Basic Auth
- 현재 미적용. 관리자 도구 접근 제한은 확인 필요.
---
## 11. 환경변수와 설정 관리
### 11.1 .env.example — 전체 변수 구조 (실제 코드)
```bash
# 필수: ASP.NET 환경
ASPNETCORE_ENVIRONMENT=Production
# PostgreSQL 연결
Postgres__Host=postgres
Postgres__Db=cloudsharp
Postgres__User=cloudsharp
Postgres__Password=cloudsharp
Postgres__Port=5432
# Redis 연결
Redis__Host=redis
Redis__Port=6379
# Opaque Session Token HMAC 비밀키
Auth__SessionHashKey=<반드시 교체>
# Local File Storage
Storage__Provider=local
Storage__RootPath=/data/storage
Storage__TempPath=/data/storage/tmp/tusd
Storage__ObjectsPath=/data/storage/objects
# tusd 설정
Tusd__Host=tusd
Tusd__Port=1080
Tusd__UploadDir=/data/storage/tmp/tusd
Tusd__HooksHttp=http://api:8080/internal/tusd/hooks
# 내부 Finalize API 인증 토큰 (반드시 설정)
Uploads__FinalizeToken=<openssl rand -base64 32>
# Finalize Recovery Worker
Uploads__FinalizeRecoveryEnabled=true
Uploads__FinalizeRecoveryIntervalSeconds=300
Uploads__FinalizeRecoveryStaleMinutes=10
Uploads__FinalizeRecoveryBatchSize=100
# Space 정책
Space__MaxStorageAllowedBytes= (공백 = 무제한)
```
### 11.2 설정 관리 특징
| 항목 | 방식 |
|------|------|
| **연결 문자열** | 개별 `Postgres__Host/Port/Db/User/Password` → ASP.NET Options 패턴 바인딩 |
| **Secret** | `Auth__SessionHashKey`, `Uploads__FinalizeToken` — 환경변수 주입, 소스코드에 하드코딩 금지 |
| **개발/운영 분리** | `ASPNETCORE_ENVIRONMENT` + `.env` 파일. `docker-compose.yml`은 기본값 제공 |
| **컨테이너 내부 주소** | compose가 서비스명을 DNS로 resolve (`postgres`, `redis`, `tusd`) |
| **하드코딩 제거** | 모든 연결 정보·파일 경로·정책 값은 환경변수 or `appsettings.json`에서 주입 |
### 11.3 ASP.NET Options 바인딩
`appsettings.json` + 환경변수 오버라이드 (`Postgres__Host``Postgres:Host`). `IOptions<T>` / `IConfiguration` 패턴 사용.
---
## 12. 로그 / 모니터링 / 운영
### 12.1 로그 철학: "중복 없이 검색 가능하게"
**한 줄 기준:** Middleware(기본 관측성) + GlobalExceptionHandler(예외 1회) + UseCase(보안 Warning, 감사 Information만)
### 12.2 계층별 로깅 정책
| 위치 | 레벨 | 책임 |
|------|------|------|
| **Middleware** | Information | 요청당 1회: `method/path/statusCode/elapsedMs/traceId/userId` |
| **GlobalExceptionHandler** | Error | 예상 못한 exception만 1회, stack trace 없음(Production) |
| **UseCase Activity Proxy** | Debug | 모든 `I*UseCases` 메서드 완료 자동 기록 (디버깅/추적) |
| **UseCase 직접** | Warning / Information | 보안 실패(Warning), 감사 이벤트(Information) |
| **Repository** | 없음 | 기본 로깅 안 함 |
| **EF Core** | Warning (Prod) | SQL command 로깅은 Development만 |
### 12.3 구조화 로그 — 영어 고정 템플릿 + named placeholder
```csharp
// 좋음:
logger.LogWarning(
"Space access denied userId={UserId} spaceId={SpaceId} permission={Permission} errorCode={ErrorCode}",
userId, spaceId, permission, ErrorCodes.SpaceForbidden.Code);
// 금지:
logger.LogWarning($"Space access denied userId={userId} ..."); // 문자열 보간
```
### 12.4 금지 사항
- 토큰/body/Authorization header 로깅
- 운영/공유 환경에서 `logger.LogError(exception, ...)` 사용 (stack trace 노출)
- validation 실패를 Warning으로 남기기 (노이즈)
- repository에서 DB 장애를 Result로 감싸기 (시스템 장애 감지 흐려짐)
### 12.5 운영 도구
| 도구 | 상태 |
|------|------|
| Grafana / Loki / Dozzle | 확인 필요 (설계서에서 언급만 됨) |
| OpenTelemetry / 분산 추적 | 예정 (확장 로드맵) |
### 12.6 장애 확인 방법
- `GET /api/v1/health` (Docker HEALTHCHECK)
- nginx access log
- ASP.NET 기본 로그 + 구조화 로그
- Recovery Worker가 `FINALIZING` stale 세션 보고
---
## 13. CI/CD / 배포 자동화
### 13.1 CI 플랫폼: GitLab CI/CD
Pipeline은 Merge Request + Default Branch Push에만 트리거.
### 13.2 Stage 구성
```
test → build → pr_review → deploy
```
### 13.3 Backend CI (`backend:test`)
```yaml
# 실제 코드
image: mcr.microsoft.com/dotnet/sdk:10.0
script:
- dotnet restore
- dotnet test tests/CloudSharp.Core.Tests # 단위 테스트
- dotnet test tests/CloudSharp.Infrastructure.Tests # 인프라 테스트
```
> **현재 범위**: Core.Tests + Infrastructure.Tests. API IntegrationTests, Architecture.Tests는 파이프라인에 미포함.
### 13.4 Docker Image Build (`backend:image`)
```yaml
# 실제 코드
extends: .docker-sock + .ghcr-login
script:
- docker build -f Dockerfile → ghcr.io/cloud-sharp/cloudsharp-backend:$CI_COMMIT_SHA + :latest
- docker push to GHCR
trigger: master push only, backend 변경 시
```
### 13.5 Nginx CI (`nginx:test` + `nginx:image`)
- `nginx -t` 구문 검증
- 이미지 빌드 → `ghcr.io/cloud-sharp/cloudsharp-nginx`
- trigger: `infra/nginx/**` 변경 시
### 13.6 배포 (Deploy)
```yaml
# 현재: Placeholder
script: echo "Deploy stage placeholder. Real deployment will be added later."
```
- GHCR 이미지 배포용 `docker-compose.ghcr.yml` 파일은 존재.
- 실제 자동 배포는 미구현.
### 13.7 브랜치 전략
- `master`: Default Branch → push 시 이미지 빌드
- `feat/*` → Merge Request 시 테스트 실행
- PR Review Job: 별도 포함 (`.gitlab/ci/pr_review.yml`)
---
## 14. 성능 최적화
### 14.1 실제 적용 사항
| 기법 | 적용 위치 | 확인 |
|------|-----------|------|
| **Pagination** | `GET /spaces` (page=1, pageSize=20/50/100), `GET /folders/{id}/children` | 코드 확인 |
| **AsNoTracking** | 조회 전용 쿼리에서 사용 권장 (EF Core 컨벤션) | 확인 필요 |
| **Projection** | 도메인 객체 → DTO 변환 시 `Map()` 사용, 필요한 필드만 반환 | 디자인 확인 |
| **Storage Sharding** | 파일 저장 경로: 256(Space) × 256×256(File) = 65,536 버킷 분산 → 디렉터리 과밀 방지 | 전략 문서 |
| **sendfile / X-Accel-Redirect** | 다운로드 파이프라인 설계에 언급 (운영 시 커널 레벨 전송) | 예정 |
### 14.2 다운로드 Range 지원 설계
- `Range: bytes=N-M` 단일 Range 지원 (206 Partial Content)
- `If-Range` + `ETag` 지원 → 파일 변경 시 전체 재전송
- multi-range (`Range: bytes=0-99,200-299`) 미지원 → 416 또는 전체 응답
### 14.3 성능 관련 확인 필요
- Include/N+1 문제 처리 (EF Core configuration)
- Index 적용 현황
- Redis 응답 캐시
- 실제 응답 시간 측정
---
## 15. 설계 문서화
### 15.1 문서 체계
```
docs/.llm/
├── INDEX.md ← LLM 개발의 단일 지식 허브
├── context/ ← 프로젝트 배경
│ ├── overview.md 서비스 개요, 핵심 컨셉, 기술 스택
│ ├── architecture.md Mermaid 포함 전체 구성도, 기술 선택·확장 방향
│ ├── domain.md 핵심 도메인 엔티티 요약 + 설계 원칙
│ ├── requirements.md 48개 기능 요구사항 (SFR-001 ~ SFR-048)
│ ├── directory.md 모노레포 전체 디렉토리 구조
│ ├── product-plan.md 기획서
│ └── space-architecture.md 멀티 클라우드 확장 아키텍처
├── design/ ← 기술 설계
│ ├── api.md 모든 Endpoint, Request/Response DTO, 구현 상태
│ ├── erd.md Mermaid ERD + 11개 엔티티 상세 컬럼
│ ├── openapi.yaml OpenAPI 3.x 전체 스펙
│ ├── pipelines/
│ │ ├── upload.md 업로드 파이프라인 (Mermaid 시퀀스·상태 다이어그램)
│ │ └── download.md 다운로드 파이프라인 (8단계 상세)
│ └── strategies/
│ ├── finalize-lock.md CAS 기반 Finalize 설계
│ ├── quota.md Quota 판정 공식 + 상태 전이
│ ├── storage.md Sharding 전략, 버킷 전략, Provider 추상화
│ └── ... 파일명 충돌, 세션 정책, 정리 배치 등
├── conventions/ ← 코딩 규칙 (14개)
│ ├── auth-policy.md Opaque Session Token + Space Role 인가
│ ├── error-handling.md FluentResults + CloudSharpError
│ ├── http-endpoints.md Minimal API Endpoint 작성 규칙
│ ├── persistence.md 트랜잭션 + TrySaveChangesAsync
│ ├── logging.md 로그 레벨·메시지·금지 규칙
│ ├── usecases-coding.md UseCase 인터페이스·구현·DI
│ ├── validation.md FluentValidation 규칙
│ ├── testing.md NUnit + Bogus 테스트 컨벤션
│ └── ...
└── wiki/ ← 살아있는 개발 기록
├── progress.md 기능별 구현 상태 트래커
├── decisions.md ADR (3건: API 구조, UseCase 구조, Endpoint 방식)
└── notes.md gotcha, 주의사항
```
### 15.2 다이어그램 현황
| 유형 | 포함 | 도구 |
|------|------|------|
| **ERD** | O | Mermaid (`erd.md`) |
| **아키텍처 구성도** | O | Mermaid (`architecture.md`) |
| **상태 전이 다이어그램** | O (UploadSession, FileReservation, FileItem) | Mermaid |
| **시퀀스 다이어그램** | O (업로드, Finalize, 다운로드, 중복 차단, Recovery) | Mermaid |
| **배포 구조도** | O (Docker Compose 서비스 관계, nginx 라우팅) | Mermaid + 문서 |
| **ADR** | O (3건: 2026-04-23 ~ 2026-04-25) | `wiki/decisions.md` |
| **기술 선택 이유** | O (`architecture.md`에 MVP 선택 근거 + 확장 방향 포함) | 표 |
### 15.3 기술 선택 이유 문서화
`architecture.md`에 **모든 기술 스택의 MVP 채택 이유 + 추후 확장 방향**이 표로 정리되어 있다. (React부터 ASP.NET, tusd, PostgreSQL, Redis, Local FS, ffmpeg, AI Worker까지.)
---
## 16. 문제 해결 사례 후보
### 16.1 업로드 Finalize 중복 실행 방지: CAS Lock
#### 문제 상황
- tusd 업로드 완료 후 hook이 API로 전달됨
- 여러 Worker 또는 hook 중복 호출 시 같은 업로드 세션이 두 번 FileItem으로 생성될 위험
- 분산 환경에서는 분산 락을 잡기도 어렵고, 파일 이동이라는 느린 I/O를 포함
#### 원인 분석
- Hook 콜백 + 백그라운드 Finalize 두 경로에서 동시에 같은 세션을 처리할 수 있음
- 파일 이동(rename)은 원자적이지만, DB 반영 전까지는 상태가 모호함
#### 해결 방법
- **CAS(Compare-And-Swap)**: `UPDATE upload_session SET status = 'FINALIZING' WHERE status = 'UPLOADING'`
- 하나의 SQL UPDATE로 원자적 점유. `affected_rows = 1` → 성공, `0` → 이미 처리 중
- 파일 I/O와 DB 트랜잭션을 분리하고, DB는 짧은 트랜잭션만 유지
- Recovery Worker가 10분 이상 `FINALIZING`에 머문 세션을 자동 정리
#### 선택 이유
- 별도 분산 락(Redis Redlock, ZooKeeper) 인프라 불필요
- UploadSession의 기존 상태 머신을 락으로 재활용
- 단순하고 예측 가능한 거버넌스
#### 결과
- **안전성**: CAS + DB UNIQUE로 이중 중복 방지
- **단순성**: 추가 인프라 없이 SQL 한 라인
- **복구성**: Recovery Worker가 교착 상태 자동 해제
---
### 16.2 Space Quota 경쟁 조건 방지
#### 문제 상황
- 한 Space의 여러 멤버가 여러 탭/클라이언트에서 동시에 대용량 업로드를 시작
- quota 판정(`expected <= available`)과 reserved 증가가 분리되면, 모든 요청이 동시에 통과하는 race condition
#### 원인 분석
- 판정 시점과 reserved 증가 시점 사이의 간극에서 동시 요청이 통과
- 낙관적 동시성으로는 업로드 거절 시점에 이미 클라이언트가 전송 중일 수 있음
#### 해결 방법
- **DB 트랜잭션 + row-level lock**: `SELECT ... FOR UPDATE`로 Space 행 잠금
- 판정 → reserved 증가 → FileReservation 생성을 **하나의 트랜잭션**으로 원자화
- Finalize 직전에도 quota 재검사 (안전장치)
#### 선택 이유
- 업로드는 자주 발생하지 않으므로(수명이 긴 세션), 경합에서의 락 지속시간이 짧다
- PostgreSQL FOR UPDATE는 격리 수준을 높이지 않고도 충분
#### 결과
- Quota 불변조건(`used + reserved <= allowed`)이 항상 유지됨
- 초과 업로드는 조기에(업로드 시작 전) 거절 → 불필요한 네트워크 전송 방지
---
### 16.3 파일명 충돌 정책: "실패 반환"
#### 문제 상황
- 클라우드 스토리지에서 동일 폴더에 같은 이름의 파일이 업로드될 때 Google Drive 등의 "자동 이름 변경(파일명(1).pdf)"이 혼란을 줄 수 있음
#### 원인 분석
- 자동 rename은 의도를 숨기고, 사용자가 모르는 파일이 누적됨
- 충돌은 드물게 발생하며, 사용자 명시적 의사결정이 필요
#### 해결 방법
- **실패 반환 정책** 채택: `FILE_NAME_CONFLICT` 에러 코드 반환
- 동일 폴더 내 활성 `FileItem` + 활성 `FileReservation`을 함께 검사 (사전 + Finalize 직전 이중 검증)
- 충돌 감지에 `normalized_name` 사용
#### 선택 이유
- "모호함보다 명시적 실패가 낫다"는 설계 철학
- 사용자가 의도적으로 파일명을 바꾸도록 유도
- 구현 단순성 (rename fallback, 증분 카운터 등 불필요)
#### 결과
- **일관성**: 항상 예측 가능한 동작
- **데이터 정합성**: 사용자 의도와 저장된 파일명이 항상 일치
---
### 16.4 Opaque Session Token vs JWT
#### 문제 상황
- 사용자가 여러 Space에서 서로 다른 Role을 가짐
- Role 변경 시 JWT 만료 전까지 stale 권한이 유지되는 문제
#### 원인 분석
- JWT에 Role claim을 포함하면 토큰 만료 전까지 기존 권한이 남음
- 블랙리스트/토큰 버전 관리가 필요해지면 JWT의 stateless 장점이 약화됨
#### 해결 방법
- **Opaque Session Token**: 무작위 문자열. 권한 정보 없음.
- Redis에 `token_hash``UserSession` 저장
- 인가 판단은 매 요청마다 DB에서 최신 `SpaceMember` 조회
#### 선택 이유
- CloudSharp의 "한 사용자 = 여러 Space × 여러 Role" 모델에 적합
- stale 권한 문제 원천 차단
- 토큰 탈취 시 정보 노출 최소화
#### 결과
- Role 변경이 **다음 API 요청부터 즉시 반영**
- 로그아웃·강제 Revoke·계정 정지 즉시 적용
- 토큰 prefix로 운영자 구분만 가능 (`cs_sess_...`, `cs_dl_...`, `cs_share_...`)
---
## 17. 포트폴리오 문장 초안
### 프로젝트 개요
> Cloud#은 Space 단위의 완전히 격리된 팀 파일 저장 공간을 제공하는 셀프호스트 서비스입니다. tus 프로토콜 기반의 재개 가능한 대용량 업로드, 단명 세션 기반의 안전한 다운로드, 그리고 내부 협업과 외부 공유를 분리한 2계층 공유 모델을 갖추고 있습니다. ASP.NET Core 10 + PostgreSQL + Redis + tusd + Docker Compose 스택 위에 Clean Architecture를 적용했습니다.
### 담당 역할
> 백엔드 API 전체(인증·인가, Space 관리, 파일·폴더 CRUD, 업로드/다운로드 파이프라인, Quota 정책, 공유 링크), 데이터베이스 설계(ERD + EF Core Configuration + Migration), 인프라 구성(Docker Compose 5-서비스 오케스트레이션, nginx Reverse Proxy, Volume 설계, Healthcheck), CI/CD 파이프라인(GitLab CI), 그리고 Clean Architecture 기반 도메인 모델링과 UseCase 설계.
### 주요 기여
- **Space 중심 멀티 테넌트 아키텍처 설계**: 파일 소유권을 User에서 Space로 승격하고, SpaceMember Role 기반 권한 체계 구축
- **업로드 파이프라인 설계**: UploadSession(전송 추적)과 FileReservation(자원 선점)을 분리한 2-Entity 업로드 모델, CAS 기반 Finalize 중복 방지 메커니즘
- **Opaque Session Token 인증 시스템**: JWT가 아닌 opaque token을 선택하고, Redis 기반 세션 관리 + HMAC 토큰 해싱 + sliding renewal 정책 구현
- **Zero Information Leak 다운로드 보안**: 외부 API에서 리소스 존재 여부를 404로 통일 마스킹, 단명 5분 TTL DownloadSession
- **Storage Sharding 전략**: hex hash prefix 기반 3단계 shard(space 2자리 + file 2+2자리) → 65,536 버킷 분산, Local FS ↔ S3/MinIO 공통 storage_key 추상화
### 사용 기술 및 선택 이유
| 기술 | 선택 이유 |
|------|-----------|
| **ASP.NET Core 10 + Minimal APIs** | 빠른 부트스트랩, Controller 오버헤드 없음, .NET 10 최신 런타임 |
| **PostgreSQL 16** | 메타데이터(릴레이션, FK, Unique)에 최적, JSON 확장성 |
| **Redis 7** | 세션 저장소(빠른 Hash 조회) + Pub/Sub 이벤트 버스 겸용 |
| **tusd (Go)** | tus 표준 구현체, ASP.NET보다 대용량 청크 업로드에 효율적 |
| **EF Core 10 + Npgsql** | PostgreSQL Native driver, Migration 자동화 |
| **FluentResults** | 예외 아닌 비즈니스 실패 명시적 표현, Bind/Map/Merge 파이프라인 |
| **FluentValidation** | 선언적 도메인 검증, .WithErrorCode() 표준화 |
| **nginx** | 리버스 프록시, tus 헤더 포워딩, 버퍼링 해제, 단일 진입점 |
| **Docker Compose** | 셀프호스트 배포 간소화 |
| **Clean Architecture** | 계층 분리로 테스트 용이성 + 장기 유지보수성 확보 |
### 아키텍처 설계
> Space를 중심으로 한 모듈러 모놀리스 Clean Architecture를 설계했습니다. `Api → Core ← Infrastructure` 의존성 방향을 통해, 도메인 로직이 HTTP나 DB, 파일시스템을 전혀 알지 못하게 했습니다. 업로드와 다운로드라는 핵심 파이프라인은 각각 전용 설계 문서화(state machine + Mermaid sequence/flowchart)와 함께, UseCase 계층에서 Bind 파이프라인으로 조합했습니다. nginx → API / tusd 분리로 업로드 plane과 API plane을 독립적으로 확장할 수 있습니다.
### API 설계
> Space의 URL 식별자로 UUID 기반 `spaceSlug`를 사용하고, 내부 Core 계층은 DB의 bigint PK(`spaceId`)만 사용하는 경계 변환 정책을 적용했습니다. `RequireSpacePermissionFilter` 엔드포인트 필터가 slug → spaceId + 권한 컨텍스트로 변환하며, IDOR 방어는 UseCase에서 리소스의 `space_id`를 재검증합니다. 모든 엔드포인트는 OpenAPI metadata(`WithName`, `WithSummary`, `Produces<T>`)가 의무화되어 있습니다.
### 인프라 구성
> 5-서비스 Docker Compose 오케스트레이션을 구성했습니다. postgres, redis, tusd는 `expose`로 내부 네트워크에만 노출하고, nginx만 유일한 외부 진입점(8080)으로 두었습니다. 모든 서비스는 Healthcheck를 가지며, API는 postgres, redis, tusd의 healthy 조건을 충족한 후에 시작합니다. API 이미지는 Multi-stage build(SDK → build → publish → runtime)로 크기를 최소화했습니다.
### 문제 해결 사례
> 1. **Finalize 중복 실행 방지**: CAS(Compare-And-Swap) `UPDATE ... WHERE status='UPLOADING'`을 통해 분산 락 없이 원자적 점유를 구현하고, Recovery Worker로 교착 상태를 자동 복구했습니다.
> 2. **Quota 경쟁 조건**: PostgreSQL `SELECT ... FOR UPDATE`로 Space 행을 잠그고, 판정 → 예약 증가를 단일 트랜잭션으로 원자화했습니다.
> 3. **stale 권한 문제**: JWT 대신 Opaque Session Token을 선택하여, Role 변경이 다음 API 요청부터 즉시 반영되게 했습니다.
> 4. **파일명 충돌**: "자동 rename"보다 "명시적 실패 반환"을 선택하여 사용자 의도와 저장 상태의 일관성을 보장했습니다.
### 프로젝트 성과
- 12개 엔티티의 관계형 데이터 모델 완성 + Mermaid ERD + 모든 엔티티에 대한 EF Core Configuration
- 48개 기능 요구사항(SFR-001~048)을 OpenAPI + UseCase 매핑으로 연결
- 업로드/다운로드 양대 파이프라인의 완전한 상태 머신 설계 (UploadSession 7상태, FileReservation 6상태)
- 14개 컨벤션 문서로 팀 코딩 표준 구축
- GitLab CI로 PR 테스트 자동화 + Docker 이미지 GHCR 배포 자동화
### 회고
> **잘한 점**: 초기부터 엄격한 문서화(API, ERD, Conventions, ADR)를 하고, Clean Architecture로 도메인 경계를 나눈 것이 기능 확장 시 큰 도움이 되었다. JWT 대신 Opaque Session Token을 선택한 것은 CloudSharp의 멀티 Role 모델에 딱 맞는 판단이었다.
>
> **아쉬운 점**: API IntegrationTests와 Architecture Tests가 CI에 포함되지 않았다. 운영 환경의 모니터링(Grafana, OpenTelemetry)이 아직 설계에만 머물러 있다. 자동 배포(CD)가 placeholder 상태다. Worker(ffmpeg, AI) 프로젝트는 구조만 있고 실제 구현은 진행되지 않았다.
>
> **향후 계획**: MinIO/S3 Storage Provider 구현, 실제 Worker 붙이기, Kubernetes 전환, OpenSearch 도입.