Files
clean-architecture-backend-…/docs/superpowers/specs/2026-08-07-fileserver-platform-design.md
T
DongHyeonkaandClaude Opus 5 5f10b791d3 chore: record pre-existing uncommitted repository state
Snapshot of the in-flight state that already existed, identically, in both
this worktree and the main checkout before this session began: the initial
HTTP Client platform implementation (previously untracked), the redis-lab
removal, and the JPA / object-storage / notification integration work.

Kept separate from this session's HTTP Client review response, which lands
in the following commit, so the two bodies of work stay reviewable apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:48:43 +09:00

58 KiB

Fileserver Platform 설계서

문서 상태: 설계 확정안
작성 기준일: 2026-08-07
입력 근거: Spring 기반 Fileserver 설계 심층 리서치
대상 저장소: Spring 기반 Backend Skeleton


1. 요약

이 설계는 Fileserver를 단순한 업로드·다운로드 컨트롤러가 아니라 다음 네 계층을 분리한 공통 파일 서비스 플랫폼으로 정의한다.

  1. Content Store — byte stream, staging, range read, publish, delete를 담당한다.
  2. Metadata Store — 파일 상태, 소유·권한 연결 정보, 크기, digest, MIME 판정, version, lease, 만료를 관리한다.
  3. Transfer Adapter — Spring MVC, Spring WebFlux, Nginx 위임으로 HTTP 전송을 제공한다.
  4. Verification Layer — checksum, 형식 판정, 악성 파일 검사, quarantine을 담당한다.

공개 API는 fileIduploadId만 사용한다. Path, 실제 파일명, 디렉터리, mount 경로, symlink와 같은 파일시스템 개념은 로컬 저장소 어댑터 밖으로 노출하지 않는다. 파일 내용과 메타데이터는 하나의 ACID transaction으로 묶을 수 없으므로 상태 머신, version, writer lease, reconciliation을 통해 일관성을 유지한다.

최초 Stable 릴리스는 Linux 로컬 파일시스템과 인증된 Kubernetes PVC RWO를 대상으로 다음을 제공한다.

  • raw 및 multipart 단일 업로드
  • 제한형 다중 파일 업로드
  • streaming append와 SHA-256 검증
  • GET, HEAD, 단일 Range, 조건부 요청
  • 직접 전송과 Nginx 위임
  • logical delete와 비동기 physical cleanup
  • MVC와 WebFlux 어댑터
  • 다중 인스턴스용 DB version·lease
  • tus 1.0 별도 Stable 모듈
  • NFS·PVC RWX 제한 지원 프로파일
  • IETF resumable upload draft-12 Experimental 모듈

2. 목표와 성공 기준

2.1 목표

  • 다양한 웹 서비스가 파일 업로드·다운로드를 즉시 사용할 수 있는 공통 기술 모듈을 제공한다.
  • 로컬 디스크, PVC, NFS, 향후 Object Storage가 동일한 저장소 의미론을 공유하도록 한다.
  • 최대 파일 크기에서도 JVM heap 사용량이 파일 크기에 비례하지 않도록 한다.
  • 부분 파일, 경로 탈출, 권한 우회, 검사 전 공개를 구조적으로 차단한다.
  • 장애 후 성공 여부가 모호한 작업을 단순 실패와 구분하고 복구할 수 있게 한다.
  • 구현자가 설계 중 다시 판단하지 않도록 HTTP 계약, 상태 전이, 오류, 설정, 테스트 완료 조건을 고정한다.

2.2 성공 기준

영역 완료 기준
공개 식별자 외부 API가 fileId, uploadId만 사용하고 실제 경로를 노출하지 않는다.
업로드 raw·multipart 스트리밍이 bounded memory로 동작하며 부분 파일은 READY 이전에 읽을 수 없다.
무결성 서버가 actual size와 SHA-256을 계산하고 client digest가 있으면 검증한다.
publish atomic move probe가 통과하거나 metadata pointer publish를 사용한다.
다운로드 200, 206, 304, 412, 416과 관련 header 계약을 일관되게 제공한다.
보안 traversal, symlink escape, 원본명 저장, 무조건 overwrite, 검사 전 공개를 차단한다.
다중 인스턴스 upload별 단일 writer lease와 metadata version 충돌 검사가 동작한다.
장애 복구 process kill, disk full, network interruption 후 READY invariant가 깨지지 않는다.
운영 temp, orphan, quota, disk usage, transfer, verification metric과 cleanup job을 제공한다.
플랫폼 Linux local과 지정 PVC 프로파일의 인증 테스트를 통과한다.

3. 범위

3.1 포함 범위

  • Spring MVC와 Spring WebFlux
  • blocking channel SPI와 async publisher SPI
  • Linux local disk
  • Kubernetes PVC RWO 인증 프로파일
  • 인증된 PVC RWX·NFSv4.1 제한 프로파일
  • Windows NTFS 호환성 CI 프로파일
  • 단일·다중 인스턴스
  • multipart/form-data, application/octet-stream
  • 단일·제한형 다중 파일 업로드
  • streaming upload, cancellation, status, cleanup
  • GET, HEAD, byte range, conditional request, cache header
  • 애플리케이션 직접 전송, zero-copy capability, Nginx 위임
  • SHA-256, MIME·signature 검사 SPI, AV·CDR SPI
  • quota reservation, concurrency limit, storage high-water 보호
  • tus 1.0
  • IETF resumable upload draft-12 Experimental
  • 관리자 health, orphan scan, reconcile, cleanup, reverify
  • metric, trace, audit, problem detail

3.2 제외 범위

  • 공개 API의 임의 절대·상대 경로 입력
  • 공개 디렉터리 list·scan
  • symlink follow·생성
  • hard link 생성
  • 공개 재귀 삭제
  • webroot 내부 저장
  • 원본 파일명 그대로의 physical filename
  • 조건 없는 overwrite
  • READY 이전 다운로드
  • 하나의 offset에 대한 동시 append
  • proxy가 이미 전달한 비멱등 upload의 자동 재시도
  • NFS lock만을 이용한 다중 인스턴스 정합성
  • 다른 FileStore 사이의 atomic move 보장
  • copy 실패 시 자동 rollback 보장
  • 모든 파일 형식의 안전성 판정
  • 임의 ZIP extraction
  • Object Storage provider 구현과 signed URL
  • FTP, SFTP, SMB client 기능

4. 고정 설계 결정

항목 결정
운영 우선 플랫폼 Linux
Java Java 21
Spring 6.2 최신 patch와 7.0 최신 patch를 release matrix에서 검증
MVC 정식 지원, streaming 전용 AsyncTaskExecutor 사용
WebFlux 정식 지원, event loop에서 blocking filesystem I/O 금지
공통 저장소 계약 Path가 아니라 create·append·finalize·stat·openRead·delete 의미론
metadata 기준 관계형 DB의 metadata가 authoritative
publish 기준 same-FileStore atomic move 또는 metadata pointer publish
공개 식별자 opaque FileId, UploadId
physical key 서버가 생성한 ContentKey
원본명 비신뢰 표시 metadata
기본 업로드 create-only
overwrite If-Match 또는 metadata version 필수
checksum 서버 계산 SHA-256 필수, client digest 선택 검증
ETag immutable READY bytes의 SHA-256 strong ETag
private cache private, no-store 기본
재개 업로드 tus 1.0 Stable, HTTPbis draft-12 Experimental
다중 append 단일 writer lease, 병렬 업로드는 독립 part 후 concatenate 방식만
삭제 logical delete 후 physical cleanup
NFS 외부 DB version·lease와 reconciliation을 전제로 제한 지원
Windows 초기 non-blocking compatibility profile

5. 지원 매트릭스

5.1 런타임·저장소

대상 지원 수준 조건
Linux ext4/XFS local 완전 지원 startup capability probe 통과
Kubernetes PVC RWO 조건부 완전 지정 CSI·StorageClass·mount option 인증
Kubernetes PVC RWX 제한 지원 실제 backend별 release certification
NFSv4.1 제한 지원 DB lease·version, ambiguous completion reconciliation
Windows NTFS 호환성 nightly test, 운영 지원은 후속 확정
Nginx stable 완전 지원 internal location과 Range 계약 인증
단일 인스턴스 완전 지원 process-local serialization 가능
다중 인스턴스 완전 지원 조건부 공유 metadata DB와 writer lease 필수

5.2 프로토콜·기능

기능 수준 모듈
raw upload Stable fileserver-mvc, fileserver-webflux
multipart 단일 Stable MVC·WebFlux
multipart batch Stable 제한형 별도 batch endpoint, 비원자적 결과 배열
direct download Stable MVC·WebFlux
single Range Stable core HTTP contract
multi Range Beta 개수·overlap·총량 budget 필수
Nginx delegation Stable fileserver-nginx
tus 1.0 Stable 별도 모듈 fileserver-tus
HTTPbis draft-12 Experimental fileserver-resumable-httpbis-draft12
NFS RWX Limited 인증 프로파일
Windows Compatibility CI profile

6. 전체 아키텍처

HTTP Client
    │
    ├─ Spring MVC Adapter
    ├─ Spring WebFlux Adapter
    └─ tus / HTTPbis Adapter
            │
            ▼
Application Services
    ├─ UploadApplicationService
    ├─ FinalizeUploadService
    ├─ DownloadApplicationService
    ├─ FileLifecycleService
    ├─ CleanupApplicationService
    └─ ReconciliationService
            │
            ├───────────────┐
            ▼               ▼
Metadata Store Port      Content Store Port
    │                       │
    ▼                       ├─ Local Filesystem Adapter
JPA Metadata Adapter        └─ Future Object Storage Adapter
            │
            ├─ Verification Port
            ├─ Authorization Port
            ├─ Quota Port
            └─ Observability

Download path
Application authorization
    ├─ Direct transfer
    └─ Nginx X-Accel-Redirect

6.1 의존 방향

  • fileserver-core-api는 Spring MVC, WebFlux, JPA, NIO 구현 타입에 의존하지 않는다.
  • fileserver-application은 core port만 사용한다.
  • fileserver-storage-local은 NIO와 local path를 캡슐화한다.
  • fileserver-metadata-jpa는 metadata port를 구현한다.
  • HTTP adapter는 application service만 호출한다.
  • Nginx 모듈은 물리 경로 대신 안전한 internal URI descriptor만 생성한다.
  • 검사·권한·quota 정책은 SPI로 주입하며 Fileserver가 비즈니스 규칙을 내장하지 않는다.

6.2 업로드 실행 흐름

1. 인증·기술 정책 확인
2. quota 예약
3. FileRecord(CREATED)와 UploadSession 생성
4. ContentStore.createUpload(CREATE_NEW)
5. FileRecord → UPLOADING
6. stream append + actual size + SHA-256 계산
7. channel close
8. FileRecord → UPLOADED
9. verification 실행
10. VERIFYING / QUARANTINED / REJECTED
11. publish strategy 실행
12. physical stat 재검증
13. metadata pointer, size, digest, MIME, version 기록
14. FileRecord → READY
15. quota 예약을 committed usage로 전환

6.3 다운로드 실행 흐름

1. FileId 조회
2. 존재 은닉 정책을 포함한 authorization
3. READY 상태 확인
4. conditional header 평가
5. Range parsing·budget 검증
6. transfer mode 선택
   - DIRECT
   - ZERO_COPY capability
   - NGINX_DELEGATED
7. 응답 header 확정
8. bytes 전송 또는 internal redirect
9. 성공·중단·전송량 관측

7. 모듈 구조

backend-skeleton/
├── modules/fileserver/
│   ├── fileserver-core-api/
│   ├── fileserver-application/
│   ├── fileserver-metadata-jpa/
│   ├── fileserver-storage-local/
│   ├── fileserver-verification/
│   ├── fileserver-mvc/
│   ├── fileserver-webflux/
│   ├── fileserver-nginx/
│   ├── fileserver-admin/
│   ├── fileserver-tus/
│   ├── fileserver-resumable-httpbis-draft12/
│   ├── fileserver-spring-boot-starter/
│   └── fileserver-testkit/
├── infra/fileserver/
│   ├── local/
│   ├── nginx/
│   ├── nfs/
│   └── kubernetes/
└── docs/fileserver/
    ├── support-matrix.md
    ├── http-contract.md
    ├── storage-certification.md
    ├── security.md
    ├── operations.md
    └── upgrade-guide.md
모듈 책임
fileserver-core-api ID, 상태, value object, port, 오류, capability
fileserver-application upload·download·lifecycle orchestration
fileserver-metadata-jpa metadata, lease, quota reservation persistence
fileserver-storage-local staging, append, range read, publish, delete, probe
fileserver-verification digest, MIME verdict, scanner pipeline
fileserver-mvc Servlet multipart/raw/download adapter
fileserver-webflux PartEvent, DataBuffer, reactive transfer adapter
fileserver-nginx internal URI와 X-Accel-Redirect response strategy
fileserver-admin health, orphan, reconcile, cleanup, reverify
fileserver-tus tus 1.0 protocol adapter
fileserver-resumable-httpbis-draft12 versioned Experimental protocol adapter
fileserver-spring-boot-starter properties, auto-configuration, startup gate
fileserver-testkit contract, filesystem, HTTP, fault, performance harness

8. 핵심 공개 모델

8.1 식별자

public record FileId(UUID value) {
    public FileId {
        Objects.requireNonNull(value, "value");
    }
}

public record UploadId(UUID value) {
    public UploadId {
        Objects.requireNonNull(value, "value");
    }
}

public record ContentKey(String value) {
    public ContentKey {
        if (value == null || !value.matches("[a-z0-9/_-]{16,200}")) {
            throw new IllegalArgumentException("invalid content key");
        }
    }
}

public record StorageNamespace(String value) {
    public StorageNamespace {
        if (value == null || !value.matches("[a-z][a-z0-9-]{1,62}")) {
            throw new IllegalArgumentException("invalid storage namespace");
        }
    }
}

ContentKey는 public HTTP contract에 포함하지 않는다. FileId는 추측하기 어려운 ID를 사용하지만 비밀 token으로 취급하지 않으며 모든 요청에서 authorization을 수행한다.

8.2 파일 상태

public enum FileState {
    CREATED,
    UPLOADING,
    UPLOADED,
    VERIFYING,
    QUARANTINED,
    READY,
    REJECTED,
    FAILED,
    DELETING,
    DELETED,
    EXPIRED
}

허용 전이는 FileStateMachine 하나에서 관리한다. persistence adapter나 controller가 상태를 직접 대입하지 않는다.

public interface FileStateMachine {
    void requireTransition(FileState current, FileState target);
    boolean canTransition(FileState current, FileState target);
}

8.3 ByteRange

public record ByteRange(long startInclusive, long endInclusive) {
    public ByteRange {
        if (startInclusive < 0 || endInclusive < startInclusive) {
            throw new IllegalArgumentException("invalid byte range");
        }
    }

    public long length() {
        return Math.addExact(Math.subtractExact(endInclusive, startInclusive), 1);
    }
}

HTTP suffix/open-ended Range는 HTTP adapter의 parser가 현재 representation 길이를 기준으로 위 value object로 정규화한다.

8.4 파일 metadata

public record FileDescriptor(
    FileId fileId,
    StorageNamespace namespace,
    FileState state,
    String originalFilename,
    String mediaType,
    long size,
    String sha256,
    String strongEtag,
    Instant publishedAt,
    long version
) {}

실제 path, scanner 원문 응답, user metadata 원문은 public descriptor에 포함하지 않는다.


9. 상태 머신과 invariant

9.1 상태 전이

CREATED → UPLOADING
UPLOADING → UPLOADED | FAILED | EXPIRED | DELETING
UPLOADED → VERIFYING | FAILED | DELETING
VERIFYING → READY | QUARANTINED | REJECTED | FAILED
QUARANTINED → VERIFYING | READY | REJECTED | DELETING
READY → DELETING
REJECTED → DELETING
FAILED → UPLOADING | VERIFYING | DELETING | EXPIRED
DELETING → DELETED | FAILED
EXPIRED → DELETING

FAILED에서의 복구 전이는 저장된 lastErrorCode와 recovery policy가 허용할 때만 수행한다.

9.2 필수 invariant

  • READY에는 읽을 수 있는 immutable content가 존재한다.
  • READY의 size와 SHA-256은 실제 bytes와 일치한다.
  • READY가 아닌 레코드는 direct download와 Nginx internal mapping에서 제외된다.
  • 하나의 upload에는 하나의 유효 writer lease만 존재한다.
  • offset은 durable append가 확인된 byte 수만큼만 증가한다.
  • client가 주장한 크기·MIME·파일명은 authoritative 값이 아니다.
  • REJECTED, DELETED, EXPIRED는 public API에서 재활성화되지 않는다.
  • DB와 storage가 불일치하면 READY를 추정하지 않고 recovery queue로 보낸다.
  • logical delete가 성공하면 신규 download authorization은 즉시 차단된다.
  • physical cleanup 실패는 DELETING 또는 FAILED 상태와 운영 경보로 남는다.

10. Metadata Store 설계

10.1 Port

public interface FileMetadataStore {
    FileRecord insert(FileRecordDraft draft);
    Optional<FileRecord> find(FileId fileId);
    FileRecord transition(
        FileId fileId,
        long expectedVersion,
        FileState expectedState,
        FileState targetState,
        FileRecordMutation mutation
    );
    FileRecord markDeleting(FileId fileId, long expectedVersion);
    List<FileRecord> findRecoverable(FileRecoveryQuery query);
}

public interface UploadSessionStore {
    UploadSession create(UploadSessionDraft draft);
    Optional<UploadSession> find(UploadId uploadId);
    WriterLease acquireLease(
        UploadId uploadId,
        String owner,
        Instant now,
        Duration leaseDuration,
        long expectedVersion
    );
    UploadSession commitOffset(
        UploadId uploadId,
        WriterLease lease,
        long expectedOffset,
        long committedOffset
    );
    void releaseLease(UploadId uploadId, WriterLease lease);
    List<UploadSession> findExpired(Instant cutoff, int limit);
}

10.2 관계형 schema

Table 핵심 컬럼
fs_file file_id, namespace, state, content_key, original_name, claimed_media_type, verified_media_type, expected_size, actual_size, sha256, strong_etag, published_at, version, last_error_code, timestamps
fs_upload_session upload_id, file_id, expected_length, committed_offset, protocol, expires_at, lease_owner, lease_until, version
fs_verification_result file_id, verifier, verdict, details_code, started_at, completed_at
fs_quota_reservation reservation_id, scope, reserved_bytes, committed_bytes, expires_at, status, version
fs_cleanup_item cleanup_id, file_id, content_key, type, attempt, next_attempt_at, status, last_error_code

fs_file.versionfs_upload_session.version은 optimistic locking에 사용한다. 모든 상태 전이는 WHERE version = ? AND state = ? 조건을 포함한다.

10.3 authoritative source

  • 공개 metadata는 fs_file을 기준으로 한다.
  • physical stat은 publish 검증과 reconciliation에 사용한다.
  • NFS·PVC의 timestamp는 Last-Modified의 authoritative source로 사용하지 않는다.
  • published_at을 HTTP Last-Modified로 사용한다.

11. Content Store Port

11.1 Capability

public record ContentStoreCapabilities(
    boolean rangedRead,
    boolean atomicCreate,
    boolean atomicPublish,
    boolean conditionalWrite,
    boolean serverSideCopy,
    boolean delegatedDownload,
    boolean resumableAppend
) {}

Capability는 설정값만 읽지 않고 실제 저장소 root에서 startup probe한 결과로 생성한다.

11.2 Blocking SPI

public interface BlockingContentStore {
    UploadHandle createUpload(CreateContentCommand command);

    AppendResult append(
        UploadHandle handle,
        long expectedOffset,
        ReadableByteChannel source,
        long contentLength
    );

    StoredContent finalizeUpload(
        UploadHandle handle,
        FinalizeContentCommand command
    );

    ContentMetadata stat(ContentKey key);

    ReadableByteChannel openRead(ContentKey key, ByteRange range);

    DeleteResult delete(ContentKey key, DeletePrecondition precondition);

    ContentStoreCapabilities capabilities();
}

11.3 Async SPI

public interface AsyncContentStore {
    CompletionStage<UploadHandle> createUpload(CreateContentCommand command);

    CompletionStage<AppendResult> append(
        UploadHandle handle,
        long expectedOffset,
        Flow.Publisher<ByteBuffer> content
    );

    CompletionStage<StoredContent> finalizeUpload(
        UploadHandle handle,
        FinalizeContentCommand command
    );

    CompletionStage<ContentMetadata> stat(ContentKey key);

    Flow.Publisher<ByteBuffer> openRead(ContentKey key, ByteRange range);

    CompletionStage<DeleteResult> delete(
        ContentKey key,
        DeletePrecondition precondition
    );

    ContentStoreCapabilities capabilities();
}

공통 SPI에 Spring Resource, DataBuffer, Reactor 타입을 포함하지 않는다. WebFlux adapter는 Flow.Publisher<ByteBuffer>Flux<DataBuffer> 사이를 변환하고 pooled buffer의 수명주기를 책임진다.

11.4 Capability 확장

public interface CopyCapableContentStore {
    CompletionStage<StoredContent> copy(
        ContentKey source,
        ContentKey target,
        CopyPrecondition precondition
    );
}

public interface CapacityAwareContentStore {
    StorageCapacity capacity();
}

public interface DelegatedDownloadStore {
    DelegatedDownloadDescriptor createDelegation(
        ContentKey key,
        ByteRange range,
        Duration ttl
    );
}

copy, capacity, delegation은 최소 Port에 강제하지 않는다.


12. Local Filesystem Adapter

12.1 저장 레이아웃

${root}/
├── staging/
│   └── ab/cd/<upload-id>.part
├── content/
│   └── ab/cd/<content-key>.bin
├── quarantine/
│   └── ab/cd/<content-key>.bin
└── probe/
  • shard는 server-generated ID의 앞 2 byte씩 사용한다.
  • 원본 파일명과 확장자를 physical filename에 사용하지 않는다.
  • staging, content, quarantine은 동일 FileStore에 위치해야 한다.
  • root는 application source, config, webroot와 분리한다.
  • startup에서 디렉터리 owner·permission을 검증한다.

12.2 경로 안전 규칙

public interface PhysicalPathResolver {
    Path stagingPath(UploadId uploadId);
    Path contentPath(ContentKey contentKey);
    Path quarantinePath(ContentKey contentKey);
}

PhysicalPathResolverfileserver-storage-local 내부 package-private 구현으로 둔다. 공개 module export 대상이 아니다.

필수 검사:

  1. absolute 또는 drive-qualified 입력을 받지 않는다.
  2. ID에서 만든 고정 component만 resolve한다.
  3. normalize 결과가 root 아래인지 확인한다.
  4. 모든 open·stat·delete에 NOFOLLOW_LINKS를 사용한다.
  5. parent component가 symlink인지 확인한다.
  6. provider가 지원하면 SecureDirectoryStream을 사용한다.
  7. open 후 file identity와 expected parent identity를 재검증한다.

12.3 staging 생성

  • CREATE_NEW, WRITE, NOFOLLOW_LINKS로 연다.
  • 충돌 시 새로운 storage key를 재발급하지 않고 invariant violation으로 기록한다.
  • file permission은 owner read·write만 허용하는 프로파일을 기본으로 한다.
  • append 전에 실제 file length와 metadata offset을 대조한다.

12.4 append

  • 고정 크기 direct buffer pool 또는 heap buffer를 사용하며 파일 전체를 적재하지 않는다.
  • 기본 buffer는 128 KiB다.
  • expectedOffset이 실제 길이 또는 metadata offset과 다르면 append를 수행하지 않는다.
  • 실제 수신 byte 수가 정책 최대값을 넘으면 즉시 중단한다.
  • append 도중 실제 size와 SHA-256을 streaming 계산한다.
  • contentLength >= 0이면 실제 append byte와 일치해야 한다.
  • cancellation과 exception 시 channel을 닫고 session은 복구 가능한 상태로 남긴다.

12.5 delete

  • symbolic link를 따라가지 않는다.
  • logical delete를 먼저 수행한 뒤 cleanup worker가 physical object를 삭제한다.
  • large file은 삭제 latency와 filesystem 특성을 metric으로 기록한다.
  • 실제 파일이 이미 없으면 idempotent success로 처리하되 reconciliation event를 남긴다.

13. Storage Capability Probe와 Startup Gate

13.1 Probe 항목

Probe 통과 기준 실패 정책
writable root create·write·close·delete 성공 startup 실패
CREATE_NEW 경쟁 두 동시 create 중 정확히 하나 성공 startup 실패
same FileStore staging·content·quarantine 동일 startup 실패
atomic move observer가 partial target을 보지 않고 move 성공 mode에 따라 실패 또는 pointer publish
replace old 또는 new만 관측 overwrite capability 비활성
fsync profile force 후 restart test 결과 저장 durability 등급 표시
symlink no-follow target 접근이 차단됨 startup 실패
open-delete OS 동작 기록 lifecycle policy 조정
capacity usable·total 조회 가능 admin capability 제한

13.2 Publish mode

public enum PublishMode {
    ATOMIC_MOVE_REQUIRED,
    ATOMIC_MOVE_PREFERRED,
    METADATA_POINTER
}
  • ATOMIC_MOVE_REQUIRED: probe 실패 시 startup 실패
  • ATOMIC_MOVE_PREFERRED: 가능하면 atomic move, 불가능하면 pointer publish
  • METADATA_POINTER: immutable physical key를 완성한 뒤 DB pointer를 READY boundary로 사용

기본값은 ATOMIC_MOVE_PREFERRED다.

13.3 Runtime capability endpoint

GET /internal/fileserver/capabilities는 다음을 제공한다.

{
  "storageType": "LOCAL",
  "publishMode": "ATOMIC_MOVE_PREFERRED",
  "rangedRead": true,
  "atomicCreate": true,
  "atomicPublish": true,
  "conditionalWrite": true,
  "delegatedDownload": true,
  "resumableAppend": true,
  "filesystemProfile": "linux-ext4"
}

physical root와 mount detail은 반환하지 않는다.


14. Publish와 완료 처리

14.1 Atomic move strategy

staging channel close
→ optional `FileChannel.force(true)`
→ verify expected length·digest
→ target parent 준비
→ `Files.move(staging, target, ATOMIC_MOVE)`
→ target stat
→ DB READY transition

REPLACE_EXISTING은 overwrite precondition이 있는 경로에서만 사용한다. create-only 경로는 target이 이미 있으면 실패한다.

14.2 Metadata pointer strategy

staging write 완료
→ immutable content key로 새 physical object 완성
→ physical stat 검증
→ DB transaction에서 contentKey pointer와 READY 상태 publish
→ 이전 physical object를 cleanup queue에 등록

이 전략은 rename의 원자성 대신 metadata store transaction을 public publish boundary로 사용한다.

14.3 Ambiguous completion

다음 상황은 AmbiguousCompletionException으로 분류한다.

  • NFS rename request가 서버에서 처리되었을 수 있으나 응답이 유실됨
  • write·force 후 연결 또는 mount 응답이 사라짐
  • DB commit 응답을 받지 못해 상태 전이 성공 여부를 알 수 없음

처리 순서:

  1. operation ID와 expected physical key를 조회한다.
  2. metadata version과 state를 재조회한다.
  3. physical stat·size·digest를 확인한다.
  4. 명백한 성공이면 성공 결과를 복원한다.
  5. 명백한 미실행이면 제한적으로 재실행한다.
  6. 판정 불가면 recovery queue와 retryable=false, reconciliationRequired=true 오류를 반환한다.

15. Upload Application 설계

15.1 공개 command

public record CreateUploadRequest(
    StorageNamespace namespace,
    String originalFilename,
    String claimedMediaType,
    OptionalLong expectedLength,
    Optional<String> expectedSha256,
    UploadProtocol protocol,
    Instant expiresAt
) {}

public interface UploadApplicationService {
    UploadSessionView create(CreateUploadRequest request, RequestContext context);

    AppendUploadResult append(
        UploadId uploadId,
        long expectedOffset,
        ReadableByteChannel content,
        long contentLength,
        RequestContext context
    );

    FileView finalizeUpload(
        UploadId uploadId,
        FinalizeUploadRequest request,
        RequestContext context
    );

    UploadSessionView status(UploadId uploadId, RequestContext context);

    void cancel(UploadId uploadId, RequestContext context);
}

Async API는 별도 interface로 동일 의미를 제공한다.

15.2 Create

  • authorization hook 실행
  • expected length가 있으면 정책 최대값 검증
  • quota reservation 생성
  • FileRecord CREATED 생성
  • UploadSession 생성
  • storage staging 생성
  • state를 UPLOADING으로 전이
  • Location과 current offset 0 반환

DB 생성 후 storage 생성이 실패하면 FileRecord를 FAILED로 전이하고 quota reservation을 해제한다. storage 생성 후 DB 응답이 모호하면 operation ID로 reconciliation한다.

15.3 Append

  • upload 상태·만료 확인
  • writer lease 획득
  • metadata offset, physical length, request offset 일치 검증
  • concurrency, rate, storage high-water gate 확인
  • streaming append
  • committed offset 저장
  • lease release

append 실패 후 offset은 실제 저장이 확인된 길이까지만 증가한다. metadata offset과 physical length가 다르면 자동 append하지 않고 reconciliation으로 보낸다.

15.4 Finalize

  • expected length가 있으면 committed offset과 비교
  • server SHA-256과 client digest 비교
  • state를 UPLOADED로 전이
  • verification pipeline 실행
  • verdict가 ACCEPT이면 publish
  • metadata READY 전이
  • quota commit
  • REJECT 또는 QUARANTINE이면 public download 금지

15.5 Multipart batch

POST /v1/files:batch는 다음 계약을 사용한다.

  • 최대 part 수 기본 16
  • 각 파일은 독립 FileRecord·UploadSession
  • 요청 전체 ACID 원자성은 보장하지 않는다.
  • 일부 실패 시 성공 파일을 rollback하지 않는다.
  • 200 OK와 파일별 결과 배열을 반환한다.
  • 총 request byte와 tenant quota를 요청 전·중 모두 검사한다.
{
  "results": [
    {"clientPartId":"a", "status":"CREATED", "fileId":"..."},
    {"clientPartId":"b", "status":"REJECTED", "problem":{"code":"FILE_TOO_LARGE"}}
  ]
}

16. Verification Layer

16.1 Port

public interface FileVerifier {
    String verifierId();
    CompletionStage<VerificationResult> verify(VerificationRequest request);
}

public record VerificationResult(
    VerificationVerdict verdict,
    String code,
    Optional<String> verifiedMediaType,
    Map<String, String> safeMetadata
) {}

public enum VerificationVerdict {
    ACCEPT,
    QUARANTINE,
    REJECT,
    RETRY
}

16.2 기본 pipeline

Length verifier
→ SHA-256 verifier
→ filename policy
→ media type detector
→ signature/parser verifier
→ optional AV scanner
→ optional CDR
→ final policy combiner
  • client Content-Type은 claimed metadata로만 저장한다.
  • 단순 magic byte 일치만으로 안전 판정을 내리지 않는다.
  • scanner timeout은 READY로 우회하지 않는다.
  • 위험 형식은 quarantine 또는 reject한다.
  • HTML, SVG 등 scriptable 문서는 기본 attachment이며 inline은 명시적 안전 프로파일에서만 허용한다.

16.3 검사 비동기화

  • 검사 시간이 짧은 프로파일은 upload request 안에서 완료하여 201을 반환할 수 있다.
  • AV·CDR처럼 긴 검사는 202 Accepted와 VERIFYING 상태를 반환한다.
  • READY 전환은 verification worker가 수행한다.
  • retryable scanner 장애는 exponential backoff와 최대 시도 횟수를 사용한다.
  • 최대 시도 초과는 FAILED 또는 QUARANTINED로 전이한다.

17. Authorization과 기술 정책 Hook

public interface FileAccessPolicy {
    void authorize(FileOperation operation, FileAccessSubject subject, FileDescriptor descriptor);
}

public enum FileOperation {
    CREATE,
    APPEND,
    FINALIZE,
    READ_METADATA,
    DOWNLOAD,
    DELETE,
    COPY,
    MOVE,
    ADMIN_REVERIFY,
    ADMIN_FORCE_DELETE
}

Fileserver는 사용자 등급·업무 역할 같은 비즈니스 정책을 내장하지 않는다. 대신 모든 공개 operation에서 위 hook을 반드시 호출하고, starter가 no-op allow-all 구현을 운영 프로파일에서 자동 생성하지 않도록 한다.

존재 은닉 프로파일에서는 권한 없는 file에 404를 반환한다. 내부 audit에는 ACCESS_DENIED를 기록하되 fileId·userId 원문을 metric label에 사용하지 않는다.


18. Quota, Capacity와 Transfer Budget

18.1 Quota Port

public interface FileQuotaService {
    QuotaReservation reserve(QuotaScope scope, long expectedBytes, Duration ttl);
    void extend(QuotaReservation reservation, long additionalBytes);
    void commit(QuotaReservation reservation, long actualBytes);
    void release(QuotaReservation reservation);
}

expected length가 없으면 프로파일별 initial reservation을 잡고 append 중 증분 예약한다.

18.2 기본 운영 프로파일

설정 Standard Large-file
최대 파일 100 MiB 5 GiB
최대 request 116 MiB 5 GiB + 16 MiB
최대 multipart part 16 16
in-memory part 512 KiB 256 KiB
stream buffer 128 KiB 256 KiB
인스턴스 동시 upload 16 32
인스턴스 direct download 64 128
scope 동시 upload 4 8
temp soft limit usable 70% usable 70%
temp hard limit usable 85% usable 85%
idle read timeout 45 s 60 s
미완료 upload TTL 24 h 72 h
multi Range 최대 개수 8 8

이 값은 starter 기본값이며 운영 환경은 부하 인증 결과로 재정의한다.

18.3 Admission control

새 upload는 다음 중 하나가 발생하면 거절한다.

  • quota reservation 실패
  • storage hard high-water 초과
  • instance upload permit 고갈
  • scope 동시성 초과
  • verification queue hard limit 초과

soft high-water에서는 대용량 upload를 throttle하거나 429/503Retry-After를 반환한다.


19. HTTP API

19.1 공개 endpoint

Method·Path 목적 성공
POST /v1/files multipart 단일 업로드 201 READY 또는 202 VERIFYING
POST /v1/files:raw raw streaming 업로드 201 또는 202
POST /v1/files:batch 제한형 다중 업로드 200 결과 배열
PUT /v1/files/{fileId}/content create-only·조건부 교체 201 또는 204
GET /v1/files/{fileId} metadata 200
GET /v1/files/{fileId}/content download 200, 206, 304
HEAD /v1/files/{fileId}/content download metadata 200, 304
DELETE /v1/files/{fileId} logical delete 202 또는 204
POST /v1/files/{fileId}:copy 조건부 copy 202
POST /v1/files/{fileId}:move logical namespace move 200 또는 204
POST /v1/uploads resumable resource 생성 201
HEAD /v1/uploads/{uploadId} offset 조회 protocol별 200/204
PATCH /v1/uploads/{uploadId} append 204
DELETE /v1/uploads/{uploadId} cancel 204

19.2 Header 계약

Header 계약
Content-Type client 값은 claimed type, verified type을 별도 저장
Content-Length 있으면 사전 검증, 없어도 streamed hard limit 적용
Content-Disposition inline 또는 attachment, filename + filename*
Accept-Ranges byte range 지원 시 bytes
Range 기본 single, budget이 있는 경우 제한형 multi
Content-Range 206 실제 범위, 416bytes */size
ETag SHA-256 strong validator
Last-Modified publishedAt
If-None-Match GET·HEAD revalidation, create-only *
If-Modified-Since ETag 보조
If-Match overwrite·delete lost-update 방지
If-Range validator 일치 시에만 partial
Cache-Control private 기본 private, no-store
Content-Digest 실제 HTTP message content digest
Repr-Digest 전체 representation digest 선택 제공
Location 생성된 file·upload resource
Retry-After 429, 503, 장기 검사의 polling 힌트
X-Accel-Redirect Nginx 내부 응답 전용

19.3 상태 코드

Status 조건
200 metadata, 전체 GET, batch result
201 file 또는 upload 생성
202 검사 또는 physical cleanup 비동기
204 append, cancel, body 없는 update
206 satisfiable Range
304 GET·HEAD validator 일치
400 잘못된 header·요청 조합
401 인증 없음
403/404 접근 거부 또는 존재 은닉
409 상태·offset·lease 충돌
410 만료 upload
411 require-content-length=true 프로파일
412 precondition 실패
413 크기·quota 정책 위반
415 허용하지 않는 upload media type
416 만족 불가능 Range
422 digest·signature·scanner reject
429 동시성·rate limit
503 storage·scanner unavailable
504 downstream timeout
507 저장공간 부족

20. Range와 Conditional Request

20.1 Range parser

public interface HttpRangeResolver {
    ResolvedRanges resolve(String rangeHeader, long representationLength, RangeBudget budget);
}

public record RangeBudget(
    int maxRanges,
    long maxTotalBytes,
    boolean mergeOverlaps
) {}

기본 public 다운로드는 single Range만 허용한다. multi Range를 활성화한 profile에서는 최대 8개, overlap merge 후 총 byte가 representation 길이 이하인 경우만 허용한다.

20.2 응답 결정 순서

authorization
→ READY 확인
→ current ETag·Last-Modified 계산
→ If-Match / If-Unmodified-Since
→ If-None-Match / If-Modified-Since
→ Range parse
→ If-Range 평가
→ 200 / 206 / 304 / 412 / 416 결정

If-Range가 불일치하면 Range를 무시하고 전체 200을 반환한다.

20.3 ETag와 digest

  • stored SHA-256을 quoted strong ETag로 사용한다.
  • metadata-only 변경은 representation ETag를 바꾸지 않는다.
  • Content-Digest는 전송 bytes 기준이다.
  • full response에서는 stored SHA-256을 재사용할 수 있다.
  • partial response에서는 해당 range digest를 streaming 계산하거나 기능을 비활성화한다.
  • 전체 representation digest가 필요하면 Repr-Digest를 제공한다.

21. Spring MVC Adapter

21.1 Upload

  • MultipartFile#getBytes()를 사용하지 않는다.
  • raw upload는 request input stream을 ReadableByteChannel로 변환한다.
  • multipart는 container threshold와 temp directory를 starter가 명시적으로 설정한다.
  • upload request thread가 storage write를 장시간 점유하지 않도록 전용 executor를 사용한다.
  • 기본 executor는 bounded queue와 rejection policy를 가진다.
  • request cancellation과 client disconnect를 application service에 전달한다.

21.2 Download

전송 전략은 다음 순서로 선택한다.

  1. Nginx 위임이 활성화되고 threshold 이상이면 delegation
  2. local Path를 안전하게 반환할 수 있고 zero-copy 조건이 맞으면 zero-copy capability
  3. 그 외 StreamingResponseBody

Range 처리는 core HTTP contract가 결정한다. Spring의 자동 Range 지원에만 의존하지 않고 MVC와 WebFlux가 같은 결과를 반환하도록 공통 resolver를 사용한다. InputStreamResource는 반복 가능한 Range resource로 사용하지 않는다.

21.3 Executor

public record MvcTransferExecutorProperties(
    int coreThreads,
    int maxThreads,
    int queueCapacity,
    Duration shutdownTimeout
) {}

기본값:

coreThreads=8
maxThreads=32
queueCapacity=64
shutdownTimeout=30s

queue가 가득 차면 무제한 대기하지 않고 429 또는 503으로 변환한다.


22. Spring WebFlux Adapter

22.1 Upload

  • raw body는 Flux<DataBuffer>를 순차 소비한다.
  • multipart streaming은 Flux<PartEvent>를 사용한다.
  • pooled DataBuffer는 전달하거나 명시적으로 release한다.
  • blocking local filesystem adapter 호출은 bounded elastic이 아니라 전용 bounded scheduler에서 실행한다.
  • async store가 제공되면 event loop를 유지한 채 Flow.Publisher<ByteBuffer>로 전달한다.
  • cancellation 시 channel, lease, temp resource를 정리한다.

22.2 Download

  • async store는 Flux<DataBuffer>로 변환한다.
  • local file zero-copy가 runtime에서 가능하면 capability optimization으로 사용한다.
  • Range와 conditional 결정은 MVC와 동일한 core resolver를 사용한다.
  • slow client에서 in-flight buffer 수가 설정 상한을 넘지 않도록 한다.

22.3 Blocking 검출

CI에서 BlockHound 또는 동등한 검증으로 다음을 차단한다.

  • event loop에서 Files.*, FileChannel, JDBC 호출
  • synchronous scanner 호출
  • blocking metadata repository 호출

23. Nginx 전송 위임

23.1 구조

Client
→ GET /v1/files/{fileId}/content
→ Application authorization + READY gate
→ validated ContentKey를 internal relative URI로 변환
→ X-Accel-Redirect: /__files/ab/cd/<content-key>.bin
→ Nginx internal location
→ physical content transfer

internal URI는 절대 physical path를 포함하지 않는다. NginxInternalUriMapper는 검증된 ContentKey만 받아 /__files/ 아래의 상대 URI를 생성한다. 이 header는 Nginx가 내부 redirect로 소비하므로 client 응답에는 노출하지 않는다. 별도 공개 signed URL을 발급하는 기능은 Object Storage 모듈의 책임으로 남긴다.

23.2 정책

  • 기본 delegation threshold는 16 MiB다.
  • private file은 Nginx shared cache를 기본 비활성화한다.
  • internal location은 외부 직접 요청을 거부한다.
  • X-Accel-Redirect는 downstream client에 그대로 전달되지 않도록 한다.
  • Range, ETag, Content-Disposition, Cache-Control 결과가 direct mode와 동일해야 한다.
  • Nginx access log에 physical root와 원본 파일명을 남기지 않는다.
  • mapper가 생성한 URI는 ContentKey의 허용 문자와 shard 규칙을 다시 검증한다.

23.3 Nginx upload

경로 기본 buffering
작은 multipart on 허용
대용량 raw off
tus PATCH off
HTTPbis PATCH off

upstream 전송이 시작된 non-idempotent upload에는 proxy_next_upstream 재시도를 적용하지 않는다.


24. 재개 가능한 업로드

24.1 공통 원칙

  • upload resource별 single writer lease
  • offset은 metadata와 physical length를 함께 검증
  • mismatch 시 body를 쓰지 않고 409
  • 서버 재시작 후 offset reconciliation
  • create 시 quota 예약
  • expiration과 cleanup
  • client checksum 검증
  • upload resource는 READY file과 별도 수명주기를 가진다.

24.2 tus 1.0 Stable

지원 기능:

  • creation
  • HEADUpload-Offset
  • PATCH application/offset+octet-stream
  • checksum extension
  • expiration extension
  • termination extension
  • concatenation extension은 Beta

성공 append는 204와 새 Upload-Offset을 반환한다. offset mismatch는 resource를 변경하지 않고 409를 반환한다.

24.3 HTTPbis draft-12 Experimental

  • module 이름과 package에 draft12를 포함한다.
  • feature flag 없이는 bean을 생성하지 않는다.
  • media type과 header를 draft version에 고정한다.
  • 104 interim response 지원 여부를 runtime capability로 표시한다.
  • 최종 RFC 변화에 따른 breaking change를 허용한다.
  • Stable core와 endpoint namespace를 분리한다.

24.4 병렬 upload

하나의 upload offset에 여러 writer를 허용하지 않는다. 병렬 전송은 다음 구조만 제공한다.

parent upload
├─ part 1 resource
├─ part 2 resource
└─ part N resource
→ 각 part checksum 검증
→ 순서와 총 길이 검증
→ concatenate
→ final verification

25. 파일 관리 기능

25.1 stat

공개 stat은 DB metadata를 반환한다. physical stat은 내부 일관성 검증에만 사용한다.

25.2 delete

If-Match 검증
→ READY/REJECTED/FAILED → DELETING
→ 공개 read 즉시 차단
→ cleanup item 등록
→ physical delete
→ quota 반영
→ DELETED

25.3 copy

  • capability가 없으면 application-level stream copy를 사용한다.
  • target은 create-only가 기본이다.
  • source와 target metadata는 별도 레코드다.
  • copy 실패 시 incomplete target은 cleanup queue로 보낸다.
  • 자동 rollback 보장을 선언하지 않는다.

25.4 move

공개 move는 physical path move가 아니라 logical namespace·ownership metadata 변경이다. physical content는 immutable key를 유지한다. physical move는 admin maintenance에만 사용한다.

25.5 list·scan

public API에는 제공하지 않는다. admin API는 bounded pagination, prefix allowlist, rate limit, dry-run을 요구한다.


26. 오류 모델과 Problem Detail

26.1 예외 hierarchy

FileserverException
├─ FileNotFoundException
├─ FileAlreadyExistsException
├─ InvalidPathException
├─ PathOutsideNamespaceException
├─ FileAccessDeniedException
├─ StorageFullException
├─ QuotaExceededException
├─ FileTooLargeException
├─ UnsupportedMediaTypeException
├─ IntegrityMismatchException
├─ UploadOffsetMismatchException
├─ UploadExpiredException
├─ FileNotReadyException
├─ AtomicPublishUnsupportedException
├─ TransferTimeoutException
├─ PartialWriteException
├─ AmbiguousCompletionException
├─ StorageUnavailableException
├─ ConcurrentFileModificationException
└─ MalwareDetectedException

모든 예외는 다음 metadata를 가진다.

public record FileserverFailureContext(
    String code,
    boolean retryable,
    boolean ambiguous,
    boolean reconciliationRequired,
    Optional<FileId> fileId,
    Optional<UploadId> uploadId,
    OptionalLong expectedOffset,
    OptionalLong currentOffset,
    Optional<FileState> currentState
) {}

26.2 Problem Detail

{
  "type": "urn:fileserver:problem:upload-offset-mismatch",
  "title": "Upload offset mismatch",
  "status": 409,
  "code": "UPLOAD_OFFSET_MISMATCH",
  "retryable": true,
  "uploadId": "...",
  "expectedOffset": 1048576,
  "currentOffset": 524288,
  "traceId": "..."
}

내부 path, mount, scanner credential, storage token을 포함하지 않는다.


27. 보안 정책

27.1 위험 등급

등급 대상 정책
F1 ID 기반 create·read·delete, single Range 기본 허용, auth·size·state gate
F2 대용량 stream, multi Range, resumable, overwrite, copy quota·budget·precondition 필수
F3 list, capacity, orphan, force delete, reverify internal admin plane
F4 arbitrary path, symlink, recursive delete, webroot storage 전체 차단

27.2 필수 방어

  • opaque ID와 server-generated physical key
  • original filename sanitization
  • extension allowlist가 있더라도 Content-Type을 신뢰하지 않음
  • signature/parser·scanner verdict
  • executable permission 제거
  • separate mount와 webroot 밖 저장
  • size, part count, concurrency, minimum-rate 제한
  • private download cache 제한
  • READY gate
  • CSRF 방어가 필요한 cookie 기반 upload endpoint
  • authorization on every access
  • range bomb 제한
  • ZIP/XML expanded-size 제한을 verifier에 적용

27.3 파일명 sanitization

제거·치환 대상:

  • /, \, NUL
  • control characters
  • bidi override characters
  • CR/LF와 quote injection
  • trailing dot·space
  • Windows reserved names
  • UTF-8 255 byte 초과

sanitized name은 Content-Disposition에만 사용하며 physical path 생성에는 사용하지 않는다.


28. 다중 인스턴스와 NFS

28.1 Writer lease

public record WriterLease(
    UploadId uploadId,
    String owner,
    UUID token,
    Instant expiresAt,
    long version
) {}
  • DB conditional update로 획득한다.
  • append 중 주기적으로 갱신한다.
  • lease token이 다르면 offset commit을 거부한다.
  • process pause로 lease가 만료된 writer는 이후 commit하지 못한다.
  • local file lock이나 NFS lock을 correctness 근거로 사용하지 않는다.

28.2 NFS reconciliation

다음 이벤트에서 metadata와 physical state를 재확인한다.

  • rename timeout
  • stale file handle
  • mount reconnect
  • attribute mismatch
  • server restart
  • lease takeover

reconciliation 결과:

CONFIRMED_SUCCESS
CONFIRMED_NOT_APPLIED
RECOVERABLE_PARTIAL
QUARANTINE_REQUIRED
UNRESOLVED

UNRESOLVED는 자동 retry하지 않고 운영 queue로 보낸다.

28.3 PVC certification unit

지원 단위는 PVC라는 이름이 아니라 다음 tuple이다.

Kubernetes version
+ CSI driver/version
+ StorageClass
+ access mode
+ filesystem/backend
+ mount options

29. Cleanup와 Reconciliation

29.1 Cleanup 종류

  • expired upload
  • cancelled staging
  • failed verification content
  • deleted READY content
  • orphan physical object
  • stale quota reservation
  • abandoned lease
  • previous version after pointer publish

29.2 안전 규칙

  • cleanup은 version과 lease를 확인한다.
  • 기본 admin 실행은 dry-run이다.
  • active upload와 동일 physical key는 삭제하지 않는다.
  • batch size와 bytes budget을 둔다.
  • 실패는 exponential backoff와 최대 retry를 사용한다.
  • 장기 실패는 orphan metric과 alert로 승격한다.

29.3 Reconciliation

public interface FileReconciliationService {
    ReconciliationResult reconcile(FileId fileId);
    ReconciliationBatchResult reconcileOrphans(ReconciliationQuery query);
}

자동 reconciliation이 READY를 임의 추정해서는 안 된다. size, digest, expected content key, metadata version이 모두 맞을 때만 상태를 복원한다.


30. 관측성

30.1 Metric

Metric 주요 tag
upload count·duration protocol, storageType, resultCode, sizeBucket
download count·duration transferMode, rangeType, resultCode, sizeBucket
transfer bytes direction, storageType
active transfers direction, instance
interruption direction, reason
resumable append protocol, result
offset mismatch protocol, clientType
checksum failure algorithm, stage
verification queue verifier, verdict, ageBucket
temp·orphan bytes storagePool, ageBucket
storage usage pool, mountProfile
quota scopeType, result
cleanup type, result
delegation ratio sizeBucket
access denial operation, policyCode

실제 file ID, upload ID, filename, path, user ID를 metric label로 사용하지 않는다.

30.2 Trace

upload.create
upload.append
upload.finalize
verify.digest
verify.media-type
verify.malware
storage.publish
storage.stat
metadata.transition
download.authorize
download.resolve-range
download.open
download.delegate
cleanup.item
reconcile.file

30.3 Audit

다음 작업은 audit 대상이다.

  • overwrite
  • delete·force delete
  • admin reverify
  • orphan reconcile
  • quarantine 승인·거절
  • delegated download 발급
  • access denial

filename, path, signed token, content sample은 audit에 기록하지 않는다.


31. Spring Boot 설정

backend:
  fileserver:
    enabled: true
    storage:
      type: local
      root: /var/lib/backend/files
      publish-mode: atomic-move-preferred
      require-same-file-store: true
      fail-on-symlink: true
      buffer-size: 128KiB
    upload:
      profile: standard
      max-file-size: 100MiB
      max-request-size: 116MiB
      max-parts: 16
      require-content-length: false
      incomplete-ttl: 24h
      idle-timeout: 45s
      instance-concurrency: 16
      scope-concurrency: 4
    download:
      single-range-only: true
      max-ranges: 8
      direct-concurrency: 64
      private-cache-control: "private, no-store"
      content-digest: false
    nginx:
      enabled: false
      delegate-threshold: 16MiB
      internal-prefix: /__files/
    verification:
      async: true
      checksum: sha-256
      require-media-type-verdict: true
      scanner-required: false
      max-attempts: 5
    quota:
      enabled: true
      reservation-ttl: 24h
    cleanup:
      batch-size: 100
      max-bytes-per-run: 10GiB
      fixed-delay: 5m
    tus:
      enabled: false
      checksum: true
      expiration: true
      termination: true
    httpbis-draft12:
      enabled: false
    mvc:
      executor:
        core-threads: 8
        max-threads: 32
        queue-capacity: 64
    webflux:
      io-workers: 16
      max-in-flight-buffers: 8

31.1 Startup validation

다음 조건은 startup 실패다.

  • storage root가 webroot 또는 application config 아래임
  • staging과 content가 다른 FileStore
  • symlink no-follow probe 실패
  • ATOMIC_MOVE_REQUIRED인데 probe 실패
  • metadata store 없이 multi-instance mode 활성화
  • no-op authorization policy가 production profile에서 활성화
  • scanner-required인데 verifier bean 없음
  • Nginx delegation을 켰는데 token service 또는 mapping 검증 없음

32. 관리자 API

Method·Path 기능 통제
GET /internal/fileserver/storage-health capacity와 probe 결과 admin network·role
GET /internal/fileserver/capabilities runtime capability path 비노출
GET /internal/fileserver/orphans bounded orphan 조회 pagination·rate limit
POST /internal/fileserver/orphans:reconcile dry-run·apply audit
POST /internal/fileserver/files/{id}:reverify 재검사 audit
POST /internal/fileserver/files/{id}:force-delete 강제 삭제 사유·이중 권한
GET /internal/fileserver/uploads/incomplete 미완료 조회 filename 마스킹
POST /internal/fileserver/uploads:cleanup cleanup lease·version 확인
GET /internal/fileserver/verification-queue 검사 지연 bounded result

관리자 API는 public starter에서 자동 노출하지 않고 별도 fileserver-admin 모듈과 management port에서만 활성화한다.


33. 테스트 전략

33.1 계약 테스트

  • Content Store blocking·async contract
  • Metadata optimistic transition contract
  • state machine illegal transition
  • upload offset and lease
  • checksum and size
  • GET·HEAD header parity
  • 200/206/304/412/416
  • Range first, middle, suffix, end, empty
  • If-Range, If-Match, If-None-Match
  • multipart single·batch
  • tus create·HEAD·PATCH·checksum·expiry·termination

33.2 보안 테스트

  • ../, percent-encoded separator, absolute path, Windows drive path
  • parent symlink replacement race
  • hard link discovery
  • filename CRLF·bidi·reserved name
  • extension·Content-Type·signature mismatch
  • scriptable content inline 차단
  • scanner timeout·malware verdict
  • internal Nginx path direct access
  • unauthorized download and existence hiding
  • multi Range bomb

33.3 장애 테스트

  • write 전·중·후 process kill
  • close 후 publish 전 kill
  • physical publish 후 DB commit 전 kill
  • disk full and quota exhaustion
  • permission denied
  • slow upload·download
  • network disconnect
  • WebFlux cancellation
  • MVC executor saturation
  • NFS disconnect·server restart·rename ambiguity
  • PVC remount·Pod reschedule
  • scanner unavailable

33.4 성능 테스트

  • 100 MiB와 5 GiB streaming
  • concurrent upload/download
  • direct vs Nginx throughput
  • p50, p95, p99, max latency
  • heap, direct memory, allocation, GC
  • temp disk and scanner throughput
  • Range overhead
  • cleanup throughput

33.5 인증 매트릭스

프로파일 빈도 Gate
Linux ext4 local PR 필수
Linux XFS local nightly release 필수
PVC RWO 주 CSI release 필수
PVC RWX release 지원 선언 시 필수
NFSv4.1 nightly 제한 지원 필수
NFS fault injection RC 제한 지원 필수
Windows NTFS nightly 초기 non-blocking
Nginx stable release nginx 모듈 필수
MVC Tomcat PR 필수
MVC Jetty release 지원 선언 시 필수
WebFlux Reactor Netty PR 필수
Spring 6.2 latest release 필수
Spring 7.0 latest release 필수

34. CI 품질 Gate

모든 pull request:

unit test
core contract test
local ext4 integration
MVC Tomcat HTTP contract
WebFlux Reactor Netty contract
architecture test
path traversal·symlink security suite
bounded-memory regression

Nightly:

XFS
NFSv4.1
Windows NTFS
large-file performance
slow client
process-kill matrix
scanner failure

Release:

Spring 6.2 / 7.0 matrix
PVC certification
Nginx contract
multi-instance lease
fault injection
support-matrix diff
sensitive-log scan

35. 릴리스 단계

Milestone A — Core Alpha

  • core model·state machine
  • JPA metadata
  • local staging·append·publish
  • raw upload
  • full download
  • checksum

Milestone B — HTTP Beta

  • multipart
  • GET·HEAD·single Range
  • conditional request
  • MVC·WebFlux
  • security verifier
  • cleanup

Milestone C — Distributed RC

  • multi-instance lease
  • Nginx delegation
  • PVC RWO certification
  • admin plane
  • chaos·performance gate

Milestone D — Extended Release

  • tus 1.0
  • NFS limited profile
  • PVC RWX certification
  • multi Range Beta
  • HTTPbis draft-12 Experimental

36. 구현자가 임의로 변경하면 안 되는 결정

  • 공개 API에 Path와 physical filename을 노출하지 않는다.
  • Content Store의 최소 Port를 filesystem 명령 mirror로 바꾸지 않는다.
  • READY 이전 다운로드를 허용하지 않는다.
  • metadata DB를 우회해 physical file 존재만으로 READY를 추정하지 않는다.
  • create-only 기본값을 unconditional overwrite로 바꾸지 않는다.
  • client Content-Type과 filename을 신뢰하지 않는다.
  • WebFlux event loop에서 blocking I/O를 실행하지 않는다.
  • MVC streaming에 unbounded executor를 사용하지 않는다.
  • NFS lock을 단독 correctness mechanism으로 사용하지 않는다.
  • upload timeout 후 blind retry를 수행하지 않는다.
  • arbitrary path, symlink, recursive delete를 escape hatch로 열지 않는다.
  • IETF draft 모듈을 Stable API와 섞지 않는다.
  • metric label에 fileId·filename·path를 넣지 않는다.

37. 완료 정의

프로젝트 완료는 다음 산출물이 코드와 CI에 연결됐을 때 선언한다.

산출물 완료 기준
지원 매트릭스 runtime·filesystem·protocol별 자동 test job 연결
상태 머신 모든 허용·금지 전이 contract test
Content Store blocking·async contract와 local adapter 인증
Metadata Store optimistic version·lease·recovery test
HTTP 계약 MVC·WebFlux·Nginx mode parity
보안 traversal·symlink·MIME·권한 공격 suite
장애 crash point·disk full·network fault 후 invariant 유지
성능 최대 파일에서도 bounded heap·direct memory
운영 metric, trace, audit, cleanup, reconciliation, runbook
재개 업로드 tus 1.0 contract suite
제한 지원 NFS·PVC RWX·Windows 수준이 runtime capability와 문서에 표시

38. 구현 순서

1. 모듈·품질 기반
2. core ID·상태·오류
3. Content Store와 Metadata Store 계약
4. JPA metadata
5. local path·staging·capability probe
6. append·checksum·quota
7. publish·state transition·reconciliation
8. upload application
9. HTTP Range·conditional core
10. MVC
11. WebFlux
12. verification·authorization
13. delete·cleanup·admin
14. Nginx delegation
15. multi-instance·PVC
16. tus 1.0
17. NFS limited certification
18. HTTPbis draft Experimental
19. chaos·performance·release matrix