Files
irukseo/포트폴리오/projects/Cloud#.md
2026-05-05 21:27:37 +09:00

20 KiB
Raw Blame History

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 전체 구성도

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)

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

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 표준 에러 응답

{
  "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에서 명시적 경계 관리.

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 + CloudSharpErrorResultHttpMapper 보안 실패만 Warning
시스템 예외 ExceptionHandlingMiddlewareProblemDetails 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<T> 반환, 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로 단순 실행 순서가 아닌 실제 준비 완료를 기다린다.

api:
  depends_on:
    postgres: { condition: service_healthy }
    redis:    { condition: service_healthy }
    tusd:     { condition: service_healthy }

9.4 nginx — tusd 스트리밍 특수 설정

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. 환경변수 / 설정 관리

# 보안 비밀 (반드시 환경변수로만 주입)
Auth__SessionHashKey=<HMAC secret>
Uploads__FinalizeToken=<openssl rand -base64 32>

# 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

// ✅
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

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 도입 검토