484 lines
20 KiB
Markdown
484 lines
20 KiB
Markdown
# 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<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`로 단순 실행 순서가 아닌 **실제 준비 완료**를 기다린다.
|
||
```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=<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**
|
||
```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 도입 검토 |