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

54 KiB
Raw Permalink Blame History

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 전체 구성 요소 관계

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):

// 실제 코드
public sealed record RegisterRequest(
    [property: Required] string Email,
    [property: Required] string Username,
    [property: Required] string DisplayName,
    [property: Required] string Password
);

구현된 Response (AuthResponse):

// 실제 코드
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 (실제 코드):

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 코드):

{
  "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 토큰 생성 규칙 (실제 코드 기반)

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)

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단계)

// 표준 UseCase 패턴:
1. Command/Query 생성 (DTO 매핑)
2. Validation (FluentValidation  .ValidateAsync)
3. 도메인 검증 (Space 상태, 권한, quota, 파일명 충돌 )
4. Result<T> 반환 (성공  Result DTO, 실패  CloudSharpError)

6.4 Bind 파이프라인 패턴 (업로드)

// 설계 기준 패턴:
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 반환 HTTP 상태 코드 결정, Response DTO 생성, IResult 반환
Domain Policy 상태 전이 검증, 값 객체 생성, 불변조건 확인 인프라 의존성 참조

7. 트랜잭션과 동시성

7.1 트랜잭션이 필요한 기능

Space 생성, 업로드 세션 생성, Finalize 등 여러 repository가 하나의 원자적 흐름을 이룰 때 UseCase에서 트랜잭션을 시작한다.

7.2 트랜잭션 패턴: IAppDbTransactionFactory

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)

-- 동시 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

-- 동일 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 패턴

// 공통 베이스
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 업로드를 프록시할 때 필요한 특수 설정:

# 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 — 전체 변수 구조 (실제 코드)

# 필수: 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__HostPostgres: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

// 좋음:
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)

# 실제 코드
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)

# 실제 코드
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)

# 현재: 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_hashUserSession 저장
  • 인가 판단은 매 요청마다 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 도입.