92 KiB
Fileserver Production Capability Deep Design
- 작성일: 2026-07-26
- 상태: 상세 설계 완료, Phase 0–1 및 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-core의FilePublicationPort, 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/PUBLISHEDoperation 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로 추적·검증·복구하는 기능
선택한 핵심 구조는 다음과 같다.
- Application은 목적지를 path/host가 아닌
FileDestinationId로 선택한다. - 안정적인
FilePublishOperationId가 retry와 reconciliation의 기준이다. - 행 데이터는 동기식 row-producer callback으로 한 번만 흘려보낸다.
- Adapter가 staging, encoding, checksum, close, publish, abort를 모두 소유한다.
- Provider는 설정값이 아니라 실제 probe 결과인 effective capability를 보고한다.
- 요구한 atomicity/durability보다 provider 보장이 약하면 자동 downgrade하지 않는다.
- publish 응답 유실은 실패가 아니라
INDETERMINATE로 모델링하고 먼저 reconcile한다. - 기본 파일명 정책은 immutable/versioned이며 unconditional overwrite와 append는 금지한다.
- File Server와 business database 사이의 원자적 commit은 주장하지 않는다.
- 사용하지 않는 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 | 모든 IOException을 INTERNAL_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.javasrc/application-core/src/main/java/dev/caskeleton/application/fileexport/ExportedFile.javasrc/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FilesystemCsvExportAdapter.javasrc/adapter/outbound/fileserver/src/main/java/dev/caskeleton/adapter/outbound/fileserver/FileExportProperties.javasrc/adapter/outbound/fileserver/src/test/java/dev/caskeleton/adapter/outbound/fileserver/FilesystemCsvExportAdapterTest.javasrc/config/architecture/modules.jsonsrc/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
다음 조건은 구현 편의를 위해 낮출 수 없다.
application-core에 Spring,Path,File,InputStream, SFTP SDK, Micrometer 타입을 노출하지 않는다.- Application request에는 host, port, credential, base directory, remote path가 없다.
- Physical final path는 adapter의 target binding에서만 계산한다.
- Caller 문자열을
Path.resolve의 자유 경로로 사용하지 않는다. - Final name을 가진 파일에 직접 streaming write하지 않는다.
- Temp/staging은 publish target과 동일 filesystem/remote namespace에 존재한다.
ATOMIC_MOVE실패 시 non-atomic move/copy로 조용히 fallback하지 않는다.- Atomic visibility와 crash durability를 같은 guarantee로 표현하지 않는다.
- 하나의 accepted publish attempt 안에서 producer는 최대 한 번만 실행한다. Process 또는 node 장애를 넘는 global at-most-once는 durable ownership evidence 없이는 주장하지 않는다.
- 같은 accepted attempt의 provider retry를 위해 application row producer를 다시 호출하지 않는다.
INDETERMINATE결과를 blind retry하지 않는다.APPEND는 R2 publication mode가 아니다.- Default collision policy는 unconditional replace가 아니다.
- File name/path/content/checksum/user/tenant를 metric tag로 사용하지 않는다.
- Unknown file과 유효하지 않은 manifest를 reaper가 자동 삭제하지 않는다.
- Fileserver가 persistence, Redis lock, objectstorage adapter에 의존하지 않는다.
local-devprovider는 prod profile에서 시작하지 않는다.- SFTP unknown host key 허용, password literal, infinite timeout/pool은 금지한다.
- 현재 mount가 기대한 mount인지 증명하지 못하면 mounted provider는 ready가 아니다.
- 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
PublishedFileReference와 FileVersion은 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
규칙:
- Authorized application use case가 principal/tenant/business 승인을 먼저 수행한다.
regenerationDecisionId는 그 결정을 audit에 연결하는 opaque ID이지 bearer credential이 아니다. - Adapter는 prior operation을 먼저 reconcile한다. Final commit 가능성이 남은
INDETERMINATE에서는 regeneration을 거부한다. - Prior state가 정확히
REGENERATION_REQUIRED이고 expected revision/attempt가 일치해야 한다. newRequest.operationId는 새 ID여야 하며 old journal을 다시 열지 않는다. 나머지 request intent와 schema를 canonicalize해 priorrequestIntentDigest와 비교한다.- Provider control plane의 고정 key
supersessions/{priorOperationId}.json을 exclusive-create하고 record 안에newRequest.operationId, expected revision/attempt, decision ID를 저장한다. - 같은 prior operation에 이미 다른 supersession이 있으면
FILESERVER_REGENERATION_CONFLICT다. - 새 operation은 원래 freeze된 policy/source revision/request intent를 재사용한다. 현재 정책으로 바꾸고 싶으면 regeneration이 아니라 별도 신규 publication이다.
- Prior sealed digest가 남아 있으면 regenerated payload digest가 정확히 일치해야 한다. Digest 전에 실패한 attempt는 application이 repeatable source revision을 증명할 때만 regeneration한다.
- 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:
- 새 record를 private temp name으로
CREATE_NEW한다. - canonical encoding과 digest를 검증한다.
- provider가 증명한 atomic replace 또는 marker protocol로
stateRevision을 전진시킨다. - required durability 수준에 맞춰 record와 directory/remote file을 sync한다.
- 더 낮거나 중복된 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 중 하나를 명시한다.
HANDOFFdestination이 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
- destination binding과 provider effective capability를 조회한다.
- request invariant, fingerprint, duplicate operation 상태를 확인한다.
- required guarantee와 provider descriptor를 비교한다.
- final name, staging name, marker/manifest plan을 생성한다.
- quota/concurrency slot과 deadline budget을 획득한다.
12.2 Stage
- target filesystem/remote namespace 내부 private staging에
.<operationId>.part를 exclusive-create한다. - Temp와 final이 같은 filesystem/fsid/remote root인지 검증한다.
- 제한 권한을 creation 시점에 설정한다.
- CSV encoder를 bounded byte buffer 위에 구성한다.
- row마다 schema, column count, formula, control, size, deadline을 검사한다.
- SHA-256, byte count, row count를 streaming 중 계산한다.
- 선택된 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
- Encoder를 flush하고 malformed/unmappable character를
REPORT정책으로 검사한다. - Channel/remote handle을 close하기 전에 요구되는 file sync를 수행한다.
- Final byte size와 digest를 확정한다.
- Frozen policy와 final locator를 포함한 private manifest draft를 canonical JSON으로 만든다.
- Sealed digest와 manifest draft를 operation journal에 durable하게 기록한다.
- Provider가 지원하면 staging content를 다시 stat/read-back한다.
- 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
- Final stat의 type, size, version을 확인한다.
- 가능한 provider는 digest를 read-back 검증한다.
- Marker/manifest가 final artifact를 정확히 가리키는지 확인한다.
- Reference index와 achieved guarantee를 포함한 receipt snapshot을 만든다.
- Operation journal을 terminal
PUBLISHED로 durable하게 전진시킨다. - 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_SYNCclaim; - 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_LINKSattribute/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_ABSENTstable 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과
FileStoreidentity; - 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
syncexport/backend stable storage는 operator attestation과 failure test 필요; asyncexport는 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-ref와 known-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:
- inactive destination은 secret을 resolve하지 않는다.
- required destination은 startup/readiness에서 reference 존재와 trust material parse를 확인하되 private key 내용을 log하지 않는다.
- 새 physical SSH connection을 만들 때 current secret/trust version을 lease한다.
- Session cache entry는 credential version과 trust version으로 표기한다.
- TTL/rotation signal에서 old-version session을 새 대여에서 제외하고 bounded drain 후 폐기한다.
- 진행 중 upload를 강제 중단할지는 security policy가 정하며, 중단 시 publish outcome을 reconcile한다.
- 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:
- 같은 operation ID의 final/part/marker를 stat;
- final size와 manifest digest 비교;
- 가능한 경우 read-back digest;
- final match면 기존 receipt 복원;
- part만 있으면 resume 조건 또는 cleanup 판단;
- 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 검증.
18.3 TOCTOU와 links
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
- 새 publication 접수 중단;
- queued request reject;
- active stage를 bounded drain;
- commit phase는 atomic operation을 마치거나 INDETERMINATE 기록;
- SFTP pool close;
- 남은 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.
- bounded batch로 staging manifest 조회;
- operation heartbeat/deadline + clock-skew grace;
- active operation 보호;
- stale candidate를
.reap또는 provider claim으로 이동; - final/marker/manifest 재확인;
- 알려진 fileserver-owned entry만 no-follow delete;
- unknown/malformed entry quarantine 또는 report;
- delete rate/duration limit;
- 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
exportCsvmigration 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-sftpimplementation 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가 먼저 구현된 후:
modules.json의app-bootstrap.allowed_dependencies에adapter-outbound-fileserver추가;app-bootstrap/build.gradle에 implementation dependency 추가;- exact module count는 19 유지;
- no destination이면 zero bean/connection/scheduler;
- optional adapter gating test에 fileserver 추가;
- disabled-adapter architecture scan에 fileserver 추가;
- 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
- Java 21
Files.moveandATOMIC_MOVE - Java 21
FileChannel.force - Java 21
SecureDirectoryStream - Java 21
FileLockplatform dependencies - Java 21
FileStore.getUsableSpace - Linux
fsync(2) - Linux
renameat2(2) - Linux
openat2(2) - NFSv4.1 RFC 8881 RENAME
- NFSv4.1 RFC 8881 WRITE
- NFSv4.1 RFC 8881 COMMIT
- Spring Integration SFTP support
- Spring Integration SFTP outbound temporary-name protocol
- Spring Integration SFTP session caching
- Spring Integration
SftpSessionAPI - OpenSSH SFTP extensions
- SFTP v3 draft
- RFC 4180 CSV
- OWASP CSV Injection