Files
clean-architecture-backend-…/docs/superpowers/specs/2026-07-26-fileserver-production-capability-design.md
T

92 KiB
Raw Blame History

Fileserver Production Capability Deep Design

  • 작성일: 2026-07-26
  • 상태: 상세 설계 완료, Phase 01 및 Phase 2 일부 local R1 구현, R2 이상 미구현
  • 독립 아키텍처 재리뷰: blocker/high 0건
  • 기준: Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 Clean Architecture
  • 대상 leaf: adapter-outbound-fileserver
  • 구현 추적: 이 문서의 목표 전체가 아니라 framework-free port, local staged CSV, single-node operation journal/recovery까지만 적용되었다.
  • 상위 문서: Production Capability Platform Design

0. 구현 상태

2026-07-28 기준 구현된 범위:

  • application-coreFilePublicationPort, typed schema/cell/row, producer/sink, opaque receipt;
  • 행 단위 UTF-8 CSV encoding과 row/byte/cell 제한;
  • spreadsheet formula 완화 정책과 SHA-256/count 계산;
  • private staging, exclusive create, file/directory force, no-replace hard-link publish;
  • 실패 시 staging 정리와 PUBLISH_INDETERMINATE 오류 분류;
  • opt-in typed settings와 local R1 bean composition;
  • canonical request fingerprint와 private WRITING/SEALED/PUBLISHED operation journal;
  • forced temp record + atomic replace와 single-node terminal receipt restoration;
  • sealed staging/final의 size·SHA-256 검증 후 producer 재실행 없는 local reconciliation.
  • operation-scoped JVM/OS file lock과 hard-link-only publication protocol;
  • overwrite-capable legacy port의 별도 opt-in/root 및 canonical overlap 차단;
  • 안전한 commit primitive가 없을 때 copy-to-final로 downgrade하지 않는 fail-closed 동작.

아직 구현되지 않은 범위:

  • Phase 2의 cross-node fencing, reference/private-manifest index, exhaustive crash/symlink-race qualification;
  • 운영 cleanup/quota/retention과 effective capability probe인 Phase 3;
  • SFTP provider인 Phase 4;
  • NFS/HA/bootstrap evidence인 Phase 5;
  • optional delete/read/scan operation인 Phase 6.

따라서 현재 journal은 single-node local recovery seam이며 Fileserver R2 완료 증거가 아니다. 기존 FileExportPort도 호환성을 위해 남아 있으며, 전체 행 materialization과 absolute path receipt를 사용하는 legacy 경로다.

1. 설계 판정

현재 Fileserver 구현은 운영 파일서버가 아니라 다음 한 경로만 제공하는 R1 이하의 로컬 CSV 예제다.

List<List<String>>
  -> 전체 StringBuilder
  -> 전체 String
  -> 전체 byte[]
  -> final path 직접 truncate/write
  -> absolute server path 반환

목표는 범용 파일시스템 CRUD API가 아니다. 목표는 다음 capability다.

계층형 경로, 외부 파일명, drop-zone, rename 또는 ready-marker 완료 계약이 필요한 생성 파일을 local/mounted filesystem 또는 SFTP에 안전하게 publish하고, 그 결과를 opaque reference로 추적·검증·복구하는 기능

선택한 핵심 구조는 다음과 같다.

  1. Application은 목적지를 path/host가 아닌 FileDestinationId로 선택한다.
  2. 안정적인 FilePublishOperationId가 retry와 reconciliation의 기준이다.
  3. 행 데이터는 동기식 row-producer callback으로 한 번만 흘려보낸다.
  4. Adapter가 staging, encoding, checksum, close, publish, abort를 모두 소유한다.
  5. Provider는 설정값이 아니라 실제 probe 결과인 effective capability를 보고한다.
  6. 요구한 atomicity/durability보다 provider 보장이 약하면 자동 downgrade하지 않는다.
  7. publish 응답 유실은 실패가 아니라 INDETERMINATE로 모델링하고 먼저 reconcile한다.
  8. 기본 파일명 정책은 immutable/versioned이며 unconditional overwrite와 append는 금지한다.
  9. File Server와 business database 사이의 원자적 commit은 주장하지 않는다.
  10. 사용하지 않는 provider는 connection, scheduler, scan, directory 생성 같은 side effect를 일으키지 않는다.

2. 기존 통합 설계에서 다룬 범위와 이번 심화 범위

상위 설계서는 Fileserver에 대해 다음 운영 기준만 정의했다.

  • streaming writer;
  • temporary file;
  • restrictive permissions;
  • fsync와 atomic rename;
  • symlink defense;
  • quota와 retention;
  • CSV formula defense;
  • opaque receipt;
  • NFS/SFTP semantics를 provider capability로 표시.

이 기준은 방향은 맞지만 다음 구현 결정이 없었다.

  • streaming port의 정확한 signature와 lifecycle;
  • producer exception과 storage exception의 분리;
  • operation ID와 request-intent/operation fingerprint;
  • publish state machine과 unknown outcome;
  • create-only, replace, versioned naming의 정확한 보장;
  • atomic visibility와 crash durability의 분리;
  • local, NFS, SFTP의 effective capability matrix;
  • target-side staging과 cross-filesystem 처리;
  • mount 누락 시 local fallback 방지;
  • SFTP extension negotiation과 reconnect 후 reconciliation;
  • manifest/marker commit protocol;
  • multi-node cleanup claim;
  • quota reservation의 한계;
  • CSV dialect, typed cell, null, control character, formula 정책;
  • error taxonomy, health, metric, test 및 CI task;
  • 기존 API에서 새 API로의 migration.

이번 문서는 위 항목을 구현 계획을 작성할 수 있는 수준까지 확정한다.

3. 현재 코드의 증거 기반 진단

영역 현재 구현 운영상 의미
Port exportCsv(String, List<String>, List<List<String>>) 호출자가 전체 행을 먼저 메모리에 적재해야 한다.
Encoding StringBuilder -> String -> byte[] 파일 크기에 비례한 heap 복제가 추가된다.
Write Files.write(finalTarget, bytes) 기존 파일을 truncate하고 partial final을 노출할 수 있다.
Naming caller-controlled fileName namespace, operation ID, version, precondition이 없다.
Receipt ExportedFile.path absolute path SFTP와 cluster에서 의미가 없고 서버 topology가 유출된다.
Path safety normalize().startsWith(baseDir) lexical traversal만 막고 symlink/TOCTOU를 막지 못한다.
CSV UTF-8, LF, 최소 quoting RFC 4180 CRLF, row width, formula, control/size 정책이 없다.
Collision unconditional overwrite concurrent writer의 결과가 정의되지 않는다.
Durability 없음 file force, directory sync, remote fsync를 구분하지 않는다.
Error 모든 IOExceptionINTERNAL_ERROR retry, conflict, capacity, unknown outcome을 구분할 수 없다.
Settings enabled, 상대 baseDirectory provider, guarantee, limits, timeout, mount identity가 없다.
Startup enabled이면 createDirectories NFS mount 누락을 로컬 디렉터리로 오인할 수 있다.
Composition leaf는 registry에만 등록 기본 app-bootstrap artifact에 fileserver가 없다.
Consumer 없음 sample을 포함해 실제 use case가 port를 호출하지 않는다.
Tests local unit 7개 concurrency, fault, symlink, memory, NFS, SFTP 증거가 없다.

주요 근거 파일:

  • src/application-core/src/main/java/dev/caskeleton/application/fileexport/FileExportPort.java
  • src/application-core/src/main/java/dev/caskeleton/application/fileexport/ExportedFile.java
  • src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilesystemCsvExportAdapter.java
  • src/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportProperties.java
  • src/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FilesystemCsvExportAdapterTest.java
  • src/config/architecture/modules.json
  • src/app-bootstrap/build.gradle

현재 7개 fileserver unit test와 leaf check는 성공한다. 이는 현재 문서화된 로컬 happy-path 계약이 동작한다는 증거일 뿐 production readiness 증거는 아니다.

4. 범위와 명시적 비범위

4.1 이번 R2 baseline에 포함

  • tabular data의 streaming CSV publication;
  • local persistent filesystem provider;
  • mounted/shared filesystem provider profile;
  • SFTP outbound publication provider;
  • staging, sealing, publish, abort;
  • SHA-256, byte/row count, versioned manifest;
  • create-unique 및 conditional replace;
  • operation-id 기반 idempotency와 reconciliation;
  • opaque inspection/reference;
  • managed namespace의 staging cleanup;
  • opt-in managed retention/delete;
  • typed settings, exact provider binding, startup safety;
  • health, metrics, traces, structured error;
  • real local/OpenSSH/NFS integration and failure tests.

4.2 안전하게 열어둘 optional operation

  • opaque reference 기반 metadata inspection;
  • opaque reference 기반 content transfer;
  • expected-version 기반 managed delete;
  • operation ID 기반 publish resolution;
  • marker/manifest verification.

이 operation은 구현할 수 있게 계약을 분리하지만 하나의 범용 FileServerPort CRUD로 합치지 않는다.

4.3 이번 범위에서 제외

  • HTTP download endpoint와 authorization: adapter:inbound:web 책임;
  • 사용자 multipart upload: inbound transport와 object-storage/quarantine workflow 책임;
  • presigned URL, multipart object upload, ETag/bucket lifecycle: object storage 책임;
  • domain별 export job entity, snapshot query, pagination: application/sample 책임;
  • DB write와 file publish의 distributed transaction;
  • inbound SFTP/NFS pickup으로 use case를 구동하는 기능;
  • directory watch/poll을 business event source로 사용하는 기능;
  • FTP/FTPS/SMB native client provider;
  • 제품별 성능 수치와 partner별 filename/dialect.

외부 파일이 들어와 use case를 구동하는 inbound pickup은 driving adapter다. 필요해지면 adapter:inbound:fileserver leaf를 registry migration으로 추가한다. 현재 outbound leaf에 listener/poller와 business handler를 넣지 않는다.

5. HARD invariants

다음 조건은 구현 편의를 위해 낮출 수 없다.

  1. application-core에 Spring, Path, File, InputStream, SFTP SDK, Micrometer 타입을 노출하지 않는다.
  2. Application request에는 host, port, credential, base directory, remote path가 없다.
  3. Physical final path는 adapter의 target binding에서만 계산한다.
  4. Caller 문자열을 Path.resolve의 자유 경로로 사용하지 않는다.
  5. Final name을 가진 파일에 직접 streaming write하지 않는다.
  6. Temp/staging은 publish target과 동일 filesystem/remote namespace에 존재한다.
  7. ATOMIC_MOVE 실패 시 non-atomic move/copy로 조용히 fallback하지 않는다.
  8. Atomic visibility와 crash durability를 같은 guarantee로 표현하지 않는다.
  9. 하나의 accepted publish attempt 안에서 producer는 최대 한 번만 실행한다. Process 또는 node 장애를 넘는 global at-most-once는 durable ownership evidence 없이는 주장하지 않는다.
  10. 같은 accepted attempt의 provider retry를 위해 application row producer를 다시 호출하지 않는다.
  11. INDETERMINATE 결과를 blind retry하지 않는다.
  12. APPEND는 R2 publication mode가 아니다.
  13. Default collision policy는 unconditional replace가 아니다.
  14. File name/path/content/checksum/user/tenant를 metric tag로 사용하지 않는다.
  15. Unknown file과 유효하지 않은 manifest를 reaper가 자동 삭제하지 않는다.
  16. Fileserver가 persistence, Redis lock, objectstorage adapter에 의존하지 않는다.
  17. local-dev provider는 prod profile에서 시작하지 않는다.
  18. SFTP unknown host key 허용, password literal, infinite timeout/pool은 금지한다.
  19. 현재 mount가 기대한 mount인지 증명하지 못하면 mounted provider는 ready가 아니다.
  20. Provider capability가 요구 guarantee를 만족하지 못하면 startup 또는 required readiness를 실패한다.

6. 대안 검토

A. 현재 port를 유지하고 BufferedWriter만 추가

Adapter의 추가 StringBuilder는 제거할 수 있지만 caller가 이미 List<List<String>> 전체를 materialize한다. Absolute path, overwrite, provider, unknown outcome 문제도 남는다. 선택하지 않는다.

B. 범용 FileServerPort에 open/read/write/list/move/delete를 모두 제공

Application이 path, directory, wildcard, file handle에 결합되고 raw filesystem facade가 된다. Redis의 raw command API를 core에 노출하지 않는 것과 같은 이유로 선택하지 않는다.

C. Mutable begin -> append -> commit/abort session을 application에 노출

페이지 단위 쓰기는 쉽지만 caller가 commit/abort/close를 누락할 수 있고 adapter resource lifecycle이 application으로 새어 나온다. SDK-like session이 되므로 기본 계약으로 선택하지 않는다.

D. 단일 publish 호출과 synchronous row-producer callback

선택한 방식이다.

  • Adapter가 staging부터 abort까지 전 lifecycle을 소유한다.
  • Sink의 동기 호출 자체가 backpressure boundary가 된다.
  • Producer는 페이지 조회를 선택할 수 있어 전체 materialization이 필요 없다.
  • Adapter가 producer를 한 번만 실행하는 것을 통제할 수 있다.
  • Checked IO와 provider 타입이 application으로 새지 않는다.

E. Provider별 Gradle leaf를 즉시 분리

fileserver-local, fileserver-mounted, fileserver-sftp는 dependency isolation이 가장 강하지만 지금은 exact-19 registry 변경과 설정/계약 중복 비용이 크다. 우선 기존 leaf 내부 provider package로 구현한다. SFTP SDK 보안/릴리스 lifecycle이 독립 배포를 요구할 때 ADR과 registry migration으로 분리한다.

7. 목표 아키텍처

flowchart LR
    USECASE[Application export use case] --> PORT[FilePublicationPort]
    PORT --> COORD[Publication coordinator]
    COORD --> VALIDATE[Schema / limits / formula policy]
    VALIDATE --> CSV[Streaming CSV encoder]
    CSV --> PIPE[Count + digest + optional transform]
    PIPE --> PROVIDER{Target provider}
    PROVIDER --> LOCAL[Local persistent filesystem]
    PROVIDER --> MOUNT[Mounted/shared filesystem]
    PROVIDER --> SFTP[SFTP remote endpoint]
    COORD --> MANIFEST[Manifest / marker / reconcile]
    BOOT[app-bootstrap] -. binds destination and verifies guarantees .-> COORD
    OBS[Health / metrics / traces] -. observes .-> COORD

Target package shape:

adapter:outbound:fileserver
  config/
  core/
    FilePublicationCoordinator
    FileProvider
    FileProviderDescriptor
    PublicationPlan
  format/csv/
    CsvEncoder
    CsvDialect
    FormulaPolicy
  publication/
    StagingArtifact
    ManifestCodec
    PublishReconciler
  provider/local/
  provider/mounted/
  provider/sftp/
  maintenance/
    StagingReaper
    RetentionExecutor
  observability/

8. 모듈과 계층 소유권

책임 소유 모듈
Export use case, destination intent, schema, row production application-core 또는 feature application
Framework-free publication/read/delete ports와 값 타입 application-core
Skeleton-wide file operational error와 provider descriptor 공통 값 shared-contract
CSV encoding, staging, manifest, local/mounted/SFTP provider adapter:outbound:fileserver
Provider selection, exact binding, prod safety, readiness composition app-bootstrap
Export job persistence, source snapshot, keyset query application + persistence adapter
HTTP response streaming adapter:inbound:web
Incoming file pickup/listener future inbound fileserver leaf
Mount, quota, backup, NFS export, remote account deployment/IaC/operations

Fileserver leaf의 허용 edge는 현재처럼 application-core, shared-contract만 유지한다. Sibling adapter edge를 추가하지 않는다.

9. Application 계약

9.1 Port 분리

public interface FilePublicationPort {
  FilePublishReceipt publish(
      FilePublishRequest request,
      TabularRowProducer producer);
}

public interface FilePublishResolutionPort {
  FilePublishResolution resolve(FilePublishOperationId operationId);
}

public interface PublishedFileInspectionPort {
  PublishedFileMetadata inspect(PublishedFileReference reference);
}

public interface PublishedFileTransferPort {
  void transfer(PublishedFileReference reference, FileChunkSink sink);
}

public interface ManagedFileDeletionPort {
  FileDeletionReceipt delete(
      PublishedFileReference reference,
      FileVersion expectedVersion);
}

public interface FileRegenerationPort {
  FilePublishReceipt regenerate(
      FileRegenerationRequest request,
      TabularRowProducer producer);
}

R2 구현 순서는 publication과 resolution이 먼저다. Inspection, transfer, delete는 필요 없는 애플리케이션에 bean과 runtime behavior를 만들지 않는다. Regeneration도 durable spool을 복구할 수 없는 topology에서만 opt-in한다.

금지되는 port:

open(Path)
list(String glob)
move(String from, String to)
delete(String rawPath)
getSftpSession()

9.2 Request

FilePublishRequest는 다음 값만 가진다.

필드 의미
operationId retry 전반에서 유지되는 안정적인 ID
destinationId 설정에 등록된 logical destination
logicalFileName display/naming input이며 path가 아님
sourceRevision snapshot 또는 source fingerprint
schema column 이름, type, null/formula 정책
formatProfileId 등록된 CSV profile
publishCondition create unique 또는 expected-version replace
retentionClassId 등록된 관리 정책
protectionProfileId 등록된 민감도/transform 정책

Request에 다음 값은 없다.

  • absolute/relative directory;
  • provider ID;
  • local/SFTP path;
  • delimiter나 charset 임의 값;
  • username, host, credential;
  • POSIX mode;
  • timeout과 pool size.

Provider와 세부 정책은 destinationId의 bootstrap binding이 결정한다.

9.3 Operation ID와 fingerprint

FilePublishOperationId는 필수다. Caller intent와 runtime policy를 분리해 canonical하게 계산한다.

requestIntentDigest = SHA-256(
  destinationId
  + logicalFileName normalized form
  + sourceRevision
  + schema version/digest
  + formatProfileId
  + publish condition
  + protection profile ID
  + retention class ID
)

operationFingerprint = SHA-256(
  requestIntentDigest
  + effectivePolicyDigest
)

규칙:

  • 동일 ID + 동일 intent/frozen policy + PUBLISHED: producer를 실행하지 않고 기존 receipt 반환;
  • 동일 ID + 다른 request intent: FILESERVER_OPERATION_MISMATCH;
  • 동일 ID + active STAGING: in-progress 또는 bounded wait;
  • 동일 ID + INDETERMINATE: reconcile만 수행;
  • 새 ID: 새 publication.

effectivePolicyDigest는 ID 문자열이 아니라 adapter가 first reservation 때 freeze한 canonical policy snapshot의 digest다.

destination binding revision
naming policy revision
format profile revision + canonical options
publication/guarantee requirements
protection transform revision
payload encryption/signing key version (사용 시)
retention policy revision
ownership/control-plane mode

SFTP login private-key version처럼 payload 의미를 바꾸지 않는 transport credential은 operation fingerprint에 넣지 않되 audit/session lifecycle에는 기록한다. 반대로 payload encryption key version은 최종 bytes와 복구에 영향을 주므로 포함한다. Raw key material은 snapshot에 없다.

기존 operation journal이 있으면 incoming requestIntentDigest를 먼저 비교한 뒤 현재 배포의 same-name profile을 다시 해석하지 않고 journal에 freeze된 policy snapshot으로 operationFingerprint를 복원한다. 해당 revision이나 key version을 더 이상 사용할 수 없으면 현재 정책으로 조용히 바꾸지 않고 FILESERVER_POLICY_REVISION_UNAVAILABLE로 중단한다. 새 operation만 현재 effective policy를 freeze한다.

accepted attempt는 request/fingerprint 검증, capacity 획득, ownership 판정까지 성공해 새 staging generation이 할당된 한 번의 실행이다. Pre-commit terminal failure 뒤 source regeneration은 기존 operation을 다시 여는 방식이 아니라 별도 regeneration 계약으로 새 operation ID를 만든다.

Fileserver의 operation protocol은 business side effect exactly-once를 의미하지 않는다.

9.4 Streaming row producer

@FunctionalInterface
public interface TabularRowProducer {
  void produce(TabularRowSink sink);
}

public interface TabularRowSink {
  void write(TabularRow row);
  void checkpoint();
}

계약:

  • 새 payload 생성이 필요한 하나의 accepted attempt에서 produce는 caller thread에서 동기적으로 최대 한 번 호출된다. 이미 완료된 operation은 0회다.
  • Sink는 thread-safe API가 아니며 producer가 다른 thread로 넘기거나 보관할 수 없다.
  • write가 실제 bounded encoding/write를 진행하므로 자연스러운 backpressure가 생긴다.
  • Deadline, cancellation, row/byte limit은 sink가 매 호출 전후에 검사한다. Producer는 page 조회 전후처럼 row를 쓰지 않는 구간에도 checkpoint()를 호출한다.
  • Sink failure는 runtime application exception으로 producer stack을 중단시킨다.
  • Producer가 던진 domain/application exception은 fileserver dependency error로 바꾸지 않는다.
  • Adapter는 producer failure 시 staging만 abort하고 원래 exception을 다시 던진다.
  • Cleanup failure는 원래 exception의 suppressed cause와 metric으로 남긴다.
  • Adapter 내부 retry 때문에 producer를 다시 실행하지 않는다.

대용량 export use case는 producer 내부에서 stable cursor/keyset으로 source를 page 단위 조회한다. File IO 전체를 감싸는 장시간 DB transaction은 허용하지 않는다.

이 callback은 arbitrary blocking code를 강제로 중단할 수 있는 hard-timeout 경계가 아니다. checkpoint()와 thread interruption은 cooperative cancellation이며, JDBC/HTTP page source에는 각 source port의 statement/request timeout도 별도로 설정한다. Producer가 interruption을 무시하거나 외부 호출에서 영구 block될 수 있는 topology는 process/job 격리를 사용한다.

9.5 Typed cell과 schema

이미 문자열화된 임의 값만 받으면 locale, timezone, null 의미가 caller마다 달라진다. 따라서 다음 작은 framework-free cell set을 제공한다.

TextCell
IntegerCell
DecimalCell
BooleanCell
DateCell
InstantCell
NullCell

임의 객체와 reflection serialization은 없다.

ExportSchema는 다음을 고정한다.

  • schema ID와 version;
  • ordered columns;
  • column name;
  • expected cell type;
  • nullable;
  • formula policy;
  • max cell bytes override;
  • canonical formatter 또는 caller-supplied text 허용 여부.

InstantCell 기본 표현은 UTC ISO-8601, number는 locale-independent canonical form이다. Partner format이 다르면 named format profile을 등록한다.

9.6 Receipt

operationId
fileReference
destinationId
publishedFileName
fileVersion
formatProfileId
mediaType
charset
byteSize
dataRowCount
columnCount
sha256
publishedAt
publicationGuaranteeAchieved
durabilityGuaranteeAchieved
formulaMitigatedCount
manifestSchemaVersion
effectivePolicyRevision

PublishedFileReferenceFileVersion은 opaque value다. Receipt에는 absolute path, SFTP URI, host, username, credential, mount path가 없다.

9.7 Safe operation catalog

개발자에게 기본 제공하는 operation은 capability-oriented하다.

Operation 제공 목적 금지되는 raw 대체
publishTabular staged CSV publish final path direct write
resolvePublish unknown/in-progress reconciliation blind retry
inspectPublished opaque reference metadata stat(rawPath)
verifyPublished size/digest/marker 검사 caller checksum 구현
transferPublished bounded chunk transfer raw InputStream 반환
regenerateExplicit prior revision을 고정한 새 operation 생성 stale retry의 producer 재실행
deleteManaged version/ownership 조건부 삭제 Files.delete(path)
reapStaging 알려진 staging만 cleanup recursive base delete

각 operation은 atomicity scope, retry safety, state growth, provider requirement, failure result를 capability card에 기록한다.

9.8 Explicit regeneration

일반 publish retry는 REGENERATION_REQUIRED operation의 producer를 다시 실행하지 않는다. Application이 데이터 재조회와 재생성을 승인할 때만 별도 계약을 사용한다.

FileRegenerationRequest
  priorOperationId
  expectedPriorStateRevision
  expectedPriorAttemptId
  newRequest (full FilePublishRequest with a new operationId)
  regenerationDecisionId
  boundedReasonCode
  policyMode = REUSE_FROZEN_POLICY

규칙:

  1. Authorized application use case가 principal/tenant/business 승인을 먼저 수행한다. regenerationDecisionId는 그 결정을 audit에 연결하는 opaque ID이지 bearer credential이 아니다.
  2. Adapter는 prior operation을 먼저 reconcile한다. Final commit 가능성이 남은 INDETERMINATE에서는 regeneration을 거부한다.
  3. Prior state가 정확히 REGENERATION_REQUIRED이고 expected revision/attempt가 일치해야 한다.
  4. newRequest.operationId는 새 ID여야 하며 old journal을 다시 열지 않는다. 나머지 request intent와 schema를 canonicalize해 prior requestIntentDigest와 비교한다.
  5. Provider control plane의 고정 key supersessions/{priorOperationId}.json을 exclusive-create하고 record 안에 newRequest.operationId, expected revision/attempt, decision ID를 저장한다.
  6. 같은 prior operation에 이미 다른 supersession이 있으면 FILESERVER_REGENERATION_CONFLICT다.
  7. 새 operation은 원래 freeze된 policy/source revision/request intent를 재사용한다. 현재 정책으로 바꾸고 싶으면 regeneration이 아니라 별도 신규 publication이다.
  8. Prior sealed digest가 남아 있으면 regenerated payload digest가 정확히 일치해야 한다. Digest 전에 실패한 attempt는 application이 repeatable source revision을 증명할 때만 regeneration한다.
  9. Provider가 exclusive supersession record를 보장하지 못하면 external job coordination이 필수며, required topology는 evidence 없이는 시작하지 않는다.

상태는 REGENERATION_REQUIRED -> SUPERSEDED(newOperationId)로 닫고 새 operation이 RESERVING에서 시작한다. 새 attempt가 실패해도 또 다른 regeneration을 만들지 않고 이미 연결된 newOperationId를 resolve/retry한다. Supersession record가 journal state update보다 우선하며, journal update 중 crash해도 fixed-key record에서 SUPERSEDED를 복원한다.

10. Error와 outcome 모델

현재 모든 IO failure를 INTERNAL_ERROR로 묶는 방식을 제거한다.

권장 skeleton-wide code:

Error 의미 기본 retry
FILESERVER_DISABLED binding/provider 비활성 false
FILESERVER_INVALID_REQUEST schema/name/profile 위반 false
FILESERVER_OPERATION_MISMATCH operation ID fingerprint 충돌 false
FILESERVER_CONFLICT create/expected-version 충돌 false
FILESERVER_CAPACITY_EXCEEDED byte/row/disk/quota 제한 조건부
FILESERVER_PERMISSION_DENIED local/remote permission false
FILESERVER_UNAVAILABLE mount/session/provider unavailable true
FILESERVER_TIMEOUT acquire/connect/idle/overall timeout true
FILESERVER_GUARANTEE_UNAVAILABLE required capability 미지원 false
FILESERVER_POLICY_REVISION_UNAVAILABLE freeze된 policy/key revision 재생 불가 false
FILESERVER_INTEGRITY_FAILED size/checksum/manifest mismatch false
FILESERVER_PUBLISH_INDETERMINATE commit 여부 불명 blind retry 금지
FILESERVER_REGENERATION_REQUIRED durable sealed source가 없어 application 판단 필요 false
FILESERVER_REGENERATION_CONFLICT prior state/revision/supersession 충돌 false
FILESERVER_CANCELLED deadline/shutdown/user cancel 조건부
FILESERVER_CLEANUP_FAILED staging/retention cleanup 실패 true

retryable=true는 같은 operation ID로 reconcile 또는 안전한 단계 retry가 가능하다는 뜻이다. 새 operation ID로 payload를 다시 쓰라는 뜻이 아니다.

Outcome phase를 별도 값으로 둔다.

PRE_COMMIT_FAILED
PUBLISHED
INDETERMINATE
CONFLICT
ABORTED
QUARANTINED
REGENERATION_REQUIRED
SUPERSEDED

11. Publication 상태 머신

stateDiagram-v2
    [*] --> RESERVING
    RESERVING --> STAGING
    STAGING --> WRITING
    WRITING --> SEALED
    SEALED --> PUBLISHING
    PUBLISHING --> COMMITTED_UNCONFIRMED: publish ACK
    PUBLISHING --> INDETERMINATE: ACK lost
    COMMITTED_UNCONFIRMED --> CONFIRMING
    CONFIRMING --> PUBLISHED
    CONFIRMING --> INDETERMINATE: stat or journal unavailable
    CONFIRMING --> QUARANTINED: integrity mismatch

    RESERVING --> ABORTING
    STAGING --> ABORTING
    WRITING --> ABORTING
    SEALED --> ABORTING
    ABORTING --> ABORTED
    ABORTING --> ORPHANED

    PUBLISHING --> INDETERMINATE
    INDETERMINATE --> RECONCILING
    RECONCILING --> PUBLISHED
    RECONCILING --> ABORTED
    RECONCILING --> QUARANTINED
    RECONCILING --> REGENERATION_REQUIRED
    REGENERATION_REQUIRED --> SUPERSEDED

    PUBLISHED --> DELETE_PENDING
    DELETE_PENDING --> DELETED

Commit point:

  • ATOMIC_RENAME: final name으로의 atomic rename 성공 응답;
  • READY_MARKER: data와 manifest 검증 후 marker의 exclusive publish 성공;
  • SFTP POSIX_RENAME: negotiated extension rename 성공 응답;
  • 응답을 잃었다면 commit 여부를 단정하지 않고 INDETERMINATE.

Commit primitive가 성공해도 즉시 PUBLISHED가 아니다. 먼저 COMMITTED_UNCONFIRMED로 전환하고 final stat, digest/manifest/marker, terminal control record를 확인한다. 이 confirm이 실패하면 이미 생성됐을 수 있으므로 pre-commit failure로 되돌리지 않고 INDETERMINATE다. 내용 불일치가 증명되면 QUARANTINED다.

PUBLISHED는 선택 protocol이 요구하는 file size, digest, manifest, marker와 terminal receipt snapshot이 모두 확인됐을 때만 반환한다. 이후 HTTP 응답이나 caller DB 저장이 유실되더라도 같은 operation ID의 재호출은 producer를 재실행하지 않고 이 terminal record에서 receipt를 복원한다.

11.1 Durable control plane과 source of truth

Operation ID만으로 idempotency가 생기지 않는다. R2 destination은 payload namespace와 별도로 provider-owned private control plane을 가져야 한다.

.ca-fileserver/
  operations/{operation-id-prefix}/{operation-id}.json
  references/{file-id-prefix}/{file-id}.json
  supersessions/{prior-operation-id}.json
  staging/{operation-id}/...
  quarantine/...

Operation journal v1 최소 필드:

journalSchemaVersion
operationId
requestIntentDigest
operationFingerprint
policySnapshotSchemaVersion
effectivePolicyRevision
effectivePolicyDigest
effectivePolicySnapshot
formatPolicyDigest
protectionPolicyDigest
retentionPolicyDigest
payloadKeyVersion
destinationId
sourceRevision
schemaDigest
publishCondition
fileId
referenceRoute
generatedRelativeLocator
publishedFileName
state
stateRevision
attemptId
coordinationMode
ownerInstanceId
heartbeatAt
stageRelativeLocator
sealedByteSize
sealedSha256
manifestDigest
receiptSnapshot
createdAt
updatedAt
lastFailureCode
supersededByOperationId

generatedRelativeLocator와 staging locator는 adapter 내부 control data이며 receipt, metric, 일반 info log로 노출하지 않는다. Journal에는 raw row/cell, credential, absolute path, remote URI를 저장하지 않는다. effectivePolicySnapshot은 canonical non-secret options만 포함하고 key/credential은 logical ID와 version만 기록한다.

PublishedFileReference는 versioned opaque route token과 random fileId로 구성하고 실제 provider locator를 포함하지 않는다. Adapter는 route token으로 bounded destination을 찾고 references/{fileId}.json을 조회한다. Reference index에는 operation ID, version, internal relative locator, manifest digest만 저장한다. 따라서 inspect/transfer/delete가 directory scan이나 caller path에 의존하지 않는다.

fileId는 충분한 entropy의 CSPRNG 값이고 reference parser는 version, 길이, route allowlist를 검증한다. 그러나 opaque/unguessable reference는 authorization token이 아니다. 어느 principal이 inspect/transfer/delete할 수 있는지는 application use case와 inbound authorization이 검증하며, fileserver는 tenant/user 권한을 추론하지 않는다.

Control record update:

  1. 새 record를 private temp name으로 CREATE_NEW한다.
  2. canonical encoding과 digest를 검증한다.
  3. provider가 증명한 atomic replace 또는 marker protocol로 stateRevision을 전진시킨다.
  4. required durability 수준에 맞춰 record와 directory/remote file을 sync한다.
  5. 더 낮거나 중복된 revision은 무시하고 fingerprint가 다르면 conflict로 격리한다.

Atomic record replace는 torn/partial control file 노출을 막을 뿐 compare-and-set이나 fencing이 아니다. stateRevision만으로 stale writer를 막지 않는다. Record를 갱신할 writer의 single-owner evidence가 없으면 multi-node destination은 global coordination guarantee를 claim하지 않으며 required profile은 fail-fast한다.

Journal은 진행 상태와 lookup의 durable evidence지만 remote payload와 한 transaction이 아니다. Truth priority는 다음과 같다.

valid final payload + matching terminal manifest/marker
  > terminal reference/operation record
  > non-terminal journal
  > in-memory registry

서로 모순되면 자동 성공/삭제하지 않고 reconciliation 또는 quarantine으로 전환한다.

Provider별 control plane:

  • local/mounted: target의 private managed root 안에 두고 payload와 동일한 provider 보장으로 갱신한다.
  • SFTP: remote private control directory 또는 pre-provisioned persistent local/shared control volume 중 하나를 명시한다.
  • HANDOFF destination이 sidecar/control file을 허용하지 않으면 persistent local/shared control volume이 필수다.
  • Ephemeral pod disk만 있는 SFTP destination은 restart 후 receipt 복원과 global reconciliation을 보장할 수 없으므로 R2가 아니다.

Final manifest와 journal은 상호 복구에 필요한 fingerprint, file ID, manifest digest를 공유한다. Startup 전체 scan은 금지하고 direct operation/file ID lookup과 bounded background reconciliation만 수행한다.

12. 공통 staged publish protocol

12.1 Plan

  1. destination binding과 provider effective capability를 조회한다.
  2. request invariant, fingerprint, duplicate operation 상태를 확인한다.
  3. required guarantee와 provider descriptor를 비교한다.
  4. final name, staging name, marker/manifest plan을 생성한다.
  5. quota/concurrency slot과 deadline budget을 획득한다.

12.2 Stage

  1. target filesystem/remote namespace 내부 private staging에 .<operationId>.part를 exclusive-create한다.
  2. Temp와 final이 같은 filesystem/fsid/remote root인지 검증한다.
  3. 제한 권한을 creation 시점에 설정한다.
  4. CSV encoder를 bounded byte buffer 위에 구성한다.
  5. row마다 schema, column count, formula, control, size, deadline을 검사한다.
  6. SHA-256, byte count, row count를 streaming 중 계산한다.
  7. 선택된 compression/encryption transform도 streaming pipeline 안에서 처리한다.
typed cells
  -> CSV encoder
  -> optional compression
  -> optional encryption/signature
  -> digest/count/limit
  -> provider staging sink

Baseline transform은 NONE이다. GZIP, PGP encryption/signature는 named protection profile로 열어두되 실제 reference implementation과 key-rotation test가 생기기 전에는 R0로 표시한다.

12.3 Seal

  1. Encoder를 flush하고 malformed/unmappable character를 REPORT 정책으로 검사한다.
  2. Channel/remote handle을 close하기 전에 요구되는 file sync를 수행한다.
  3. Final byte size와 digest를 확정한다.
  4. Frozen policy와 final locator를 포함한 private manifest draft를 canonical JSON으로 만든다.
  5. Sealed digest와 manifest draft를 operation journal에 durable하게 기록한다.
  6. Provider가 지원하면 staging content를 다시 stat/read-back한다.
  7. Permission과 regular-file/no-link 조건을 재확인한다.

12.4 Publish

선택 가능한 protocol:

Protocol Commit point 사용 조건
UNIQUE_ATOMIC_CREATE staged inode -> unique final hard-link create same filesystem, hard-link support proven
ATOMIC_REPLACE temp -> existing final atomic replace provider-specific replace semantics proven
READY_MARKER verified marker exclusive publish consumer가 marker를 이해함
SFTP_POSIX_RENAME negotiated posix rename extension과 same remote filesystem
DIRECT_FINAL_WRITE close prod 금지

Required protocol이 불가능하면 실패한다. Copy+delete, remove+rename 같은 fallback으로 guarantee를 낮추지 않는다. Publish primitive의 성공 응답은 COMMITTED_UNCONFIRMED이며 아직 caller에게 receipt를 반환하지 않는다.

12.5 Confirm

  1. Final stat의 type, size, version을 확인한다.
  2. 가능한 provider는 digest를 read-back 검증한다.
  3. Marker/manifest가 final artifact를 정확히 가리키는지 확인한다.
  4. Reference index와 achieved guarantee를 포함한 receipt snapshot을 만든다.
  5. Operation journal을 terminal PUBLISHED로 durable하게 전진시킨다.
  6. quota slot을 반환하고 receipt를 반환한다.

12.6 Failure

  • Publish 이전: staging abort/delete, 실패하면 orphan 등록;
  • Publish 요청 전송 후 응답 유실: INDETERMINATE;
  • Publish ACK 뒤 final stat/control record update 실패: INDETERMINATE;
  • Final exists + expected digest match: PUBLISHED로 reconcile;
  • Final exists + digest/fingerprint mismatch: conflict/quarantine;
  • Marker exists + data 없음: integrity incident;
  • Data exists + marker 없음: marker protocol에서는 unpublished residue;
  • Cleanup은 원래 source exception을 덮지 않는다.

12.7 Artifact publication ordering

다음 artifact를 구분한다.

D-stage   staged payload
D-final   consumer-visible payload
J-sealed  durable non-terminal operation journal + manifest draft
M-private provider control-plane terminal manifest
M-public  optional consumer-facing sidecar manifest
K-ready   consumer-aware ready marker
R-ref     opaque reference index
J-final   terminal operation journal + receipt snapshot

모든 protocol에서 J-sealed가 publish primitive보다 먼저 durable해야 한다. 그래야 data commit 후 process가 죽어도 producer를 다시 실행하지 않고 final locator, expected size/digest, fingerprint, frozen policy로 복구할 수 있다.

Protocol 순서 Consumer commit point
UNIQUE_ATOMIC_CREATE D-stage sync → J-sealed → data atomic hard-link create → final confirm → M-private → R-ref → J-final data link
ATOMIC_REPLACE D-stage sync + expected version → J-sealed → proven atomic replace → final confirm → M-private → R-ref → J-final data replace
SFTP_POSIX_RENAME local spool seal/sync → J-sealed → remote part upload/(remote fsync) → POSIX rename → remote confirm → M-private → R-ref → J-final POSIX rename
READY_MARKER D-stage + M-public stage/sync → J-sealed → versioned data/manifest publish → verify → K-ready exclusive publish → confirm → M-private → R-ref → J-final ready marker

M-private, R-ref, J-final은 각각 temp+verified atomic replace/marker를 사용하며 마지막 J-final이 durable하기 전에는 port가 receipt를 반환하지 않는다. 이 세 record가 provider와 원자 transaction을 이루는 것은 아니다. 각 write는 idempotent하고 operation ID, state revision, manifest digest로 재구성 가능해야 한다.

Single-file atomic rename protocol에서 M-public은 commit 구성요소가 될 수 없다. Data와 public manifest를 하나의 consumer contract로 원자 공개해야 하면 READY_MARKER 또는 provider가 실패 시험으로 증명한 atomic directory/bundle publish를 선택한다. Data rename 뒤 sidecar를 추가하면서 “둘이 atomic”이라고 주장하지 않는다.

Crash 판정:

관찰 상태 복구
D-stage만 있고 J-sealed 없음 never committed; bounded abort/reap
J-sealed + D-stage, final/marker 없음 sealed payload로 publish resume 또는 abort; producer 0회
D-final matches J-sealed, M-private/R-ref/J-final 일부 없음 COMMITTED_UNCONFIRMED; missing control record 재구성
D-final digest가 J-sealed와 다름 conflict/quarantine; overwrite 금지
K-ready 존재 + matching data/public manifest committed; private control records 재구성
data/public manifest 존재 + K-ready 없음 marker protocol에서 unpublished residue
R-ref 존재 + J-final 없음 final/manifest 확인 후 J-final 복구
J-final 존재 + final/marker 없음 integrity incident; 성공으로 반환 금지
publish request ACK 유실 INDETERMINATE; 위 evidence로 reconcile

Public manifest가 필요 없는 single-file consumer도 mandatory private manifest/control record는 유지한다. HANDOFF partner root가 private sidecar를 허용하지 않으면 별도 persistent control volume에 둔다.

13. Guarantee 모델

하나의 productionReady=true boolean으로 provider를 표현하지 않는다.

13.1 Descriptor

FileServerProviderDescriptor는 configured claim이 아니라 effective claim이다.

값 예시
Provider LOCAL_POSIX, SHARED_POSIX, SFTP
Visibility scope NODE, CLUSTER, REMOTE_ENDPOINT
Streaming write/read/range/resume
Publish primitive atomic rename, hardlink publish, marker, remote rename
Atomicity atomic unique, no-replace, replace
Durability file sync, directory sync, remote fsync, NFS stable commit
Consistency local immediate, close-to-open, remote-server
Concurrency none, advisory, leased
Fencing supported/unsupported
Security secure directory, no-follow, server chroot
Permissions POSIX/ACL/remote chmod
Capacity usable-space observation, native quota
Recovery stage list/delete, reconcile, read-back digest
Control plane durable operation index, reference lookup, atomic revision
Coordination process-local, provider reservation, external required
Limits file/chunk/concurrency/queue/in-flight/request handles/timeouts

각 항목은 다음 상태와 증거를 함께 가진다.

SupportStatus:
  SUPPORTED
  UNSUPPORTED
  UNVERIFIABLE

Evidence:
  SPEC
  NEGOTIATED_EXTENSION
  ACTIVE_PROBE
  OPERATOR_ATTESTED
  FAILURE_TESTED

API가 존재한다는 것과 보장이 검증됐다는 것은 다르다.

13.2 분리해야 하는 보장

  • Atomic visibility: reader가 partial final name을 보지 않는가?
  • Crash durability: host/server crash 뒤 data와 name이 남는가?
  • Immediate cross-node visibility: 다른 node가 즉시 관찰하는가?
  • Create-only atomicity: 같은 name 경쟁에서 정확히 한 writer만 성공하는가?
  • Replace atomicity: old 또는 new만 보이고 중간 상태가 없는가?
  • Outcome certainty: timeout 뒤 commit 여부를 알아낼 수 있는가?
  • Integrity: provider에 저장된 bytes가 digest와 일치하는가?

한 축의 성공을 다른 축의 증거로 사용하지 않는다.

14. Local persistent filesystem provider

14.1 용도

  • single-node 또는 node-attached persistent volume;
  • application이 소유하는 private directory;
  • generated/versioned file publication;
  • 동일 node에서 소비하거나 별도 delivery가 있는 경우.

Container ephemeral directory를 production persistent filesystem으로 분류하지 않는다.

14.2 Startup 조건

  • prod root는 absolute path;
  • autoCreate=false;
  • directory가 미리 존재;
  • root와 ancestor가 symlink가 아님;
  • expected owner/group/mode;
  • world/group writable 정책 위반 없음;
  • expected FileStore/device/mount sentinel 일치;
  • staging과 final directory가 동일 FileStore;
  • minimum usable-space watermark;
  • required SecureDirectoryStream/atomic move capability probe.

Mount가 빠졌을 때 underlying local directory를 자동 생성해 성공하면 안 된다.

14.3 Write와 durability

  • temp file CREATE_NEW;
  • creation attribute로 기본 0600;
  • publish 직전 target policy에 맞춰 0640 등 설정;
  • FileChannel.force(true)로 file data/metadata sync;
  • same-filesystem atomic move;
  • required profile에서 directory sync 수행.

Java FileChannel.force는 local storage device에만 강한 저장장치 기록 보장을 주고 non-local device에는 보장하지 않는다. Directory fsync는 Java portability가 낮으므로:

  • portable JDK provider는 FILE_SYNC까지만 claim;
  • Linux-specific tested implementation만 FILE_AND_DIRECTORY_SYNC claim;
  • directory sync가 요구되는데 구현이 없으면 fail-fast;
  • site replication과 backup은 별도 operation guarantee.

14.4 Path security

Strict mode:

  • open directory-relative operation;
  • SecureDirectoryStream 지원 시 이를 사용;
  • 모든 target/staging operation은 relative single-segment name;
  • NOFOLLOW_LINKS attribute/stat;
  • final과 temp가 regular file인지 확인;
  • untrusted user가 root에 entry를 만들 수 없도록 permission boundary.

toRealPath 선검사 후 일반 open만 수행하는 것은 check/open 사이 race를 제거하지 못한다. Provider가 secure relative operation을 지원하지 않고 root에 untrusted writer가 있으면 R2 strict mode를 claim할 수 없다.

14.5 Collision

Portable Java ATOMIC_MOVE는 target이 이미 존재할 때 replace/fail이 implementation-specific다. 따라서 기본은 server-generated unique/versioned final name이다.

  • CREATE_UNIQUE: UUID/ULID 기반 final name, collision 시 hard fail;
  • CREATE_IF_ABSENT stable name: native no-replace/hardlink primitive가 증명된 provider만;
  • REPLACE_IF_VERSION: expected version 확인 + provider-specific atomic replace evidence;
  • unconditional replace: legacy profile만;
  • append: 금지.

15. Mounted/shared filesystem provider

Mounted provider는 local provider class의 별칭이 아니다. 동일 JDK API를 사용하더라도 guarantee와 운영 검증이 다르다.

15.1 공통 조건

  • mount는 IaC가 pre-provision;
  • application auto-mount/auto-create 금지;
  • expected mount sentinel과 FileStore identity;
  • stage와 final은 target mount 내부;
  • bounded concurrency, queue, in-flight bytes;
  • blocking/hung IO timeout과 shutdown 정책;
  • multi-client integration evidence.

Virtual thread는 blocked platform-thread 비용을 줄일 수 있지만 filesystem 또는 server 부하, queue, byte pressure를 제한하지 않는다. Semaphore와 byte budget은 별도다.

15.2 NFS semantics

NFSv4 rename은 client 관점에서 atomic이며 source/target directory가 같은 server filesystem 이어야 한다. 이것이 의미하는 것은 partial final name을 피할 수 있다는 것이지 다음을 의미하지 않는다.

  • 즉시 cluster-wide directory visibility;
  • strong cache coherence;
  • fenced lock;
  • application-level exactly-once;
  • Java force가 local disk와 같은 durability를 제공함.

NFS profile 규칙:

  • immutable/versioned file 우선;
  • reader는 close/reopen과 marker/manifest contract 사용;
  • directory polling을 authoritative event source로 사용하지 않음;
  • final receipt나 DB/message가 discoverability source;
  • FileLock/NFS lease를 correctness의 단독 근거로 사용하지 않음;
  • 동일 final name multi-writer와 append 금지;
  • server sync export/backend stable storage는 operator attestation과 failure test 필요;
  • async export는 strong durability profile에서 거부.

15.3 NFS effective guarantee

Guarantee 기본 판정
Streaming write SUPPORTED
Same-fsid rename visibility SPEC + ACTIVE_PROBE
Immediate other-node discovery UNVERIFIABLE
File sync from Java UNVERIFIABLE
Server stable commit OPERATOR_ATTESTED + FAILURE_TESTED 필요
File lock fencing UNSUPPORTED
Multi-node immutable key SUPPORTED
Stable-name multi-writer replace external coordination 필요

15.4 Mount loss

다음 상태를 구분한다.

  • mount unavailable;
  • stale file handle;
  • mount identity changed;
  • underlying local mountpoint visible;
  • read-only remount;
  • free space/inode exhaustion;
  • server reboot/cache delay.

Sentinel mismatch나 mount identity change는 새 publication을 fail-closed하고 readiness를 내린다. 기존 staging을 자동 삭제하지 않는다.

16. SFTP provider

16.1 구현 선택

초기 구현은 fileserver leaf 안에서 Spring Integration SFTP의 programmatic API를 사용한다.

  • DefaultSftpSessionFactory;
  • bounded CachingSessionFactory;
  • RemoteFileTemplate.execute 또는 executeWithClient;
  • underlying Apache MINA SftpClient로 extension negotiation.

Message channel, SpEL path expression, outbound adapter를 application API로 노출하지 않는다. Boot BOM이 관리하는 compatible spring-integration-sftp version을 사용하며 SDK type은 leaf 밖으로 나가지 않는다.

16.2 Security

  • host key verification 필수;
  • known-hosts 또는 pinned fingerprint/host CA;
  • allowUnknownKeys=false;
  • TOFU와 changed-key 자동 수락 금지;
  • private key/agent 또는 secret-source credential;
  • password/private-key literal을 YAML/log에 저장하지 않음;
  • key rotation은 old/new dual trust window와 audit;
  • modern cipher/KEX/MAC allowlist;
  • remote account는 chroot 또는 restricted root;
  • root-owned/non-group-writable chroot hierarchy;
  • writable child만 service account에 허용;
  • remote path는 destination binding의 fixed relative segments만 사용.

16.2.1 Secret material resolution과 rotation

YAML의 private-key-secret-refknown-hosts-secret-ref는 문자열 치환용 secret 값이 아니라 등록된 logical reference다. 다음 adapter-private bootstrap SPI를 fileserver leaf가 소유한다.

FileServerSecretMaterialProvider
  acquire(SecretReference) -> SecretMaterialLease

SecretMaterialLease
  version
  expiresAt
  readOnlyBytes/readOnlyChars
  close()
  • 이 SPI는 application port가 아니며 application-core에 노출하지 않는다.
  • app-bootstrap은 허용된 config-tree/file-mounted secret source 또는 명시적으로 설치한 runtime provider를 조합한다.
  • Fileserver는 sibling secret/cache adapter를 직접 호출하지 않는다.
  • 여러 adapter가 같은 seam을 실제로 필요로 할 때만 별도 skeleton-wide secret contract ADR을 작성하며, 이 설계에서 shared-contract를 선제 확장하지 않는다.

SecretReference는 bounded registry ID이고 raw file path, URI, environment variable name, secret value를 허용하지 않는다. Config-tree 구현은 bootstrap이 고정한 private root 아래에서 no-follow/owner/mode를 검증해 읽는다. SDK가 임시 Path만 받는 경우 fileserver가 0600 temp file의 생성·삭제를 소유한다.

Lifecycle:

  1. inactive destination은 secret을 resolve하지 않는다.
  2. required destination은 startup/readiness에서 reference 존재와 trust material parse를 확인하되 private key 내용을 log하지 않는다.
  3. 새 physical SSH connection을 만들 때 current secret/trust version을 lease한다.
  4. Session cache entry는 credential version과 trust version으로 표기한다.
  5. TTL/rotation signal에서 old-version session을 새 대여에서 제외하고 bounded drain 후 폐기한다.
  6. 진행 중 upload를 강제 중단할지는 security policy가 정하며, 중단 시 publish outcome을 reconcile한다.
  7. Lease close 시 mutable buffer/temp file을 best-effort zeroize/delete한다.

JVM/SDK가 복사한 key material의 완전한 zeroization은 보장할 수 없다. Heap dump, crash dump, debug log 접근 통제와 process isolation도 운영 통제에 포함한다. Reference resolve 실패, 만료, rotation mismatch는 credential literal fallback 없이 readiness/error로 드러낸다.

16.3 Pool과 timeout

명시적으로 제한한다.

  • max physical SSH connections;
  • max SFTP sessions/channels;
  • session cache size;
  • session acquisition timeout;
  • connect timeout;
  • authentication timeout;
  • socket/read/write idle timeout;
  • overall operation deadline;
  • max outstanding requests;
  • max packet/read/write size;
  • keepalive와 stale-session test;
  • shutdown drain timeout.

Spring Integration의 unbounded cache와 사실상 infinite wait default를 그대로 사용하지 않는다.

16.4 Selected staging strategy

R2 SFTP는 producer를 remote network retry 때문에 재실행하지 않도록 local secure spool을 기본으로 사용한다.

row producer
  -> bounded local encrypted/secure spool + digest
  -> remote .part upload
  -> remote stat/fsync if supported
  -> negotiated publish
  -> local spool cleanup

Trade-off:

  • heap은 bounded;
  • local disk capacity는 추가 필요;
  • network retry 시 source query를 반복하지 않음;
  • sensitive file은 local spool encryption/protection profile 필요;
  • spool reaper와 quota가 필수.

Direct pipe streaming은 retry와 failure isolation이 약하므로 R1 opt-in으로만 둔다.

16.5 Extension handshake

다음 extension은 존재한다고 가정하지 않고 연결마다 협상한다.

Extension 제공 가능 보장
posix-rename@openssh.com POSIX rename semantics
fsync@openssh.com open remote file의 server fsync
statvfs@openssh.com capacity observation
limits@openssh.com packet/request/handle 제한
hardlink@openssh.com 검증된 same-fs hardlink publication 후보

Base SFTP v3 rename은 atomicity를 약속하지 않는다. posix-rename이 없고 destination이 atomic rename을 요구하면 startup/readiness 실패다.

16.6 SFTP durability 한계

fsync@openssh.com은 file handle만 sync한다. Directory-entry fsync extension은 없으므로 SFTP provider는 FILE_AND_DIRECTORY_CRASH_DURABLE을 claim하지 않는다.

가능한 receipt:

REMOTE_FILE_SYNCED
REMOTE_RENAME_ACKNOWLEDGED
REMOTE_NAME_DURABILITY_UNVERIFIED

16.7 Unknown outcome

다음은 FAILED가 아니라 INDETERMINATE다.

  • rename request 이후 connection reset;
  • server가 commit 후 response 전 crash;
  • timeout이 publish request와 겹침;
  • client shutdown 중 remote ACK 유실.

Reconcile:

  1. 같은 operation ID의 final/part/marker를 stat;
  2. final size와 manifest digest 비교;
  3. 가능한 경우 read-back digest;
  4. final match면 기존 receipt 복원;
  5. part만 있으면 resume 조건 또는 cleanup 판단;
  6. final mismatch면 overwrite하지 않고 conflict/quarantine.

16.8 Resume

Resume는 기본 off다. 다음 조건을 모두 만족할 때만 가능하다.

  • source spool이 immutable/repeatable;
  • expected total size와 digest가 있음;
  • remote partial prefix가 동일 source prefix임을 검증;
  • server offset write semantics가 검증됨;
  • operation ID와 staging name이 동일;
  • overall deadline 안에서 재개.

단순 remote size만 보고 offset부터 이어 쓰지 않는다.

17. CSV format와 spreadsheet safety

17.1 Named profile

임의 delimiter option을 request마다 받지 않고 named profile을 등록한다.

Baseline:

  • CSV_RFC4180_MACHINE;
  • CSV_SPREADSHEET_SAFE.

Partner dialect는 fork가 별도 profile ID로 추가한다.

17.2 RFC 4180 machine profile

  • media type text/csv;
  • UTF-8;
  • CRLF record separator;
  • optional header 여부 명시;
  • 모든 row는 schema와 같은 field count;
  • comma separator;
  • double-quote escaping;
  • null과 empty string 정책 분리;
  • BOM off가 기본;
  • malformed/unmappable encoding은 fail;
  • NUL 및 금지 control character 정책;
  • final line break 정책 명시.

현재 구현의 LF-only 출력을 그대로 RFC 4180이라고 부르지 않는다.

17.3 Spreadsheet formula

CSV quoting은 formula execution을 막지 않는다. 모든 column은 다음 중 하나를 선택한다.

Policy 동작
REJECT_FORMULA_LIKE formula-like value면 export 실패
SPREADSHEET_TEXT_PREFIX named profile의 명시적 literalization
PRESERVE_MACHINE_DATA 변환하지 않으며 spreadsheet용 아님
TRUSTED_VALUE 생성값에만 제한적으로 사용

판정은 leading whitespace/control normalization 후 =, +, -, @, tab, CR/LF와 separator/quote를 통한 새 cell 형성 가능성을 검사한다.

범용으로 모든 spreadsheet와 machine re-import에 동시에 안전한 변환은 없다. 따라서:

  • machine용과 human-spreadsheet용 profile을 분리;
  • 변환은 schema에 명시;
  • 변환된 cell count를 receipt/metric에 기록;
  • 원문 cell을 log/audit에 남기지 않음;
  • round-trip data preservation을 요구하면 formula-like cell을 reject.

17.4 Limits

Binding과 profile이 제한한다.

  • max columns;
  • max rows;
  • max cell characters;
  • max encoded cell bytes;
  • max row bytes;
  • max total output bytes;
  • max header bytes;
  • max multiline cell lines;
  • max operation duration.

Limit은 사전 추정만 하지 않고 streaming 중 byte counter로 강제한다.

17.5 CSV test oracle

  • golden byte snapshot;
  • RFC edge case;
  • comma/quote/CR/LF/CRLF;
  • emoji와 multi-byte chunk boundary;
  • unpaired surrogate/encoding failure;
  • null vs empty;
  • row width mismatch;
  • duplicate/blank header policy;
  • formula vector와 leading whitespace;
  • output limit 직전/초과;
  • locale/timezone 독립.

18. File name, path, permission security

18.1 Logical name

logicalFileName은 path가 아니다.

  • Unicode NFC normalization;
  • single logical stem;
  • max UTF-8 bytes;
  • control/NUL/separator 금지;
  • ./.. 금지;
  • Windows device name/drive/UNC grammar 금지;
  • extension은 format/protection profile이 생성;
  • physical final name은 constrained template이 생성.

예:

{logicalStem}-{utcDate}-{operationId}.csv

Arbitrary SpEL과 caller-supplied subdirectory expression을 사용하지 않는다.

18.2 Directory binding

Directory는 configuration에서만 온다.

  • normalized fixed relative segments;
  • no ..;
  • no absolute override;
  • provider root 바깥으로 나갈 수 없음;
  • segment별 max length;
  • startup 시 resolved root 검증.

Unsafe pattern:

normalize -> startsWith -> later open

Safe baseline:

  • private trusted root;
  • secure directory-relative open/move;
  • CREATE_NEW;
  • no-follow attribute checks;
  • target type regular-file 검증;
  • unpredictable staging name;
  • strict directory ownership/permissions.

Hardlink 공격은 portable Java stat만으로 완전히 막기 어렵다. Root에 untrusted writer가 없다는 permission precondition이 핵심이다.

18.4 Permission

  • staging file: owner read/write only;
  • final file: binding의 explicit owner/group/mode;
  • directory: traversal 가능한 최소 권한;
  • umask에만 의존하지 않고 creation attribute/chmod 검증;
  • SFTP는 publish 전에 remote permission 설정;
  • ACL/POSIX 미지원 provider는 required permission guarantee를 claim하지 않음.

19. Collision, concurrency, idempotency

19.1 Publication mode

CREATE_UNIQUE
CREATE_IF_ABSENT
REPLACE_IF_VERSION

REPLACE_ALWAYS는 dev/legacy exception이고 APPEND는 baseline에서 금지한다.

19.2 Same operation concurrency

한 process:

  • in-memory bounded operation registry로 같은 process의 concurrent duplicate producer 실행 방지;
  • terminal result cache는 optimization일 뿐 source of truth가 아님.

Multi-node:

  • shared filesystem의 exclusive reservation과 stale-owner recovery가 failure test로 검증됐으면 operation claim CREATE_NEW;
  • SFTP에는 portable fenced claim이 없으므로 application export-job store가 단일 worker claim;
  • fileserver는 DB/Redis adapter를 직접 호출하지 않음;
  • provider descriptor가 EXTERNAL_OPERATION_COORDINATION_REQUIRED를 표시.

보장 수준을 구분한다.

보장 필요 evidence
accepted attempt 내부 producer at-most-once 모든 provider baseline
process 내부 concurrent single-flight in-memory registry
crash 뒤 sealed payload 재사용 durable journal + persistent spool/staging
여러 node에서 producer global at-most-once durable single-owner claim과 실패 후 ownership 규칙
하나의 logical final로 수렴 stable source revision + immutable operation-derived name + reconcile
stable name conditional replace provider CAS/fencing primitive 또는 authoritative external metadata

외부 job claim이 lease 만료 뒤 old worker를 fence하지 못하면 global at-most-once로 기록하지 않는다. 이 경우 두 producer가 실행될 수 있으므로 source revision은 repeatable해야 하고, 동일 operation의 sealed digest가 다르면 어느 쪽도 overwrite하지 않고 quarantine한다.

SFTP local spool을 seal한 뒤에는 network retry가 producer를 재실행하지 않는다. 그러나 pod 장애로 ephemeral spool이 사라졌다면 기존 attempt를 재생할 수 없다. Persistent spool/control plane이 없으면 operation은 REGENERATION_REQUIRED로 끝나며 global at-most-once 또는 restart-safe idempotency를 claim하지 않는다. R2 required destination은 이 topology를 fail-fast한다.

19.3 Stable final name

여러 pod가 같은 stable name을 교체해야 하면 fileserver lock만으로 correctness를 만들지 않는다.

  • immutable version file을 먼저 publish;
  • current pointer/marker를 expected-version 조건으로 교체;
  • provider가 atomic conditional replace를 증명하지 못하면 external authoritative metadata 사용;
  • reader가 generation/version을 검증.

19.4 FileLock

Java FileLock은 advisory로 취급하며:

  • 같은 JVM thread coordination 용도가 아님;
  • NFS에서는 lease/failover 한계;
  • fencing token을 제공하지 않음;
  • correctness의 단독 근거가 아님.

20. Transaction과 async export workflow

Database와 fileserver는 하나의 transaction이 아니다.

20.1 금지 shape

DB write transaction begin
  -> file publish
  -> DB save
commit

DB rollback이 이미 published file을 되돌리지 못한다.

20.2 권장 large export

REQUESTED export job commit
  -> worker claims job
  -> stable snapshot/cursor pages
  -> fileserver publish(operationId)
  -> receipt persist
  -> COMPLETED

규칙:

  • Fileserver는 job table과 scheduler를 소유하지 않음;
  • application이 retry/cancel/authorization/source snapshot을 소유;
  • fileserver는 one publish attempt와 reconcile을 소유;
  • page query는 짧은 read transaction;
  • source revision이 변하면 same operation fingerprint conflict;
  • HTTP request thread에서 무제한 대용량 export를 실행하지 않음.

20.3 DB command와 file delivery

File delivery가 command 결과에 필수면:

  • business transaction에 delivery intent/outbox/job을 기록;
  • commit 후 worker가 file publish;
  • 실패는 retry/terminal compensation;
  • downstream acknowledgement가 필요하면 별도 inbound receipt use case.

21. Quota, backpressure, timeout, shutdown

21.1 Resource budgets

Per binding/provider:

  • max active publications;
  • max queued publications;
  • max in-flight buffer bytes;
  • max local spool bytes;
  • max file bytes;
  • max rows/columns/cell bytes;
  • min usable-space watermark;
  • max staging artifacts/bytes;
  • max remote sessions/handles/outstanding requests.

Virtual thread를 사용해도 이 budget은 제거되지 않는다.

21.2 Capacity observation 한계

FileStore.getUsableSpace와 SFTP statvfs는 관찰값이지 reservation이 아니다. 다른 process와 node가 동시에 쓸 수 있다.

  • Application은 per-operation byte limit을 강제;
  • native user/group/project quota가 있으면 operator capability로 기록;
  • logical tenant quota가 필요하면 external reservation ledger가 필요;
  • disk low watermark는 새 publish를 거부;
  • pressure 상황에서 unknown final을 임의 삭제하지 않음.

21.3 Timeout 분리

queueAcquireTimeout
sessionAcquireTimeout
connectTimeout
authenticationTimeout
idleReadWriteTimeout
contentProductionDeadline (cooperative)
publishCommitTimeout
overallOperationDeadline
shutdownDrainTimeout

Timeout 뒤 underlying local/NFS/SFTP IO가 즉시 중단됐다고 가정하지 않는다. Publish 단계와 겹치면 INDETERMINATE로 전환한다.

Timeout guarantee를 capability에 기록한다.

  • queue/session/connect/provider IO: 해당 client가 제공하는 cancel/close와 deadline으로 강제;
  • content production: sink.write/checkpoint와 thread interruption 기반 cooperative deadline;
  • upstream DB/HTTP query: 그 port/client의 statement/request timeout이 별도로 필요;
  • arbitrary producer code: Java thread를 안전하게 강제 종료할 수 없으므로 hard timeout을 주장하지 않음;
  • hard wall-clock isolation이 필수인 대규모 export: 별도 worker process/job을 종료한 뒤 fileserver staging을 reconcile.

21.4 Cancellation

  • row sink가 deadline/cancel을 확인;
  • local channel은 interrupt/close;
  • SFTP session/channel은 cancel 시 dirty/close;
  • staging은 commit 전이면 abort;
  • publish commit이 시작됐으면 결과 reconcile;
  • cancellation 결과도 operation journal에 남김.

21.5 Graceful shutdown

  1. 새 publication 접수 중단;
  2. queued request reject;
  3. active stage를 bounded drain;
  4. commit phase는 atomic operation을 마치거나 INDETERMINATE 기록;
  5. SFTP pool close;
  6. 남은 staging은 다음 startup reconciliation 대상.

22. Manifest, marker, retention, reconciliation

22.1 Manifest v1

Canonical JSON:

manifestSchemaVersion
operationId
requestIntentDigest
operationFingerprint
effectivePolicyRevision
effectivePolicyDigest
fileReference
fileId
fileVersion
destinationId
logicalFileName
publishedFileName
sourceRevision
schemaId
exportSchemaVersion
schemaDigest
publishCondition
formatProfileId
protectionProfileId
protectionPolicyDigest
payloadKeyVersion
retentionPolicyDigest
byteSize
rowCount
columnCount
sha256
publicationGuarantee
durabilityGuarantee
createdAt
publishedAt
retentionClassId
formulaMitigatedCount

Provider-private terminal manifest에는 복구용 generatedRelativeLocator와 operation journal revision을 추가할 수 있다. 외부 consumer가 보는 public manifest와 private control manifest를 분리하며, public manifest에는 internal locator를 넣지 않는다.

포함 금지:

  • absolute/remote path;
  • credential;
  • raw row/cell;
  • raw tenant/user ID;
  • secret key ID를 넘어선 key material.

Checksum은 accidental corruption 검출이지 authenticity가 아니다. Shared directory를 신뢰할 수 없으면 signed/HMAC manifest protection profile이 필요하다.

22.2 Marker protocol

version/data.csv
version/manifest.json
version/_SUCCESS

_SUCCESS가 manifest digest를 담고 마지막 commit point가 된다.

Marker protocol은 marker를 이해하는 consumer에게만 atomic publication이다. 외부 시스템이 단순히 *.csv를 scan하면 marker profile을 선택할 수 없다.

22.3 Ownership mode

HANDOFF
MANAGED
  • HANDOFF: published final은 외부 consumer 소유로 간주하고 auto-delete하지 않음;
  • MANAGED: manifest/ref/version이 fileserver 소유임을 증명하는 artifact만 retention 적용.

두 mode 모두 staging cleanup은 수행할 수 있다.

22.4 Reaper

Default는 REPORT_ONLY.

  1. bounded batch로 staging manifest 조회;
  2. operation heartbeat/deadline + clock-skew grace;
  3. active operation 보호;
  4. stale candidate를 .reap 또는 provider claim으로 이동;
  5. final/marker/manifest 재확인;
  6. 알려진 fileserver-owned entry만 no-follow delete;
  7. unknown/malformed entry quarantine 또는 report;
  8. delete rate/duration limit;
  9. audit와 metric 기록.

Atomic cleanup claim이 없는 multi-node provider:

  • single maintenance runner를 deployment가 보장하거나;
  • application composition이 leadership을 제공하거나;
  • report-only로 제한.

Fileserver가 Redis/JDBC lock adapter를 직접 의존하지 않는다.

22.5 Reconciliation cases

관찰 상태 기본 판정
final + matching manifest/digest PUBLISHED 복원
final + digest mismatch QUARANTINED/incident
temp only, active lease 유지
stale temp only abort/reap 후보
final data, marker 없음 marker protocol에서 unpublished
marker, data 없음 integrity incident
manifest, data 없음 integrity incident
unknown external file 보존/report
newer unknown manifest schema 보존/quarantine
SFTP part + no final resume 조건 또는 cleanup
SFTP final + ACK lost stat/digest 후 PUBLISHED 복원

22.6 Retention delete

ELIGIBLE
  -> DELETE_PENDING/tombstone
  -> provider delete
  -> DELETED audit
  • expected version;
  • legal hold;
  • retention grace;
  • unknown schema 보존;
  • delete failure retry;
  • no recursive delete of base/root;
  • target resolution은 opaque reference only.

23. Configuration design

23.1 Activation SSOT

Binding map이 activation SSOT다. Binding이 없으면 capability는 inactive다.

ca-skeleton:
  fileserver:
    destinations:
      worklog-export:
        required: true
        policy-revision: worklog-export-v1
        provider-ref: mounted-primary
        directory: outbound/worklog
        format-profile-ref: csv-machine
        publication:
          protocol: unique-atomic-rename
          collision: create-unique
          required-visibility: atomic-final-name
          required-durability: file-sync
        ownership: managed
        retention-class-ref: export-7d
        protection-profile-ref: internal
        limits:
          max-file-size: 1GB
          max-rows: 5000000
          max-columns: 100
          max-cell-size: 1MB
          max-duration: 30m

    providers:
      mounted-primary:
        type: mounted
        root-directory: ${APP_FILESERVER_PRIMARY_ROOT}
        auto-create: false
        mount-sentinel: .ca-fileserver-mount
        expected-mount-id: ${APP_FILESERVER_PRIMARY_MOUNT_ID}
        min-usable-space: 10GB
        max-concurrent-publications: 4
        max-queued-publications: 16
        control-plane:
          mode: target-private
          directory: .ca-fileserver

      partner-sftp:
        type: sftp
        host: ${APP_FILESERVER_SFTP_HOST}
        port: 22
        username: ${APP_FILESERVER_SFTP_USERNAME}
        secret-material-provider-ref: config-tree-primary
        private-key-secret-ref: ${APP_FILESERVER_SFTP_PRIVATE_KEY_SECRET_REF}
        known-hosts-secret-ref: ${APP_FILESERVER_SFTP_KNOWN_HOSTS_SECRET_REF}
        allow-unknown-keys: false
        session-cache-size: 4
        session-wait-timeout: 2s
        connect-timeout: 5s
        operation-timeout: 2m
        require-extensions:
          - posix-rename@openssh.com
        control-plane:
          mode: persistent-local
          root-directory: ${APP_FILESERVER_SFTP_CONTROL_ROOT}
        local-spool:
          root-directory: ${APP_FILESERVER_SFTP_SPOOL_ROOT}
          persistent: true
          max-total-size: 20GB
          at-rest-protection-profile-ref: spool-internal

    secret-material-providers:
      config-tree-primary:
        type: config-tree
        root-directory: ${APP_FILESERVER_SECRET_CONFIG_ROOT}
        auto-create: false

    format-profiles:
      csv-machine:
        type: csv
        revision: csv-machine-v1
        dialect: rfc4180
        charset: UTF-8
        line-ending: CRLF
        bom: false
        formula-mode: preserve-machine-data

    protection-profiles:
      internal:
        revision: internal-v1
        payload-transform: none
      spool-internal:
        revision: spool-internal-v1
        payload-transform: none
        require-encrypted-spool-volume: true

    retention-classes:
      export-7d:
        revision: export-7d-v1
        duration: 7d
        mode: report-only

위 값은 topology 예시이며 제품별 실제 size/timeout 수치를 의미하지 않는다.

23.2 Typed settings

  • immutable constructor-bound record;
  • Bean Validation과 cross-field validator;
  • duration/data-size typed value;
  • explicit immutable policy revision과 canonical snapshot digest;
  • provider별 sealed settings;
  • blank/default path 금지;
  • prod relative path 금지;
  • secret material 대신 secret reference;
  • unknown property fail;
  • inactive provider는 bean/connection 생성 없음.

23.3 Startup validation

  • destination/provider/profile/reference 존재;
  • duplicate ID 없음;
  • same ID/revision에 다른 canonical policy 금지;
  • journal이 참조하는 N/N-1 frozen policy revision 가용;
  • exact provider binding;
  • required guarantee 충족;
  • local-dev prod 금지;
  • mounted root absolute/pre-provisioned;
  • auto-create prod 금지;
  • R2 control plane이 persistent이고 operation/reference direct lookup을 지원;
  • SFTP spool/control root의 absolute/pre-provisioned/owner/mode/capacity;
  • cluster-wide resolution을 요구하면 control volume의 모든 node 접근성과 coordination evidence;
  • marker protocol과 consumer compatibility;
  • replace mode와 provider replace capability;
  • SFTP known-host와 bounded timeout/pool;
  • secret provider root와 reference grammar, material owner/mode, rotation/session-drain policy;
  • managed retention과 manifest support;
  • staging/final same target namespace;
  • required health/metrics registration.

23.4 Environment registry

docs/registries/env-keys.yaml, application.yml, typed settings, conditional beans를 end-to-end 검증한다.

Template baseline에 필요한 key 예:

APP_FILESERVER_PRIMARY_ROOT
APP_FILESERVER_PRIMARY_MOUNT_ID
APP_FILESERVER_SFTP_HOST
APP_FILESERVER_SFTP_USERNAME
APP_FILESERVER_SFTP_PRIVATE_KEY_SECRET_REF
APP_FILESERVER_SFTP_KNOWN_HOSTS_SECRET_REF
APP_FILESERVER_SFTP_CONTROL_ROOT
APP_FILESERVER_SFTP_SPOOL_ROOT
APP_FILESERVER_SECRET_CONFIG_ROOT

Dynamic destination topology는 YAML/config tree가 소유하고 secret value는 secret source가 제공한다.

24. Health와 observability

24.1 Health

Probe 내용
Liveness JVM/process만; filesystem/SFTP 금지
Startup settings, destination graph, capability probe, manifest compatibility
Readiness enabled + required destination만
Component health optional destination도 상태 노출

Filesystem readiness:

  • root/mount sentinel;
  • read-only/permission;
  • mount identity;
  • usable-space/inode watermark;
  • cached bounded create/write/force/rename/delete probe;
  • 전체 directory scan 금지.

SFTP readiness:

  • bounded session acquire/connect/auth;
  • host key;
  • cached extension capability;
  • remote root stat;
  • pool saturation;
  • write probe는 dedicated hidden probe directory에서 low-frequency opt-in.

24.2 Metrics

fileserver.operation.duration
fileserver.operation.total{provider_type,operation,outcome,failure_code}
fileserver.bytes
fileserver.rows
fileserver.active
fileserver.queue.depth
fileserver.queue.rejected
fileserver.quota.rejected
fileserver.publish.indeterminate
fileserver.reconcile.total{outcome}
fileserver.integrity.failure
fileserver.staging.age
fileserver.staging.bytes
fileserver.cleanup.total{outcome}
fileserver.usable_space.ratio
fileserver.sftp.session.active
fileserver.sftp.session.wait
fileserver.sftp.reconnect
fileserver.formula.mitigated

Allowed tags:

  • provider type/ID from bounded registry;
  • destination ID from bounded registry;
  • operation kind;
  • outcome/failure code;
  • format profile;
  • publication guarantee.

Forbidden tags:

  • operation/file ID;
  • file name/path;
  • checksum;
  • tenant/user;
  • host when dynamically unbounded;
  • row/cell content.

24.3 Trace

Span:

fileserver.publish
fileserver.stage
fileserver.provider.upload
fileserver.publish.commit
fileserver.reconcile
fileserver.cleanup

Attributes are bounded provider/destination/profile/guarantee/outcome only. Operation ID는 log/trace correlation field로 사용할 수 있지만 metric tag로 쓰지 않는다.

24.4 Log와 audit

  • physical path와 remote URI를 info log에 기록하지 않음;
  • credential, known-host content, cell value 금지;
  • publish/unknown/reconcile/delete는 structured event;
  • overwrite/delete/retention은 audit 대상;
  • diagnostic cause는 server log only;
  • filename이 민감할 수 있으므로 logical name도 기본 mask.

25. Threat model

위협 통제
../, absolute, drive/UNC caller path 제거, destination ID + logical stem
nested/target symlink secure relative operation, no-follow, trusted root
check/open TOCTOU SecureDirectoryStream 또는 provider strict capability
hardlink attack private directory UID/mode, untrusted writer 금지
mount 누락 local fallback pre-provision, auto-create off, mount sentinel/identity
partial final staging + atomic rename/marker
old good file truncation direct final write 금지
arbitrary overwrite create unique/expected version
same-name writer race immutable name, provider primitive, external coordination
process crash state/manifest/reconcile
publish ACK loss INDETERMINATE, no blind retry
CSV formula column formula policy
delimiter/control injection schema/dialect encoder
heap/disk/inode exhaustion streaming limits, quota, watermarks
PII leakage path/content-free logs/metrics/receipt
SFTP MITM pinned known-host/host CA
credential leakage secret reference and log scrub
malicious remote server size/time/request limits, strict extension parsing
forged manifest trusted control dir 또는 signed manifest
reaper over-delete owned manifest only, report-only, no recursive delete
NFS weak coherence immutable versions, marker/receipt, no polling authority
advisory/lease lock loss lock을 correctness boundary로 사용하지 않음
stale resume corruption digest/prefix/repeatability 검증 없으면 resume off

26. Test와 CI design

26.1 Application contract tests

  • request/value/receipt invariant;
  • receipt에 path/URI 없음;
  • opaque reference direct lookup과 forged/unknown route 거부;
  • operation ID fingerprint;
  • effective policy snapshot digest와 frozen revision resume;
  • 동일 completed operation에서 producer 0회;
  • 하나의 accepted attempt에서 producer 최대 1회;
  • global coordination capability가 없을 때 at-most-once claim 거부;
  • producer source exception 보존;
  • cooperative checkpoint/deadline과 blocking producer 한계 표면화;
  • sink failure에서 staging abort;
  • mismatch conflict;
  • regeneration은 prior expected revision/attempt + new operation ID + exclusive supersession 필수;
  • stale retry가 regeneration port를 우회하지 못함;
  • Spring/Path/File/InputStream/vendor type leakage ArchUnit.

26.2 CSV unit/property tests

  • RFC 4180 golden bytes;
  • CRLF;
  • comma/quote/CR/LF/multiline;
  • UTF-8/emoji/multi-byte buffer boundary;
  • malformed surrogate;
  • null/empty;
  • row width/type mismatch;
  • blank/duplicate header;
  • formula vectors와 whitespace/control prefix;
  • cell/row/total byte limit;
  • locale/timezone independence;
  • randomized round-trip parser property.

26.3 Local filesystem integration

  • staging/final same FileStore;
  • restrictive create permission;
  • reader가 partial final을 보지 않음;
  • same operation concurrency;
  • create conflict;
  • expected-version replace;
  • symlink parent/target swap;
  • root/mount identity change;
  • permission denied/read-only;
  • ENOSPC/EDQUOT/inode exhaustion;
  • write/flush/force/rename/dir-sync 단계별 fault;
  • commit ACK 뒤 stat/reference-index/journal update failure와 INDETERMINATE;
  • J-sealed/data/M-private/R-ref/J-final 각 경계 crash와 deterministic reconstruction;
  • data-only atomic rename에서 public sidecar atomicity를 claim하지 않음;
  • operation/reference journal atomic revision, corruption, direct recovery;
  • journal과 final/manifest 모순의 truth-priority/quarantine;
  • process kill 후 상태별 reconciliation;
  • active writer와 reaper race;
  • two reaper claim race;
  • unknown file preservation;
  • bounded heap with generated millions of rows.

26.4 SFTP real integration

실제 OpenSSH container를 production-readiness profile에서 사용한다.

  • known-host success/mismatch/rotation;
  • key auth failure;
  • connect/auth/read/write/overall timeout;
  • bounded pool wait/reject;
  • large spool/upload;
  • disconnect during upload;
  • disconnect before/after rename ACK;
  • persistent spool/control volume 유실 및 재시작 receipt 복원;
  • server restart;
  • extension present/absent;
  • posix-rename, fsync, statvfs, limits;
  • remote permission;
  • private-key/known-host rotation에서 old session drain과 새 version 사용;
  • secret reference 미해결, path escape, material log/heap fixture 부재;
  • part/final/marker reconciliation;
  • resume prefix mismatch;
  • graceful shutdown.

Mock-only test로 SFTP guarantee를 증명하지 않는다.

26.5 NFS/multi-client

전용 Linux runner 또는 nightly profile:

  • 실제 NFS server와 두 mount client;
  • same-fsid rename;
  • close/reopen visibility;
  • directory cache delay;
  • server restart;
  • temporary disconnect/stale handle;
  • lease expiry;
  • async export profile 거부;
  • mount missing/local fallback;
  • two-node immutable publication;
  • marker-aware consumer.

NFS service가 없으면 production-readiness job은 skip하지 않고 실패한다.

26.6 Maintenance/security/observability

  • active stage 보존;
  • stale/unknown/newer manifest 처리;
  • report-only default;
  • retention/legal hold/version;
  • repeated cleanup idempotency;
  • metric cardinality;
  • path/content/credential log absence;
  • trace propagation;
  • readiness cache;
  • liveness independence;
  • provider capability mismatch startup failure;
  • content producer가 checkpoint를 호출하지 않는 경우 hard-timeout을 주장하지 않음;
  • control plane persistence/cluster access와 secret provider binding validation.

26.7 Compatibility

  • manifest N/N-1 read;
  • format/protection/retention/binding frozen policy N/N-1 resume;
  • same profile ID/revision의 canonical digest drift 기동 실패;
  • newer schema quarantine;
  • rolling deployment writer/reader matrix;
  • OpenSSH supported-version matrix;
  • NFS server/client supported matrix;
  • Linux/macOS/Windows local grammar;
  • legacy exportCsv migration wrapper;
  • public contract snapshot.

26.8 CI tasks

:adapter:outbound:fileserver:test
:adapter:outbound:fileserver:integrationTest
:adapter:outbound:fileserver:sftpIntegrationTest
:adapter:outbound:fileserver:filesystemFailureTest
:adapter:outbound:fileserver:securityTest
:adapter:outbound:fileserver:contractTest
fileserverProductionReadiness

PR:

  • application/CSV/local contract;
  • architecture/config gating;
  • deterministic OpenSSH baseline.

Nightly:

  • NFS multi-client;
  • failure injection;
  • supported provider matrix;
  • process kill/recovery;
  • longer concurrency/heap soak.

27. Gradle, dependency, bootstrap design

27.1 Dependency ownership

현재 가장 가까운 src/adapter/outbound/fileserver/CLAUDE.md는 pure JDK, external dependency 없음, NFS/SFTP stand-in만을 허용한다. 따라서 이 문서만으로 SFTP SDK를 Gradle에 추가할 수 없다. 구현 Phase 0에서 아키텍처 승인과 함께 다음 rule drift를 먼저 갱신한다.

  • local CLAUDE.md의 책임을 local-only demo에서 provider-based publication으로 변경;
  • external NONE 규칙을 exact allowlist로 변경;
  • broad spring-boot-starter 허용 문구를 실제 narrow autoconfigure 정책으로 수정;
  • README의 registry SSOT와 runtime composition 설명 수정;
  • architecture/Gradle test가 새 allowlist를 강제.

이 rule migration 전 SFTP dependency 추가나 runtime activation은 HARD-STOP이다.

adapter-outbound-fileserver:

  • JDK NIO local/mounted provider;
  • Spring autoconfigure;
  • SLF4J API;
  • Micrometer/Observation instrumentation if direct;
  • spring-integration-sftp implementation dependency;
  • Apache MINA types transitive/implementation only;
  • provider test tools in test configurations.

application-core:

  • project dependencies와 Java standard types only;
  • SFTP/Spring/Micrometer 없음.

SFTP dependency는 api로 노출하지 않고 Boot BOM compatible version을 사용한다. 별도 broad starter를 추가하지 않는다.

27.2 Bootstrap composition

안전한 explicit binding/gating과 config test가 먼저 구현된 후:

  1. modules.jsonapp-bootstrap.allowed_dependenciesadapter-outbound-fileserver 추가;
  2. app-bootstrap/build.gradle에 implementation dependency 추가;
  3. exact module count는 19 유지;
  4. no destination이면 zero bean/connection/scheduler;
  5. optional adapter gating test에 fileserver 추가;
  6. disabled-adapter architecture scan에 fileserver 추가;
  7. env/settings/readiness contract 추가.

Classpath에 들어왔다는 이유로 local provider가 활성화되면 안 된다.

27.3 SDK split trigger

다음 중 하나가 실제로 발생하면 fileserver-sftp leaf split ADR을 작성한다.

  • SFTP SDK security patch cadence가 독립적;
  • local-only runtime에서 SFTP transitive dependency 제거 필요;
  • provider별 deployment artifact 분리;
  • 팀/릴리스 ownership 분리;
  • module test/runtime 시간이 독립 관리되어야 함.

28. Migration

Phase 0 — Truthful topology와 contract freeze

  • 현재 Fileserver를 R1 local CSV demo로 명시;
  • Fileserver CLAUDE.md와 README의 responsibility/dependency/registry SSOT drift 수정;
  • current bootstrap 미합성 상태 명시;
  • v2 contract와 error registry 승인;
  • journal/reference/control-plane schema 승인;
  • accepted-attempt와 global coordination guarantee 분리;
  • effective policy freeze/revision/digest와 N/N-1 resume 정책 승인;
  • explicit regeneration/supersession 계약 승인;
  • protocol별 artifact publication/crash ordering 승인;
  • secret material SPI와 lifecycle 승인;
  • activation/binding/settings schema 승인;
  • 기존 API deprecation 계획.

Acceptance:

  • 문서와 startup diagnostics가 NFS/SFTP 구현이 있다고 주장하지 않는다.

Phase 1 — Streaming application contract와 CSV

  • FilePublicationPort;
  • operation ID/fingerprint;
  • effective policy snapshot;
  • row producer/sink;
  • typed cell/schema;
  • CSV profiles/formula/limits;
  • opaque receipt;
  • legacy adapter wrapper.

Acceptance:

  • 전체 rows/CSV/byte[] materialization 없이 bounded heap test 통과.

Phase 2 — Secure local/mounted publication

  • staging;
  • digest/manifest;
  • sealed journal과 protocol별 artifact ordering;
  • file sync;
  • atomic publish;
  • path/permission/mount safety;
  • error/outcome state;
  • local reconciliation.

Acceptance:

  • reader partial final 0건, 단계별 crash recovery, symlink race test 통과.

Phase 3 — Resource/maintenance/observability

  • concurrency/byte quota;
  • timeout/cancel/shutdown;
  • staging reaper/report;
  • managed retention;
  • health/metrics/traces/audit.

Acceptance:

  • capacity failure가 bounded하고 unknown file을 삭제하지 않는다.

Phase 4 — SFTP provider

  • Spring Integration/Apache MINA;
  • host key/secrets;
  • bounded pool/timeouts;
  • local spool;
  • extension negotiation;
  • remote unknown-outcome reconciliation;
  • OpenSSH contract tests.

Acceptance:

  • required extension 없음, ACK loss, server restart 경로가 silent downgrade 없이 검증된다.

Phase 5 — NFS/HA evidence와 bootstrap

  • multi-client NFS profile;
  • operator attestation;
  • app-bootstrap composition;
  • env/readiness/architecture gates;
  • sample/reference export workflow.

Acceptance:

  • 선택한 deployment profile의 effective guarantee만 R2로 표시된다.

Phase 6 — Optional read/delete와 module split review

  • opaque content transfer;
  • expected-version managed delete;
  • provider split 조건 재평가;
  • rolling compatibility matrix.

29. 완료 기준

Fileserver R2 완료를 주장하려면:

  • application contract에 path/provider/SDK가 없음;
  • bounded-memory streaming;
  • stable operation ID와 fingerprint;
  • effective policy revision/digest freeze와 rolling resume;
  • durable operation/reference control plane과 restart receipt 복원;
  • accepted-attempt와 global producer execution guarantee를 분리;
  • direct final write 없음;
  • no silent atomicity/durability downgrade;
  • receipt에 achieved guarantee와 opaque reference;
  • local/mounted/SFTP provider가 각자 capability card 제공;
  • required guarantee startup validation;
  • symlink/mount/permission/host-key security;
  • secret reference resolution/rotation/session-drain lifecycle;
  • create/replace concurrency semantics;
  • unknown outcome reconciliation;
  • explicit regeneration/supersession revision guard;
  • commit 후 confirm/control-record 실패의 INDETERMINATE 처리;
  • protocol별 data/manifest/reference/journal crash ordering;
  • quota/backpressure/timeouts/shutdown;
  • cooperative content deadline과 hard provider timeout의 분리;
  • manifest/reaper/retention 안전성;
  • CSV schema/formula/encoding/limit;
  • classified error;
  • bounded observability;
  • real provider/failure/compatibility tests;
  • app-bootstrap opt-in composition;
  • runbook과 capacity inputs;
  • core dependency purity와 exact-19 architecture gate 통과.

다음 문구는 금지한다.

  • “NFS/SFTP stand-in이므로 production-ready”
  • “rename이 atomic이므로 crash durable”
  • “fsync를 호출했으므로 모든 remote storage에서 durable”
  • “FileLock으로 distributed correctness 보장”
  • “CSV quoting으로 formula injection 해결”
  • “retry하면 같은 파일이 정확히 한 번 생성”
  • “path normalize로 symlink 공격 해결”
  • “usable space가 남았으므로 quota 확보”

30. 운영 runbook 요구

  • mounted root/mount identity mismatch;
  • disk/inode/tenant quota;
  • staging backlog/orphan;
  • publish indeterminate;
  • checksum/manifest mismatch;
  • SFTP host-key rotation;
  • SFTP credential rotation;
  • pool saturation/session leak;
  • remote extension/version drift;
  • NFS server restart/stale handle;
  • async/sync export configuration;
  • retention legal hold;
  • manifest schema rolling upgrade;
  • cleanup report-only에서 delete mode 전환;
  • backup/restore 후 operation reconciliation;
  • frozen policy/key revision unavailable;
  • regeneration approval과 supersession conflict;
  • committed data와 private control record 불일치.

31. Primary references