# Cloud# (CloudSharp) — 백엔드 / 인프라 포트폴리오 > **프로젝트 유형:** 셀프호스트 파일 서비스 플랫폼 > **개인 기여 영역:** 백엔드 API · 데이터 모델링 · 인프라 구성 · CI/CD · 설계 문서화 > **기술 스택:** ASP.NET Core 10, PostgreSQL 16, Redis 7, tusd, Docker Compose, nginx --- ## 1. 프로젝트 개요 ### 한 줄 정의 > **Space 단위의 완전 격리형 저장 공간**을 제공하는 셀프호스트 파일 서비스로, tus 프로토콜 기반의 재개 가능한 대용량 업로드와 단명(短命) 다운로드 세션을 갖췄다. ### 해결하려는 문제 기존 클라우드 스토리지(Google Drive, Dropbox 등)는 **개인 계정 중심**으로 설계되어 있어, 팀/프로젝트 단위에서 폴더 공유만으로는 다음 요구를 충족하기 어렵다. - 격리된 저장 공간 (멤버십 + Quota + 권한) - 대용량 파일의 중단 재개 업로드 - 셀프호스트 + 외부 공유 정책의 분리 ### 주요 사용자 셀프호스트를 선호하는 개인·팀·소규모 조직, Space 단위 협업이 필요한 프로젝트 그룹. ### 백엔드 핵심 책임 | 책임 | 설명 | |------|------| | 인증/인가 | Opaque session token 기반 인증 + Space Role 기반 인가 | | 메타데이터 진실 원천 | 12개 엔티티의 정합성 보장 (PostgreSQL) | | 업로드 파이프라인 | tusd 연계 세션 생성 → 전송 → Finalize 전 생명주기 관리 | | Quota 정책 | `used + reserved + expected ≤ allowed` 원자적 판정 | | Storage 추상화 | Local FS / MinIO / S3 공통 `storage_key` 추상 | | 다운로드 보안 | 5분 TTL DownloadSession 기반 Zero Information Leak | | 동시성 제어 | CAS 기반 Finalize 중복 차단 + Recovery Worker | --- ## 2. 담당 역할 **1인 백엔드 + 인프라 + 설계 담당.** 프론트엔드를 제외한 전 영역. - **백엔드 API**: 인증·인가, Space 관리, 파일/폴더 CRUD, 업로드/다운로드 파이프라인, Quota, 공유 링크 - **데이터베이스 설계**: 12개 엔티티 ERD, EF Core Configuration, Migration 자동화 - **인프라 구성**: Docker Compose 5-서비스 오케스트레이션, nginx Reverse Proxy, Volume·Healthcheck 설계 - **CI/CD**: GitLab CI 파이프라인 (test → build → image push to GHCR) - **설계 문서화**: API/ERD/Conventions/ADR 등 살아있는 문서 체계 구축 --- ## 3. 아키텍처 ### 3.1 아키텍처 유형 **모듈러 모놀리스 + Clean Architecture.** > **선택 이유:** MVP 단계에서 마이크로서비스의 운영 복잡도(분산 트랜잭션, 서비스 디스커버리, 로그 수집)를 피하면서도, 코드 레벨로는 도메인 경계를 엄격히 분리해 추후 분리 가능성을 확보했다. > 의존성 방향은 `Api → Core ← Infrastructure`로, 도메인이 HTTP나 DB, 파일시스템을 알지 못하게 했다. ### 3.2 전체 구성도 ```mermaid flowchart TB Client["Browser / Client"] subgraph Docker["Docker Compose Host"] NGINX["nginx :80"] API["ASP.NET Core API :8080"] TUSD["tusd :1080"] PG[("PostgreSQL")] REDIS[("Redis")] FS[("Local FS /data/storage")] end Client -->|"HTTP :8080"| NGINX NGINX -->|"/api/*"| API NGINX -->|"/files/*"| TUSD API --> PG API --> REDIS API --> FS API -.->|"hook callback"| TUSD TUSD --> FS ``` ### 3.3 서비스 책임 분리 | 컴포넌트 | 책임 | |----------|------| | **nginx** | 단일 외부 진입점, tus 헤더 포워딩, 스트리밍 버퍼링 해제 | | **ASP.NET API** | 권한·정책·메타데이터·Finalize·공유·세션 | | **tusd (Go)** | tus 프로토콜 청크 수신, hook으로 API에 생명주기 위임 | | **PostgreSQL** | 메타데이터 진실 원천 | | **Redis** | 세션 저장소 + Pub/Sub 이벤트 버스 | ### 3.4 핵심 설계 의도 - **업로드 plane과 API plane 분리**: 대용량 청크 전송은 Go 기반 tusd가 담당, 비즈니스 판단은 ASP.NET이 담당 → 장애 격리 + 독립 확장 - **단일 외부 포트(8080)**: 셀프호스트 사용자 입장의 운영 단순화. DB/Redis/tusd/API는 `expose`만 사용하고 외부 노출 금지 --- ## 4. 주요 기여 — 문제 해결 사례 ### 4.1 Finalize 중복 실행 방지: CAS Lock **문제 상황** tusd hook 콜백 + 백그라운드 Recovery Worker 두 경로에서 같은 UploadSession이 동시에 처리되어 FileItem이 중복 생성될 위험이 있었다. Finalize는 파일 이동(rename)이라는 느린 I/O를 포함하므로 일반적인 DB 트랜잭션만으로는 동시성 제어가 부족했다. **원인 분석** - 상태 판정과 처리 시작이 분리되면 race condition 발생 - 분산 락(Redis Redlock 등) 도입은 인프라 복잡도 증가 - 파일 I/O 동안 DB 트랜잭션을 잡고 있으면 락 점유 시간이 너무 길다 **설계 선택 — CAS(Compare-And-Swap)** ```sql UPDATE upload_session SET status = 'FINALIZING', finalize_attempts = finalize_attempts + 1 WHERE id = :session_id AND status = 'UPLOADING'; -- affected_rows = 1 → 점유 성공 -- affected_rows = 0 → 이미 처리 중 (즉시 무시) ``` **구현 방식** - 파일 I/O와 DB 트랜잭션을 분리: 무거운 작업은 트랜잭션 밖, DB 변경(FileItem INSERT, Quota 갱신, FileReservation CONSUMED)만 짧은 트랜잭션 - Recovery Worker(5분 주기)가 10분 이상 `FINALIZING`에 머문 세션을 자동 보정 (FileItem 존재 시 COMPLETED, 임시 파일 소실 시 FAILED) **결과** - 별도 락 인프라 없이 단일 SQL UPDATE로 원자적 점유 실현 - 교착 상태 자동 복구로 운영 부담 최소화 - DB UNIQUE 제약(`storage_key`)과 함께 이중 안전장치 구성 --- ### 4.2 Space Quota 경쟁 조건 방지 **문제 상황** 한 Space의 여러 멤버가 동시에 대용량 업로드를 시작할 때, quota 판정(`expected ≤ available`)과 reserved 증가 사이의 간극에서 모든 요청이 통과하는 race condition이 발생할 수 있다. **설계 선택 — DB row-level lock** ```sql BEGIN; SELECT * FROM space WHERE id = :id FOR UPDATE; -- quota 판정 + reserved += expected_size + FileReservation 생성 COMMIT; ``` **선택 이유** - 업로드 세션 생성은 빈번하지 않고 락 지속시간이 짧다 - 낙관적 동시성으로는 이미 전송 중인 요청을 거절할 수 없다 - Finalize 직전에도 quota를 재검사하여 이중 안전장치를 구성 **결과** - Quota 불변조건 `used + reserved ≤ allowed`가 항상 유지 - 초과 업로드는 **시작 시점**에 거절되어 불필요한 네트워크 전송 방지 --- ### 4.3 인증 토큰: Opaque Session vs JWT **문제 상황** 한 사용자가 여러 Space에서 서로 다른 Role을 가지며, Role 변경이 즉시 반영되어야 한다. JWT를 사용하면 토큰 만료 전까지 stale 권한이 유지되며, 모든 Role을 claim에 담으면 토큰 크기가 비대해진다. **설계 선택 — Opaque Session Token** ``` Authorization: Bearer cs_sess_{base64url(CSPRNG 32bytes)} ``` - 권한 정보를 토큰에 담지 않음 - Redis에 `HMAC-SHA-256(token, secret)` 해시만 저장 (원문 저장 금지) - 매 요청마다 DB에서 최신 SpaceMember를 조회 **구현 방식** - `CloudSharpSessionAuthenticationHandler`: 헤더 검증 → 해시 계산 → Redis 조회 → 만료 확인 - `RequireSpacePermissionFilter`: route의 spaceSlug → spaceId 변환 + Role 충족 확인 → `AuthorizedSpaceContext` 주입 - UseCase 단계에서 리소스의 `space_id` 재검증 (IDOR 방어) - Idle 12시간 / Absolute 30일 / sliding renewal **결과** - Role 변경이 **다음 API 요청부터 즉시 반영** - 로그아웃·강제 Revoke를 Redis key 삭제 한 번으로 처리 - 토큰 탈취 시에도 토큰 자체에는 정보가 없어 노출 최소화 --- ### 4.4 파일명 충돌 정책: "명시적 실패 반환" **문제 상황** Google Drive 같은 서비스는 동일 폴더에 같은 이름의 파일이 업로드되면 자동으로 `파일명(1).pdf`로 rename한다. 그러나 사용자가 의도하지 않은 파일이 누적되어 데이터 일관성이 깨진다. **설계 선택 — `FILE_NAME_CONFLICT` 에러 반환** - 활성 `FileItem` + 활성 `FileReservation`을 함께 검사 (사전 + Finalize 직전 이중 검증) - DB 레벨에서 `UNIQUE (space_id, folder_id, normalized_name) on active rows` 제약 **선택 이유** - "모호함보다 명시적 실패가 낫다"는 설계 철학 - 사용자가 의도적으로 파일명을 결정하도록 유도 - 구현 단순성 (rename fallback, 카운터 등 불필요) **결과** - 항상 예측 가능한 동작 + 사용자 의도와 저장 상태의 일관성 --- ## 5. API 설계 ### 5.1 기본 계약 | 항목 | 값 | |------|-----| | 내부 API | `/api/v1/*` (Bearer 인증) | | 외부 공개 API | `/public/v1/*` (share_token + 비밀번호) | | 내부 전용 | `/api/internal/*` (`X-CloudSharp-Internal-Token`) | | 문서화 | Swagger UI + OpenAPI 3.x + ReDoc | ### 5.2 URL 식별자 분리 정책 - **외부 노출**: Space는 UUID 기반 `spaceSlug` (URL 식별자) - **내부 Core**: bigint PK `spaceId`만 사용 - **변환 위치**: `RequireSpacePermissionFilter`가 slug → id + 권한 컨텍스트로 변환 > bigint PK가 외부에 노출되어 enumeration 공격에 취약해지는 문제를 차단했다. ### 5.3 표준 에러 응답 ```json { "requestId": "req_01JXYZ...", "error": { "code": "FILE_NAME_CONFLICT", "message": "같은 이름의 파일이 이미 존재합니다.", "details": [{ "field": "displayName", "reason": "conflict" }] } } ``` - `requestId` = `HttpContext.TraceIdentifier` → 로그 추적 가능 - ErrorCode는 OpenAPI에 문서화된 고정 문자열 → 클라이언트가 분기 처리 가능 - 외부 공개 API는 권한 없음/리소스 없음/비활성 모두 **404로 통일** → Zero Information Leak --- ## 6. 데이터베이스 설계 ### 6.1 주요 엔티티 (12개) User, Space, SpaceMember, SpaceInvite, Folder, FileItem, **UploadSession**, **FileReservation**, ShareLink, ShareLinkTarget, DownloadSession. ### 6.2 핵심 모델링 결정 **① UploadSession ↔ FileReservation 1:1 분리** - UploadSession: 전송 상태 추적(tus I/O, 네트워크 관점, 7-state machine) - FileReservation: 비즈니스 자원 선점(quota·파일명, 도메인 관점, 6-state machine) - 변경 주기와 실패 원인이 다르므로 분리하되 1:1로 연결 **② Space 중심 소유권** - 파일/폴더의 FK는 User가 아닌 Space - 행위자(`created_by_user_id`)와 소유자(`space_id`) 분리 - 멤버 탈퇴 시에도 파일은 Space에 남음 **③ DownloadSession 별도 테이블** - 로그인 세션과 다운로드 토큰은 TTL/revoke 정책/subject_type이 전혀 다름 - `subject_type` (USER/SHARE_LINK)으로 내부 인증과 외부 공유를 단일 테이블로 통합 ### 6.3 정합성 보장 | 원칙 | 적용 | |------|------| | Soft Delete | 대부분 테이블에 `deleted_at TIMESTAMP NULL` | | CHECK 제약 | `storage_used_bytes ≥ 0`, `received_size ≤ expected_size` | | UNIQUE | `storage_key`, `(space_id, folder_id, normalized_name)` on active rows | | Migration | EF Core 시작 시점 자동 적용 (`RunDatabaseMigrationsOnStartup`) | --- ## 7. 트랜잭션과 동시성 ### 7.1 트랜잭션 패턴 공통 `IAppDbTransactionFactory` 추상화로 UseCase에서 명시적 경계 관리. ```csharp await using var tx = await transactionFactory.BeginAsync(ct); var result = await repository.SaveAsync(entity, ct); if (result.IsFailed) { await tx.RollbackAsync(ct); return ...; } await tx.CommitAsync(ct); ``` ### 7.2 Finalize 트랜잭션 경계 분리 ``` 🔓 트랜잭션 밖: 임시 파일 검증 + 파일 이동 (느린 I/O) 🔒 DB 트랜잭션: FileItem INSERT Space.storage_used_bytes += final_size Space.storage_reserved_bytes -= reserved_size FileReservation → CONSUMED UploadSession → COMPLETED ``` > 파일 이동 동안 DB 락을 잡지 않는다. DB 트랜잭션 실패 시 발생할 고아 파일은 Recovery Worker가 정리. ### 7.3 중복 생성 3중 방지 1. UseCase 사전 검증 (파일명 충돌) 2. DB UNIQUE 제약 3. `TrySaveChangesAsync()` — conflict 감지 시 `Result.Fail` --- ## 8. 예외 처리와 응답 구조 ### 8.1 3계층 실패 전략 | 실패 유형 | 처리 | 로그 레벨 | |-----------|------|-----------| | Validation | ASP.NET 400 자동 응답 | 없음 | | 비즈니스 | `FluentResults` + `CloudSharpError` → `ResultHttpMapper` | 보안 실패만 Warning | | 시스템 예외 | `ExceptionHandlingMiddleware` → `ProblemDetails` 500 | Error 1회 (stack trace 운영 비공개) | ### 8.2 ErrorCode → HTTP 매핑 | 패턴 | HTTP | |------|------| | `*_NOT_FOUND` | 404 | | `*_FORBIDDEN`, `*_UNAUTHORIZED` | 403 | | `*_CONFLICT`, `*_DUPLICATE` | 409 | | 나머지 | 400 | ### 8.3 책임 분리 - **Endpoint**: 라우팅, DTO ↔ Command 변환, Result → HTTP 변환만 - **UseCase**: `Result` 반환, HTTP를 모름 - **Domain Policy**: 상태 전이 검증, 인프라 의존성 없음 --- ## 9. 인프라 구성 ### 9.1 Docker Compose 5-서비스 | 서비스 | 외부 노출 | 역할 | |--------|-----------|------| | postgres | ❌ (`expose`만) | 메타데이터 | | redis | ❌ (`expose`만) | 세션 + Pub/Sub | | tusd | ❌ (`expose`만) | tus 청크 업로드 | | api | ❌ (`expose`만) | ASP.NET 백엔드 | | **nginx** | ✅ `:8080` | **유일한 외부 진입점** | ### 9.2 Volume 구조 ``` postgres_data → /var/lib/postgresql/data redis_data → /data ./storage → /data/storage (API + tusd 공유) ├── objects/ ← 최종 파일 │ └── spaces/{shard}/{spaceId}/objects/{s1}/{s2}/{fileKey}.bin └── tmp/tusd/ ← tusd 임시 파일 ``` > **Storage Sharding**: hex hash 기반 `256 × 256 × 256 = 65,536` 버킷 분산으로 디렉토리 과밀 방지. ### 9.3 Healthcheck + depends_on 모든 서비스가 Healthcheck를 가지며, `condition: service_healthy`로 단순 실행 순서가 아닌 **실제 준비 완료**를 기다린다. ```yaml api: depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } tusd: { condition: service_healthy } ``` ### 9.4 nginx — tusd 스트리밍 특수 설정 ```nginx proxy_set_header Tus-Resumable $http_tus_resumable; proxy_set_header Upload-Length $http_upload_length; proxy_set_header Upload-Offset $http_upload_offset; client_max_body_size 0; # 대용량 무제한 proxy_buffering off; # 스트리밍 버퍼링 해제 proxy_request_buffering off; proxy_read_timeout 600s; # 장시간 업로드 ``` ### 9.5 Dockerfile 설계 - Multi-stage build (SDK → build → publish → runtime)로 최종 이미지 크기 최소화 - `aspnet:10.0` 기본 이미지에 `curl` 추가하여 컨테이너 HEALTHCHECK 지원 --- ## 10. 환경변수 / 설정 관리 ```bash # 보안 비밀 (반드시 환경변수로만 주입) Auth__SessionHashKey= Uploads__FinalizeToken= # DB / Redis / Storage / tusd Postgres__Host=postgres Redis__Host=redis Storage__Provider=local Tusd__HooksHttp=http://api:8080/internal/tusd/hooks ``` - ASP.NET Options 패턴 + `__` 환경변수 오버라이드 - 모든 연결 정보·경로·정책 값은 환경변수 또는 `appsettings.json`에서 주입 (하드코딩 금지) - Compose가 서비스명을 DNS로 resolve (`postgres`, `redis`, `tusd`) --- ## 11. 로그 / 모니터링 **로그 철학:** *"중복 없이 검색 가능하게"* | 위치 | 레벨 | 책임 | |------|------|------| | Middleware | Information | 요청당 1회 (`method/path/status/elapsedMs/traceId/userId`) | | GlobalExceptionHandler | Error | 예상 못한 예외만 1회 (운영 stack trace 미노출) | | UseCase Activity Proxy | Debug | 모든 UseCase 메서드 자동 추적 | | UseCase 직접 | Warning/Information | 보안 실패, 감사 이벤트만 | **구조화 로그 — 영어 고정 템플릿 + named placeholder** ```csharp // ✅ logger.LogWarning( "Space access denied userId={UserId} spaceId={SpaceId} permission={Permission}", userId, spaceId, permission); // ❌ 문자열 보간 금지 ``` **금지사항**: 토큰/body/Authorization header 로깅, validation 실패를 Warning으로 남기기, 운영 환경 stack trace 노출. --- ## 12. CI/CD **GitLab CI 4-stage 파이프라인**: `test → build → pr_review → deploy` ```yaml backend:test: image: mcr.microsoft.com/dotnet/sdk:10.0 script: - dotnet test tests/CloudSharp.Core.Tests - dotnet test tests/CloudSharp.Infrastructure.Tests backend:image: script: - docker build → ghcr.io/cloud-sharp/cloudsharp-backend:$CI_COMMIT_SHA - docker push (master push only) ``` | 항목 | 상태 | |------|------| | Backend Unit/Infrastructure 테스트 | ✅ | | Docker Image Build → GHCR | ✅ | | nginx 설정 검증 (`nginx -t`) | ✅ | | API IntegrationTests / Architecture Tests | ⚠️ CI 미포함 (개선 예정) | | 자동 배포 (CD) | ⚠️ Placeholder (개선 예정) | --- ## 13. 사용 기술 및 선택 이유 | 기술 | 선택 이유 | |------|-----------| | **ASP.NET Core 10 / Minimal API** | 빠른 부트스트랩, Controller 오버헤드 없음, 최신 런타임 | | **PostgreSQL 16** | FK·Unique·CHECK 제약이 풍부, JSON 확장성 | | **Redis 7** | 빠른 Hash 조회 + Pub/Sub 이벤트 버스 | | **tusd (Go)** | tus 표준 구현체, ASP.NET보다 청크 업로드에 효율적 | | **EF Core 10 + Npgsql** | PostgreSQL Native, Migration 자동화 | | **FluentResults** | 예외 아닌 비즈니스 실패의 명시적 표현, Bind 파이프라인 | | **FluentValidation** | 선언적 검증 + `.WithErrorCode()`로 ErrorCode 표준화 | | **nginx (alpine)** | tus 헤더 포워딩, 버퍼링 해제, 단일 진입점 | | **Docker Compose** | 셀프호스트 배포 간소화 | --- ## 14. 프로젝트 성과 - **데이터 모델**: 12개 엔티티, Mermaid ERD, 11개 EF Core Configuration 클래스 - **요구사항 매핑**: 48개 기능 요구사항(SFR-001~048)을 OpenAPI + UseCase에 매핑 - **상태 머신 설계**: UploadSession 7-state, FileReservation 6-state - **설계 문서**: 14개 컨벤션 문서로 코딩 표준 구축, 3건의 ADR - **CI 자동화**: PR 테스트 + master push 시 GHCR 이미지 빌드 --- ## 15. 회고 ### 잘한 점 - **초기부터 엄격한 문서화**(API, ERD, Conventions, ADR)와 Clean Architecture를 적용해, 기능 확장 시 도메인 경계가 무너지지 않았다. - **JWT 대신 Opaque Session Token**을 선택한 결정이 멀티 Role 모델에 정확히 부합했다. 인증 정보가 토큰에 없으므로 Role 변경이 즉시 반영된다. - **UploadSession과 FileReservation의 1:1 분리**로 네트워크 관점과 도메인 관점의 책임이 명확해졌고, 실패 복구 경로가 단순해졌다. ### 아쉬운 점 - API IntegrationTests와 Architecture Tests가 CI 파이프라인에 포함되지 않았다. - 운영 모니터링(Grafana, OpenTelemetry, 분산 추적)이 설계 단계에 머물러 있다. - 자동 배포(CD)가 placeholder 상태이며, 실제 운영 환경 검증이 부족하다. - 후처리 Worker(ffmpeg 썸네일, AI 메타데이터)는 구조만 있고 미구현이다. ### 향후 계획 - MinIO/S3 Storage Provider 구현으로 Local FS 외 백엔드 검증 - Worker 프로젝트에 ffmpeg 썸네일 파이프라인 적용 - IntegrationTest를 CI에 포함시키고, 자동 배포 파이프라인 완성 - OpenTelemetry 도입 + Grafana/Loki 운영 환경 구축 - 장기적으로 Kubernetes 전환 + OpenSearch 도입 검토