72 KiB
Server file capability infrastructure handoff
- 상태: 설계 완료, 서버 구현 대기
- 목표 상태:
AVAILABLE_NOT_COMPOSED - 대상: 이후 서버 template 또는 제품 backend에서 구현할 공통 file capability
- 비대상: 제품 domain, 제품 use case, HTTP route/controller, provider 선택
- 기준일: 2026-07-28
이 문서는 frontend에 준비된 File/Blob/picker/download capability와 이후 연결할 서버측 기반을 정의한다. 서버는 object storage, multipart, integrity, signed capability, idempotency와 quarantine을 실제 provider adapter로 제공하되, 제품이 파일 기능을 선택하기 전에는 endpoint, background job, database migration과 runtime composition을 설치하지 않는다.
브라우저 쪽 capability envelope, bounded fetch, resume checkpoint와 Image CDN
descriptor 계약은
presigned-transfer-and-image-cdn.md와
VD-12가 소유한다. Range resume/background 경계는 VD-14, top-level transfer
composition과 Image descriptor provider는 VD-16이 소유한다. 이 문서의 server
port를 그 frontend DTO에 직접 노출하지 않고, 제품 BFF/controller가 두 경계
사이를 매핑한다.
S3, MinIO, GCS, Azure Blob, PostgreSQL, Redis, scanner vendor는 이 문서의
port 구현 후보일 뿐이다. provider SDK type, bucket/key, multipart upload ID,
scanner 원문 결과는 port 밖으로 노출하지 않는다.
이 설계의 최상위 불변조건은 다음과 같다.
object storage에 byte가 존재하는 것, multipart complete가 성공한 것, scan이 clean으로 끝난 것, 사용자가 접근 가능한
AVAILABLE상태는 서로 다른 commit point다. 어느 단계도 다음 단계를 암묵적으로 보장하지 않는다.
1. 설계 경계
1.1 이 문서가 제공하는 것
- streaming object read/write의 provider-neutral port와 불변조건
- multipart staging/complete/abort 상태 머신
- 실제 byte length와 checksum의 authoritative 검증 규칙
- 짧은 수명의 제한된 signed capability
- 동시 replay를 견디는 idempotency store
- immutable quarantined object를 검사하는 scan port
- provider locator를 숨기는 opaque object reference
- deadline, retry, cancellation과 resource cleanup 규칙
- provider exception을 닫는 공통 failure model
- 모든 provider가 통과해야 하는 동일 contract test
- 미선택 상태, 선택적 composition과 완전 제거 계약
1.2 이 문서가 만들지 않는 것
AttachContractDocument,UploadProfileImage같은 제품 use caseContractAttachment,EvidenceDocument같은 domain model- 업무별 MIME, 용량, retention, 소유권과 승인 규칙
- 실제 REST/GraphQL endpoint와 request/response DTO
- account/tenant authorization 구현
- 실제 bucket, region, database, queue, scanner 선택
- frontend offline sync 전체 protocol
- CDN/Service Worker/Cache Storage release protocol
제품 선택 이후에는 이 capability 위에 feature application port와 use case를 추가한다. 공통 capability가 domain 이름이나 aggregate ID를 알게 해서는 안 된다.
이 문서의 create, putPart, complete, scan, promote는 provider와 technical
lifecycle을 닫기 위한 platform protocol operation이지, 그대로 외부에 노출할 제품
use case가 아니다. 제품 use case가 생기기 전에는 caller authorization, aggregate
연결, 업무 정책 선택과 endpoint가 없으므로 실행 경로도 composition하지 않는다.
단, orphan reconciliation과 resource cleanup 규칙은 adapter가 만든 기술 자원의
정합성을 위한 infrastructure 책임으로 정의해 둘 수 있다.
1.3 계층과 방향
future product feature
domain/use case
-> product-owned file port
-> server file capability facade
-> ObjectByteStorePort
-> MultipartStagingPort
-> ObjectRegistryPort
-> IdempotencyStorePort
-> ContentScannerPort
-> CleanPromotionPort
-> SignedTransferCapabilityPort
-> provider adapters
S3 / MinIO / GCS / Azure Blob
PostgreSQL / Redis
scanner / CDR
HTTP controller는 미래의 inbound adapter다. object store, metadata database, scanner와 signer 구현은 outbound adapter다. 이 문서의 port와 protocol은 application-neutral server platform 경계다.
1.4 control plane과 data plane
두 plane을 하나의 범용 FileService method로 뭉개지 않는다.
| plane | 책임 | 일반적인 경로 |
|---|---|---|
| control | authorization, session, 정책 snapshot, idempotency, 상태 전이, capability 발급 | Browser → Web API/BFF |
| data | bounded byte upload/download, range, checksum, backpressure | Browser → BFF proxy 또는 제한된 signed URL → object store |
| inspection | quarantine read, parser/scanner/CDR, verdict | worker → private object store/scanner |
브라우저는 provider 관리자 credential, internal bucket/key 또는 database credential을 받지 않는다. direct transfer가 필요하면 control plane이 exact operation에 한정된 short-lived capability만 발급한다.
1.5 권장 서버 모듈 경계
언어와 framework가 달라도 dependency 방향은 다음과 같이 유지한다.
file-capability/
protocol/
identifiers, lifecycle, policy, failure, receipts
ports/
object-store, multipart, registry, idempotency, signer, scanner, promotion
runtime/
streaming, integrity, deadline, retry, resource-lease, reconciliation
adapters/
object-store/<provider>
registry/<provider>
idempotency/<provider>
signer/<provider>
scanner/<provider>
contract-tests/
reusable suites, fixtures, conformance evidence
composition/
optional provider profile, probes, workers, kill switches
protocol과ports는 HTTP/framework/provider SDK에 의존하지 않는다.runtime은 제품 domain을 모르며 port와 protocol만 사용한다.- concrete SDK type과 exception은 각 adapter package 안에서 끝난다.
- concrete adapter를 동시에 import할 수 있는 곳은 composition root와 해당 provider contract fixture뿐이다.
- 제품 module은 capability facade 또는 product-owned port에만 의존한다.
- provider adapter는 별도 package/dependency로 격리해 미선택 build와 제거 profile에서 아예 포함되지 않게 한다.
- 미래 inbound controller와 제품 use case는 이 tree 밖의 제품 feature가 소유한다.
2. 공통 식별자와 영속 모델
2.1 외부에 노출 가능한 opaque reference
다음 reference는 URL-safe cryptographic random 값이거나 같은 수준의 registry-issued opaque 값이어야 한다.
ObjectRef
UploadSessionRef
UploadPartRef
TransferCapabilityReceipt
IdempotencyKey
ScanJobRef
최소 요구사항:
- 최소 128-bit의 예측 불가능성
- account ID, email, 업무 key, filename, bucket, region을 인코딩하지 않음
- 대소문자와 Unicode normalization이 개입하지 않는 제한된 alphabet
- log/metric/trace label에 원문을 기록하지 않음
- reference만으로 authorization을 얻지 못함
- 서로 다른 종류의 reference를 type/namespace로 혼용하지 않음
- authorization scope와 object generation/version은 reference 문자열에 넣지 않고 server registry와 authorization decision에서 별도로 검증
- caller가 볼 권한이 없는 reference와 존재하지 않는 reference는 enumeration이 가능한 API에서 같은 외부 failure 표현 사용
ObjectRef는 provider object key가 아니다. server-owned registry가 아래 binding을
소유한다.
ObjectBinding
objectRef
generation
providerProfileId
encryptedProviderLocator
state
byteLength
mediaType
wholeObjectDigest
createdAt
policySnapshotId
encryptedProviderLocator에는 provider가 요구하는 bucket/key/version locator만
보관한다. controller, domain과 frontend DTO에 반환하지 않는다. provider migration
시에도 ObjectRef는 유지하고 binding만 versioned CAS로 교체할 수 있어야 한다.
2.2 technical lifecycle
제품 domain 상태와 분리된 server file capability의 기술 상태는 다음과 같다.
UploadSession
OPEN
-> COMPLETE_REQUESTED
-> PROVIDER_COMMITTED
-> QUARANTINE_RECORDED
-> QUARANTINED
-> ABORT_REQUESTED -> ABORTED
-> EXPIRED -> CLEANUP_PENDING -> CLEANED
StoredObject
STAGING
-> QUARANTINED
-> AVAILABLE
-> REJECTED
-> DELETING -> DELETED
불변조건:
AVAILABLE전에는 일반 download capability를 발급하지 않는다.QUARANTINEDobject는 private/non-executable 위치와 response policy를 사용한다.- 상태 전이는
(objectRef, generation, expectedState)CAS로 수행한다. - scan verdict는 exact generation과 digest에 binding한다.
- complete/abort/expiry race는 하나의 durable 상태만 승리한다.
PROVIDER_COMMITTED는 provider complete 성공과 registry 응답 유실 사이를 복구하는 필수 fence다.QUARANTINE_RECORDED는 exact provider generation/length/digest와 scan outbox가 같은 database transaction 또는 동등한 atomic outbox로 durable해진 상태다.- terminal state를 되돌리려면 새 generation과 새 audit record를 만든다.
- provider object 존재만으로
AVAILABLE이라고 판단하지 않는다.
2.3 정책 snapshot
미래 use case가 선택한 정책은 session 생성 시 immutable snapshot으로 binding한다.
TransferPolicySnapshot
policyId
policyVersion
maxObjectBytes
minPartBytes
maxPartBytes
maxPartCount
maxConcurrency
allowedChecksumAlgorithms
requiredScanProfile
sessionExpiresAt
capabilityMaxTtl
orphanRetention
caller는 이 한도를 낮출 수만 있다. 진행 중인 session이 mutable configuration을 다시 읽어 한도가 상승하거나 의미가 바뀌어서는 안 된다. 긴급 차단은 별도 deny/kill-switch registry로 fail-closed하게 적용한다.
3. Server platform ports
아래 signature는 언어 중립적인 의미 계약이다. 실제 서버 언어의 native stream, SDK response와 exception이 이 경계를 통과해서는 안 된다.
3.0 공통 실행 문맥과 숫자 표현
모든 I/O port 호출은 같은 bounded execution context를 받는다.
OperationContext
registeredOperation
opaqueRequestId
opaqueTraceId?
monotonicDeadline
cancellation
- ingress timeout은 server-owned hard cap으로 clamp한다.
- 다른 process로 전달할 때 client의 absolute timestamp를 신뢰하지 않고 남은 timeout을 상한 안에서 전달한 뒤 각 process에서 monotonic deadline으로 바꾼다.
- child operation은
min(parent remaining, operation cap)만 사용할 수 있다. - queue 대기, retry backoff와 cleanup 전의 본 작업도 전체 deadline에 포함한다.
- request/trace ID와 idempotency key는 서로 다른 식별자다.
- byte length, offset과 합계는 signed 64-bit 범위를 안전하게 표현하는
bigint/decimal value object를 사용한다. JSON number나 provider SDK의 좁은 정수로 암묵 변환하지 않고 모든 덧셈·곱셈 overflow를 검사한다.
3.1 ObjectByteStorePort
ObjectByteStorePort
capabilities() -> Result<ObjectStoreCapabilities>
inspect(locator, context)
-> Result<ProviderObjectMetadata | NOT_FOUND>
openRead(
locator,
expectedVersion?,
range?,
context
) -> Result<OpenedObjectRead>
putAtomic(
newLocator,
expectedLength,
expectedDigest?,
mediaType,
byteStream,
context
) -> Result<ProviderWriteReceipt>
deleteExact(
locator,
expectedVersion,
context
) -> Result<DeleteReceipt>
OpenedObjectRead는 다음을 제공한다.
OpenedObjectRead
providerVersion
declaredByteLength
mediaType
contentRange?
stream: bounded backpressure byte stream
close()
필수 규칙:
- read stream은 첫 실패 이후 byte를 더 전달하지 않는다.
- EOF 전에 declared length를 초과하거나 미달하면
INTEGRITY_FAILED다. - range는 normalized inclusive/exclusive 의미를 port에서 하나로 고정한다.
- empty object와 empty range를 구분한다.
- conditional version mismatch는
CONFLICT이지 임의의NOT_FOUND가 아니다. putAtomic성공은 provider가 새 object/version을 durable하게 확정한 뒤만 반환한다.- overwrite를 기본 허용하지 않는다. 새 locator 또는 exact version CAS만 허용한다.
deleteExact의 late response loss는 inspect/reconcile로 결정하며 blind retry하지 않는다.
3.2 MultipartStagingPort
MultipartStagingPort
create(
newLocator,
mediaType,
providerConstraints,
context
) -> Result<ProviderMultipartRef>
putPart(
multipartRef,
partNumber,
exactOffset,
expectedLength,
expectedDigest,
byteStream,
context
) -> Result<ProviderPartReceipt>
inspectParts(
multipartRef,
context
) -> Result<ProviderPartSet>
complete(
multipartRef,
orderedExactPartReceipts,
context
) -> Result<ProviderWriteReceipt>
abort(
multipartRef,
context
) -> Result<AbortReceipt>
ProviderMultipartRef, provider part ETag와 raw upload ID는 adapter-private
branded/opaque 값이다. frontend가 받는 UploadSessionRef와 동일하지 않다.
필수 규칙:
- part number는 연속성·범위·중복을 server ledger가 검증한다.
- part receipt는 multipartRef, part number, length, digest와 provider version에 binding한다.
- provider ETag를 SHA-256으로 해석하지 않는다.
complete는 server ledger가 승인한 ordered receipt만 사용한다.complete성공 후에도 object는QUARANTINED다.abort는 idempotent하며 already-aborted를 성공으로 재생할 수 있다.- complete가 먼저 commit됐다면 abort는 완료 object를 삭제하지 않고
CONFLICT를 반환한다. - 실패한 create/put/complete는 provider multipart orphan을 남길 수 있으므로 bounded reconciliation 대상이 된다.
3.3 ObjectRegistryPort
ObjectRegistryPort
reserveObject(bindingDraft, expectedAbsent, context) -> Result<ObjectReservation>
getObject(objectRef, context) -> Result<ObjectBinding | NOT_FOUND>
transitionObject(objectRef, generation, expectedState, nextState, context)
-> Result<ObjectBinding>
createSession(sessionSnapshot, context) -> Result<UploadSessionRecord>
getSession(sessionRef, context) -> Result<UploadSessionRecord | NOT_FOUND>
appendPart(sessionRef, expectedRevision, partRecord, context)
-> Result<UploadSessionRecord>
transitionSession(sessionRef, expectedRevision, expectedState, nextState, context)
-> Result<UploadSessionRecord>
listExpiredOpenSessions(cursor, maxRows, context) -> Result<SessionPage>
listReconciliationCandidates(cursor, maxRows, context) -> Result<ObjectPage>
registry transaction이 object provider transaction과 원자적이라고 가정하지 않는다. 모든 cross-system mutation은 durable phase와 reconciliation을 갖는 saga다.
3.4 SignedTransferCapabilityPort
SignedTransferCapabilityPort
issueUploadPart(
subjectScope,
sessionRef,
providerLocator,
partNumber,
exactLength,
requiredDigest,
requiredHeaders,
expiresAt,
context
) -> Result<SignedTransferCapability>
issueDownload(
subjectScope,
objectRef,
generation,
providerLocator,
allowedRange?,
responseMetadata,
expiresAt,
context
) -> Result<SignedTransferCapability>
SignedTransferCapability
receipt
href
method
requiredHeaders
objectRef
generation/sessionRef
exactLengthOrMaximum
expectedDigest?
mediaType
safeExtension?
expiresAt
capability는 이미 authorization된 control-plane 결과다. signer port가 업무 권한을 판단하지 않는다.
3.5 IdempotencyStorePort
IdempotencyStorePort
begin(scope, operation, key, requestFingerprint, leaseUntil, context)
-> Result<ACQUIRED<LeaseAndFence> | IN_PROGRESS | REPLAY<CompletionReceipt>>
renew(leaseAndFence, extendUntil, context)
-> Result<LeaseAndFence>
complete(leaseAndFence, requestFingerprint, completionReceipt, expiresAt, context)
-> Result<CompletionReceipt>
releaseRetryable(
leaseAndFence,
requestFingerprint,
proofSideEffectNotStarted,
context
)
-> Result<void>
pruneExpired(cursor, maxRows, context)
-> Result<PrunePage>
scope + operation + key가 unique key다. 같은 key를 다른 request fingerprint로
재사용하면 CONFLICT다. idempotency record는 provider upload ID, signed URL,
credential, 원본 filename 또는 request body를 저장하지 않는다.
lease expiry 뒤 takeover는 fence를 증가시키고 이전 owner의 renew/complete를
거절한다. 외부 side effect가 시작됐는지 불명확하면 record를 지우거나
releaseRetryable하지 않고 RECOVERY_REQUIRED로 남겨 reconciler가 provider와
registry를 확인한다.
3.6 ContentScannerPort
ContentScannerPort
capabilities() -> Result<ScannerCapabilities>
submit(
scanJobRef,
objectRef,
generation,
immutableDigest,
scanProfileId,
sourceFactory,
context
) -> Result<ScanSubmissionReceipt>
inspect(jobRef, context) -> Result<ScanStatus>
cancel(jobRef, context) -> Result<CancelReceipt>
ScanStatus
PENDING | RUNNING
CLEAN {
objectRef, generation, immutableDigest,
engineProfile, definitionVersion, completedAt
}
REJECTED {
objectRef, generation, immutableDigest,
allowlistedReasonCodes, completedAt
}
FAILED_RETRYABLE { safeReasonCode }
FAILED_TERMINAL { safeReasonCode }
scanner의 raw stdout, path, vendor exception, signature name과 원본 filename은 application response나 telemetry로 전달하지 않는다.
3.7 CleanPromotionPort
검사와 availability commit 권한을 한 adapter에 함께 주지 않는다.
CleanPromotionPort
promote(
objectRef,
expectedGeneration,
expectedQuarantineDigest,
sealedCleanScanReceipt,
idempotency,
context
) -> Result<AvailableObjectReceipt>
reject(
objectRef,
expectedGeneration,
expectedQuarantineDigest,
sealedRejectedScanReceipt,
idempotency,
context
) -> Result<RejectedObjectReceipt>
- scan receipt는 exact object/generation/digest/scan-policy version에 binding된 server-only sealed value다.
- scanner는 registry를
AVAILABLE로 전환할 권한을 갖지 않는다. - promotion adapter는 raw scanner 결과를 해석하지 않고 sealed receipt만 검증한다.
- CDR/sanitized output은 원본과 다른 object generation/digest이므로 새 object로 length/digest를 다시 검증한다.
4. Streaming object read/write
4.1 공통 stream envelope
stream은 다음을 명시적으로 소유한다.
- backpressure와 최대 queued bytes
- immutable declared length 또는
unknown - operation deadline과 cancellation
- 첫 실패에서 terminal close
- byte counter와 digest accumulator
- completion/close truth
- resource lease
전체 object를 하나의 byte array, string, base64 또는 temporary in-memory Blob으로 합치는 API를 공통 경로로 제공하지 않는다. small-object 편의 API가 필요하면 composition-owned hard cap 아래 별도 wrapper로만 제공한다.
4.2 proxy upload
request body
-> transport maximum-body guard
-> decoded stream
-> authoritative byte counter
-> digest accumulator
-> provider write stream
-> provider commit
-> registry phase transition
Content-Length는 preflight hint일 뿐 실제 count를 대체하지 않는다.- chunked transfer나 HTTP/2/3에서도 실제 decoded payload를 센다.
- max bytes를 넘는 즉시 upstream read와 provider write를 모두 cancel한다.
- provider commit 전에 client disconnect가 발생하면 staging을 abort/cleanup한다.
- provider commit 뒤 client response가 유실되면 idempotency replay가 같은 receipt를 반환해야 한다.
4.3 proxy download
authorized immutable ObjectBinding
-> exact provider version open
-> byte/digest/range verifier
-> response stream
-> close/cancel
- authorization은 open 전에 완료한다.
AVAILABLE과 exact generation을 다시 확인한다.- metadata preflight와 stream open이 분리된 provider에서는 둘 다 같은 immutable
provider version에 pin하고
If-Match와 동등한 조건을 강제한다. version pin을 지원하지 않으면 HEAD 뒤 GET 같은 TOCTOU 경로를 사용하지 않는다. - response header를 확정하기 전에 media type, length, range와 filename을 normalize한다.
- range response는 exact provider version, normalized range와 실제 반환 byte count를 검증한다. whole-object SHA-256만으로 부분 range의 무결성을 검증했다고 주장하지 않는다.
- 검증 가능한 range가 필요하면 immutable chunk digest/Merkle manifest를 별도로
설계하고 exact chunk proof를 검증한다. 그렇지 않은 range는 transport integrity와
version/range binding만 보장하며
digestVerified=false로 명시한다. Content-Dispositionfilename은 advisory metadata로 sanitize한다.- sensitive object는 기본적으로 attachment,
nosniff, private/no-store 정책을 사용한다. - downstream disconnect는 provider read를 cancel한다.
- download 완료 metric은 server stream 종료이지 사용자 disk 저장 완료가 아니다.
4.4 direct provider transfer
signed URL을 사용하는 direct transfer에서는 server가 byte stream을 직접 보지 못한다. 그러므로 다음을 모두 만족해야 한다.
- capability가 method, exact provider locator, part/range, length, checksum header, expiry와 subject/session scope에 binding
- provider가 해당 조건을 실제 request에서 강제
- complete 전에 server가 provider part/object metadata를 authoritative하게 재조회
- provider metadata가 충분하지 않으면 quarantined object를 server-side stream으로 다시 읽어 length와 whole-object digest 검증
- verification 완료 전 registry 상태를
AVAILABLE로 전환하지 않음
provider가 검증하지 않는 client-declared metadata를 signed request에 포함했다는 이유만으로 integrity를 주장하지 않는다.
5. Multipart protocol
5.1 control-plane 상태 흐름
frontend reference runtime과 연결하는 ordered multipart wire protocol literal은
PRESIGNED_MULTIPART_V1이다. create/status/part capability/complete/abort의
application contract, session response와 durable checkpoint 전체에서 이 protocol
값을 exact하게 검증한다. UPLOAD_PART capability binding 자체에도 protocol을
포함하고, part capability endpoint는 sessionId로 조회한 server-side session과
이 값이 일치하는지 확인해야 한다.
create
authorize + policy snapshot + idempotency begin
reserve ObjectRef/generation
create provider multipart staging
persist OPEN session
return session constraints
put part
reauthorize session scope
validate OPEN + expiry + part plan
claim part idempotency
stream or issue exact signed part capability
persist verified ProviderPartReceipt by session revision CAS
complete
reauthorize
idempotency begin
CAS OPEN -> COMPLETE_REQUESTED
validate ordered complete part set
provider complete
persist PROVIDER_COMMITTED with exact provider generation
verify final length/digest
atomically persist object QUARANTINED + scan outbox
persist QUARANTINE_RECORDED -> QUARANTINED
return stable quarantined receipt
abort
reauthorize
idempotency begin
CAS OPEN -> ABORT_REQUESTED
provider abort/cleanup
persist ABORTED
5.2 create
server가 결정하고 snapshot해야 하는 값:
- object/session opaque reference
- max object bytes
- min/max part bytes와 max part count
- max client concurrency
- required checksum algorithm
- exact session expiry
- scan profile
- direct/proxy transfer mode
client가 bucket/key, provider upload ID, concurrency ceiling, checksum algorithm이나 expiry를 선택하지 않는다.
5.3 part
- client가 보낸
requestBindingSha256,uploadBindingSha256와 part digest를 authorization proof로 취급하지 않는다.sessionId로 server-owned session을 조회하고 subject/purpose/state/expiry를 재검증한 다음 canonical binding과 part plan을 직접 재계산한다. partNumber, exact offset, expected length를 session plan과 대조한다.- 같은 idempotency key와 같은 fingerprint 재전송은 같은 receipt를 반환한다.
- 같은 part number의 다른 length/digest는
CONFLICT다. - concurrent upload의 aggregate in-flight bytes와 provider connections에 hard cap을 둔다.
- 마지막 part를 제외한 최소 part 크기는 provider와 platform ceiling을 모두 만족해야 한다.
- part 성공 response가 유실되면 provider inspect와 ledger reconcile로 결정한다.
5.4 complete
- client가 제출한 receipt 목록을 그대로 provider에 전달하지 않는다.
- server ledger의 exact verified part set과 ordered fingerprint를 다시 계산한다.
- part count, offset 연속성, byte sum과 중복/누락을 검증한다.
- provider complete의 ETag를 whole-object SHA-256으로 해석하지 않는다.
- provider complete 직후 exact provider locator/version을
PROVIDER_COMMITTED로 먼저 기록한다. 이 checkpoint 전후 crash와 응답 유실은 provider inspect + journal reconcile로 결정한다. - object metadata와 scan outbox가 durable해진 뒤
QUARANTINE_RECORDED를 기록한다. - complete 성공은
QUARANTINEDobject가 durable하다는 의미이며 clean/available을 의미하지 않는다.
5.5 abort, expiry와 orphan cleanup
abort와 expiry job은 같은 state CAS를 사용한다.
| 현재 상태 | complete | abort/expiry |
|---|---|---|
OPEN |
COMPLETE_REQUESTED claim 가능 |
ABORT_REQUESTED claim 가능 |
COMPLETE_REQUESTED |
resume/reconcile | object 삭제 금지, conflict |
PROVIDER_COMMITTED |
verify/resume | object 삭제 금지, conflict |
QUARANTINE_RECORDED |
outbox/session resume | 일반 abort 금지 |
QUARANTINED |
stable replay | 일반 abort 금지 |
ABORT_REQUESTED/ABORTED/EXPIRED |
conflict/expired | stable replay |
orphan janitor는 다음을 bounded page로 처리한다.
- registry OPEN이지만 session TTL이 지난 행
- registry에 provider multipart ref가 있으나 terminal state가 아닌 행
- provider staging은 있으나 registry binding이 없는 owned orphan
- provider complete 가능성이 있으나 response가 유실된
COMPLETE_REQUESTED - provider object는 durable하지만 registry/outbox가 미완료인
PROVIDER_COMMITTED/QUARANTINE_RECORDED DELETING에서 provider delete response가 유실된 object
전체 bucket list/delete를 자동 실행하지 않는다. owned prefix/tag와 registry binding이 동시에 확인된 대상만 정리한다.
6. Byte length와 checksum
6.1 authoritative source
| 값 | 신뢰 수준 |
|---|---|
client Content-Length |
preflight hint |
| client checksum | expected value, 단독 authority 아님 |
| provider ETag | opaque provider version |
| provider checksum field | contract test를 통과한 알고리즘/representation에서만 사용 |
| server streaming counter | proxy path의 authoritative length |
| server digest accumulator | proxy path의 authoritative digest |
| server post-complete readback | direct path의 authoritative fallback |
6.2 알고리즘
- 기본 whole-object algorithm은 SHA-256으로 제한한다.
- algorithm confusion을 막기 위해 digest에 algorithm tag를 항상 포함한다.
- lowercase/uppercase나 base64/hex encoding을 port 하나로 canonicalize한다.
- multipart part digest와 whole-object digest를 구분한다.
- OPFS의
SHA-256-TREE-V1, multipart ETag와 whole-object SHA-256은 서로 다른 digest다. - digest가 맞아도 MIME/content safety가 증명되는 것은 아니다.
6.3 mismatch
다음 경우 provider commit 또는 availability promotion을 금지한다.
- 실제 byte length가 expected length와 다름
- object가 composition hard cap을 초과
- part byte sum이 whole-object length와 다름
- expected digest와 actual digest가 다름
- provider metadata와 server ledger가 다름
- digest algorithm 또는 encoding이 policy와 다름
이미 direct upload가 provider에 commit된 뒤 mismatch가 발견되면 object를
REJECTED 또는 cleanup-pending quarantine으로 유지하고 일반 read capability를
발급하지 않는다.
7. Short-lived signed capability
7.1 capability binding
capability 또는 그 server-side receipt는 최소 다음을 binding한다.
- issuer와 audience
- subject/session scope
- operation: upload part, object download 또는 authorized range
ObjectRef + generation또는UploadSessionRef + partNumber- exact HTTP method
- exact provider locator
- exact length 또는 server-enforced maximum
- required checksum와 signed headers
- upload 성공의 exact status, receipt response header와 expected response bytes
- media type과 safe extension
- issued-at/not-before/expiry
- capability policy version
- random receipt/nonce
값 하나라도 caller request, object registry와 다르면 발급 또는 handoff를 fail-closed한다.
7.2 lifetime
- session TTL과 per-request capability TTL을 분리한다.
- capability TTL은 composition이 정하고 implementation hard ceiling보다 낮출 수만 있다.
- 대용량 transfer 시간과 재발급 UX를 측정해 값을 선택한다.
- 만료 capability를 연장하지 않고 authorization 후 새 capability를 발급한다.
- proxy 경로는 stream admission 시 expiry를 검증하고, admission 뒤에는 별도의 bounded operation deadline과 maximum transfer duration을 적용한다. expiry가 지났다는 이유만으로 이미 허용된 stream을 임의의 시점에 자를지는 정책으로 명시하며 기본값은 새 요청/재시도만 거절하는 것이다.
- direct provider 경로는 provider가 “시작 시 유효”와 “전송 내내 유효” 중 어떤 의미를 실제로 강제하는지 contract test로 고정한다. 중간 만료의 강한 회수가 필요하면 provider URL이 아니라 proxy/relay 경로를 사용한다.
- signing key rotation 시 current/previous verification window와 강제 폐기 절차를 정의한다.
문서 출발점으로는 per-request capability를 수분 단위로 유지하되, 실제 값은 provider와 최대 part/object 크기의 production-like transfer evidence로 승인한다. 장기 URL을 session 전체와 동일하게 발급하지 않는다.
7.3 single-use의 한계
object store signed URL은 일반적으로 URL 자체만으로 single-use를 보장하지 않는다. 정확한 single-use가 요구되면 다음 중 하나를 선택한다.
- BFF proxy가 nonce를 원자 consume한 뒤 stream
- control plane receipt를 원자 consume하고 아주 짧은 provider capability 발급
- provider가 지원하는 조건부 write/version 정책과 server ledger를 결합
single-use를 구현하지 않았으면 문서나 API 이름으로 주장하지 않는다.
7.4 URL과 logging
- HTTPS 외 protocol은 local test 외 금지
- allowlisted provider/origin만 허용
- redirect는 기본 금지
- query의 signature/token을 log, analytics, trace attribute에 기록하지 않음
Referer와 browser history 노출을 고려한 delivery 정책- response header와 CORS expose 목록을 explicit하게 고정
- signed URL을 database의 장기 object locator로 저장하지 않음
8. Idempotency store
8.1 scope와 fingerprint
idempotency는 다음 연산에 기본 적용한다.
- upload session create
- part registration/stream upload
- multipart complete
- abort
- scan submission
- availability promotion
- delete
requestFingerprint는 operation별 canonical request의 SHA-256이다. 원문 payload,
filename, signed URL, token과 PII를 fingerprint input/record에 넣지 않는다.
업무 payload가 필요한 미래 use case는 feature-owned canonicalization을 추가한다.
fingerprint는 registered canonicalizer만 만들며 operation/version, exact
object/session generation, length, digest와 relevant precondition을 포함한다.
deadline, trace/request ID 같은 volatile 값은 제외한다.
8.2 concurrency
begin은 하나의 atomic operation이어야 한다.
first caller -> ACQUIRED + lease/fence
same fingerprint -> IN_PROGRESS 또는 completed REPLAY
different fingerprint -> CONFLICT
- process-local lock만으로 correctness를 주장하지 않는다.
- lease owner가 죽으면 expiry 이후 같은 fingerprint만 reclaim할 수 있다.
- reclaim은 fence를 증가시키며 old owner의 complete를 거절한다.
- complete는 expected lease/fence와 request fingerprint를 다시 확인한다.
- durable side effect와 idempotency complete 사이 crash는 reconciliation 가능한 operation receipt로 해결한다.
- “exactly once”를 주장하지 않고 at-least-once delivery + idempotent effect로 설계한다.
- exactly-once가 실제로 필요하면 업무 mutation과 receipt를 같은 transaction에
넣거나 transactional inbox/outbox로 묶어야 한다. 별도 Redis
SETNX뒤 database/object mutation을 실행하는 구조는 duplicate suppression일 뿐이다. - idempotency store가 unavailable한 keyed mutation은 fail-closed한다.
8.3 receipt와 retention
completion receipt에는 다음처럼 재생에 필요한 최소 정보만 둔다.
CompletionReceipt
operation
stableResultCode
objectRef/sessionRef
generation/revision
safeResponseFingerprint
completedAt
- replay response는 최초 성공과 의미가 같아야 한다.
- transient provider error 전체를 영구 replay하지 않는다.
- retention은 최대 client retry/session window를 포함하되 무제한이 아니다.
- row/byte hard cap, expiry index와 bounded prune를 필수로 둔다.
- 아직 replay 가능한 receipt를 storage pressure만으로 삭제하지 않는다.
9. Quarantine과 scan
9.1 격리
- staging/quarantine object는 public ACL과 CDN 배포를 금지한다.
- 일반 download capability issuer가
QUARANTINED를 읽지 못하게 한다. - scanner principal은 exact quarantine read와 verdict write만 가진다.
- clean destination writer와 destructive delete 권한을 최소화한다.
- 원본 filename으로 provider path를 만들지 않는다.
9.2 검사 pipeline
QUARANTINED object + exact generation/digest
-> durable scan outbox
-> scanner/validator
malware
MIME sniff + allowlisted parser
archive traversal/symlink/nesting/expanded-size ratio
image dimension/pixel budget
PDF/active content
optional CDR
-> bound verdict
-> CAS promotion or rejection
scanner가 clean이라고 반환해도 submit 당시와 object generation/digest가 다르면 stale verdict로 폐기한다.
9.3 promotion
promotion은 provider별로 다음 중 하나다.
- immutable quarantine object를 그대로 유지하고 registry access state만
AVAILABLE로 CAS - clean bucket/key로 server-side copy 후 length/digest/version을 재검증하고 binding CAS
copy+delete를 atomic rename으로 주장하지 않는다. crash 단계마다 source/destination binding을 재검증하는 saga와 cleanup phase가 필요하다. availability가 commit되기 전 source를 삭제하지 않는다.
9.4 scanner 장애
- required scanner unavailable은 clean으로 degrade하지 않는다.
- retryable failure는 bounded backoff와 retry count/age ceiling을 가진다.
- terminal failure는 quarantine을 유지하고 operator/user recovery를 요구한다.
- scan process/container에는 wall-clock, CPU, memory, file count, recursion depth, expanded bytes와 output bytes hard limit를 둔다.
- scan definition/profile freshness가 composition의 maximum age를 넘으면 이전
CLEANverdict로 promotion하지 않고 새 job/generation fence로 재검사한다. - scan backlog가 SLO를 초과하면 신규 session 발급을 제한하거나 kill switch를 사용한다.
- scan timeout 이후에도 late verdict가 state를 바꾸지 못하게 generation/job fence를 검증한다.
10. Deadline, retry와 resource cleanup
10.1 deadline budget
각 public capability invocation은 절대 deadline 또는 남은 budget을 받는다.
request deadline
- authorization
- registry transaction
- provider call
- stream transfer
- verification
- response margin
하위 adapter가 각자 전체 timeout을 새로 시작해 총 시간이 무한히 늘어나서는 안
된다. 남은 budget이 최소 provider timeout보다 작으면 side effect 전에
DEADLINE_EXCEEDED로 종료한다. caller cancellation은 CANCELLED이고 server
deadline 소진은 DEADLINE_EXCEEDED다. 둘은 metric과 retry 판단에서도 합치지
않는다.
10.2 retry matrix
공통 retry directive는 세 종류뿐이다.
NEVER
SAFE { afterMs? }
SAME_IDEMPOTENCY_KEY { afterMs? }
| operation | directive | 조건 |
|---|---|---|
| immutable metadata/read open | SAFE |
같은 exact version과 남은 deadline |
| range read | SAFE |
같은 exact version/range와 아직 전달되지 않은 경계 |
| session create | SAME_IDEMPOTENCY_KEY |
같은 canonical fingerprint |
| put part | SAME_IDEMPOTENCY_KEY |
같은 key/part/length/digest |
| multipart complete | SAME_IDEMPOTENCY_KEY |
stable receipt + inspect/reconcile |
| abort/delete | SAME_IDEMPOTENCY_KEY |
exact state/version + inspect/reconcile |
| signed capability issue | NEVER |
authorization/expiry 확인 후 새 operation으로 발급 |
| scan submit | SAME_IDEMPOTENCY_KEY |
stable job ref + generation/digest binding |
retry는 exponential backoff, full jitter, max attempts와 전체 deadline을 가진다.
rate limit/provider overload에서는 Retry-After 또는 provider-safe backoff hint를
상한 안에서 반영한다.
- retry owner는 ingress/application orchestration, adapter wrapper 또는 provider SDK 중 정확히 한 계층이다. service mesh와 SDK의 숨은 retry는 끄거나 동일한 total-attempt budget에 포함해 retry amplification을 막는다.
- mutation의 적용 여부가
UNKNOWN이면 새 idempotency key로 재시도하지 않는다. 같은 key로 receipt를 조회하거나 provider/registry reconciliation을 먼저 한다. - 이미 response byte를 client에 전달한 stream read는 처음부터 자동 재시작하지 않는다. resumable protocol이 명시된 경우에만 검증된 다음 range에서 재개한다.
10.3 cleanup
모든 adapter는 다음 자원을 명시적으로 종료한다.
- input/output stream과 provider response body
- multipart writer/upload handle
- temporary file와 bounded buffer
- digest/scanner process stream
- database cursor/transaction/connection lease
- scheduled timeout/retry task
- lock/semaphore permit
- tracing span
resource owner는 다음과 같은 idempotent lease 계약을 구현한다.
ResourceLease
state: OPEN | CLOSING | CLOSED
transferOwnership(newOwner)
close(reason, cleanupDeadline) -> CleanupReport
- ownership transfer는 명시적이며 transfer 뒤 이전 owner는 close하지 않는다.
close는 여러 번 호출돼도 안전하고 첫 close reason과 cleanup 결과를 보존한다.- request deadline과 별도로 짧고 bounded한 cleanup budget을 예약한다. 이 budget은 client response deadline을 연장하지 않으며, 즉시 끝낼 수 없는 provider cleanup은 durable reconciliation record로 넘긴다.
- finalizer/garbage collector는 correctness 경로가 아니라 마지막 누수 경보다.
success, failure, cancellation, timeout, downstream disconnect와 exception 모든 경로를 contract test한다. cleanup 자체의 실패가 최초 failure를 덮지 않으며 safe secondary observation만 남긴다.
10.4 shutdown
- 신규 session/capability 발급 중지
- in-flight admission 중지
- bounded grace 동안 active stream drain
- 남은 stream cancel
- leased idempotency operation을 reclaim 가능 상태로 둠
- multipart/scanner reconciliation checkpoint 저장
- provider clients/executors close
무기한 graceful shutdown을 허용하지 않는다.
11. 공통 failure model
11.1 closed failure
FileCapabilityFailure
code
operation
retry: NEVER
| SAFE { afterMs? }
| SAME_IDEMPOTENCY_KEY { afterMs? }
effect: NOT_APPLIED | APPLIED | UNKNOWN
recovery
safeReasonCode?
correlationId
권장 closed code:
INVALID_INPUT
UNAUTHENTICATED
FORBIDDEN
NOT_FOUND
CONFLICT
POLICY_REJECTED
LIMIT_EXCEEDED
PAYLOAD_TOO_LARGE
UNSUPPORTED_MEDIA_TYPE
INTEGRITY_FAILED
EXPIRED_RESOURCE
QUARANTINED
REJECTED
RATE_LIMITED
IDEMPOTENCY_KEY_REUSED
OPERATION_IN_PROGRESS
CANCELLED
DEADLINE_EXCEEDED
DEPENDENCY_UNAVAILABLE
CONTRACT_MISMATCH
RECOVERY_REQUIRED
CORRUPT_DATA
INTERNAL
권장 operation:
OBJECT_INSPECT
OBJECT_READ
OBJECT_WRITE
OBJECT_DELETE
MULTIPART_CREATE
MULTIPART_PART
MULTIPART_COMPLETE
MULTIPART_ABORT
CAPABILITY_ISSUE
IDEMPOTENCY
SCAN_SUBMIT
SCAN_INSPECT
OBJECT_PROMOTE
RECONCILE
recovery는 RETRY, REAUTHORIZE, RESTART_SESSION, REOPEN,
READ_ONLY, SUPPORT, NONE 같은 allowlist다.
NOT_APPLIED는 side effect가 시작되지 않았음이 증명된 경우만 사용한다.APPLIED는 durable receipt로 effect를 증명할 수 있는 경우다.- response loss, provider timeout 또는 process crash로 확정할 수 없으면
UNKNOWN이며, caller에게 성공이나 안전한 신규 요청을 암시하지 않는다. SAFE는 read 또는 side effect 전 실패에만 사용한다.SAME_IDEMPOTENCY_KEY는 key/fingerprint/receipt 계약이 갖춰진 mutation에만 사용한다. 단순retryable: true는 허용하지 않는다.
11.2 mapping
- provider status/exception class를 application failure로 한 곳에서 mapping한다.
- raw SDK exception, request ID, bucket/key, endpoint와 provider body를 port 밖으로 throw하지 않는다.
- unknown provider failure는
INTERNAL또는DEPENDENCY_UNAVAILABLE중 사전에 정한 fail-closed mapping을 사용하고 effect는 보수적으로UNKNOWN으로 둔다. - authorization과 object existence를 노출하면 안 되는 API는
FORBIDDEN과NOT_FOUND외부 표현을 동일하게 만들 수 있다. - HTTP status는 inbound adapter가 failure code에서 mapping하며 domain/application이 HTTP status를 반환하지 않는다.
- retry directive와 effect certainty는 provider message 문자열이 아니라 typed mapping, operation semantics, idempotency receipt와 reconciliation evidence로 결정한다.
11.3 safe observability
다음을 기록하지 않는다.
- object/session/idempotency reference 원문
- filename과 user/account ID
- provider locator, bucket/key/version
- signed URL/query/header
- checksum 원문
- scanner raw result
- request/response body와 exception message/stack
허용 가능한 metric dimension:
- provider profile ID
- operation
- stable failure code
- byte/part/latency bucket
- transfer mode proxy/direct
- state transition
- scan profile와 safe verdict class
- retry count bucket
- retry directive와 effect certainty
high-cardinality identifier를 metric label로 사용하지 않는다.
12. Provider contract test
12.1 한 suite, 여러 adapter
각 port는 provider-neutral contract suite factory를 제공한다.
objectByteStoreContract(createProviderFixture)
multipartStagingContract(createProviderFixture)
signedCapabilityContract(createProviderFixture)
rangeDownloadContract(createProviderFixture)
idempotencyStoreContract(createProviderFixture)
contentScannerContract(createProviderFixture)
objectRegistryContract(createProviderFixture)
cleanPromotionContract(createProviderFixture)
imageDescriptorContract(createProviderFixture)
동일 suite를 in-memory fake, emulator와 실제 provider adapter에 실행하되 결과 등급을 섞지 않는다.
- in-memory fake: orchestration 개발과 빠른 invariant 회귀
- emulator/container: SDK wiring과 local integration
- actual provider conformance: 선택한 provider product/API/version의 격리된 production-like account/region에서 실행한 promotion evidence
fake나 emulator 통과는 actual provider conformance를 대체하지 않는다. actual provider에서 destructive/fault test를 실행할 수 없다면 누락 항목, 보완 통제, 승인 owner와 expiry가 있는 명시적 waiver가 필요하며 자동으로 “동등”하다고 간주하지 않는다.
12.2 object store matrix
- zero-byte와 boundary-size object
- exact bytes/media/version round trip
- range beginning/middle/end/invalid/empty
- declared length 미달·초과
- checksum mismatch
- provider metadata/ETag와 digest 구분
- mid-stream read/write failure
- cancellation/downstream disconnect
- deadline before call/during stream/after provider commit
- concurrent exact-version write/delete
- response loss 뒤 inspect/reconcile
- resource close exactly once
- object locator escaping/path traversal 거절
12.3 multipart matrix
- create/put/inspect/complete 정상 흐름
- minimum/maximum part size와 count
- duplicate part same fingerprint replay
- duplicate part different fingerprint conflict
- missing/duplicate/out-of-order receipt
- complete/abort/expiry races
- complete response loss와 recovery
- part success response loss와 inspect
- orphan multipart cleanup
- provider complete ETag가 whole SHA-256이 아님을 검증
- direct signed PUT의 required length/checksum header 강제
12.4 idempotency matrix
- 동시 100개 begin에서 정확히 하나만
STARTED - 같은 fingerprint의
IN_PROGRESS와 stable replay - 다른 fingerprint conflict
- lease expiry/reclaim
- crash between durable effect and complete
- retryable release
- receipt expiry와 bounded prune
- count/byte cap
- tenant/scope/operation key isolation
- clock skew/invalid expiry fail-closed
12.5 capability matrix
- wrong method/object/part/range/header 거절
- expiry/not-before
- TTL hard ceiling
- modified query/path/host 거절
- wrong audience/subject/session
- key rotation current/previous/expired
- redirect/CORS/header exposure policy
- token/query redaction
- direct provider가 signed constraint를 실제로 강제하는지 확인
12.6 scanner matrix
- clean/rejected/timeout/unavailable
- corrupt/truncated input
- stale generation/digest verdict 폐기
- duplicate submission replay
- archive path traversal/nesting/expanded-size limit
- scanner crash와 process/resource cleanup
- late verdict after cancellation/timeout
- scan backlog admission control
- clean verdict 전 download/promotion 불가
12.7 Range download matrix
- exact
RANGE_RESUMABLE_DOWNLOAD_V1과 unknown/missing version 거절 - immutable generation/strong validator/total length/media/full digest binding
- beginning/middle/final segment의 exact
206 Content-Range If-Rangematch와 mismatch의206/full200- 별도 generation precondition의
412 - before-start/at-end/beyond-end
416과 authoritative total - capability expiry/reissue의 same-binding 유지
- generation replacement 뒤 old partial 이어 쓰기 거절
- encoded/transform response와 redirect 거절
- direct provider와 BFF relay가 같은 constraint를 실제로 강제
12.8 Image descriptor/CDN matrix
- exact
IMAGE_CDN_DESCRIPTOR_V1과 unknown/missing field/version 거절 - authorization/existence hiding/quarantine 상태
- immutable asset revision과 preset binding exact recomputation
- arbitrary source/transform/query 거절
- old/new key overlap, signer cutover와 old key drain
- expiry/reissue/emergency revoke
- public immutable/private no-store cache header
- cross-origin CORS/CSP와 ambient credential 비의존
- malformed/animated/oversize rendition 거절
- asset/preset mismatch와 provider response loss
12.9 fault injection과 evidence
- provider latency, throttle, connection reset, partial response
- database deadlock/serialization retry
- registry commit 전후 process termination
- provider commit 뒤 response loss
- queue duplicate/out-of-order
- scanner unavailable와 slow verdict
- clock movement은 wall clock/monotonic clock 책임에 맞춰 주입
CI가 만드는 immutable conformance evidence에는 최소 다음을 포함한다.
ProviderConformanceEvidence
adapterName/version/artifactDigest
providerProduct/apiVersion/runtimeVersion
environmentClass/region
capabilityProfileDigest
contractSuiteName/version/artifactDigest
startedAt/completedAt
passed/failed/skipped counts
faultSuiteResult
waiverIds[]
evidenceExpiresAt
ciRunIdentity/signature
실제 provider evidence와 fake/emulator result는 별도 artifact로 보관한다. 필수 test skip, expired evidence, adapter/provider/config 변경 또는 contract suite version 불일치는 promotion을 막는다. 최소 정기 schedule과 provider/SDK upgrade, capability/config 변경 시 actual provider conformance를 다시 실행한다.
13. 선택적 composition
13.1 서로 독립적인 상태 축
source catalog 상태, runtime 설치, 현재 health와 traffic admission을 하나의
ENABLED boolean으로 합치지 않는다.
InstallationState
NOT_SELECTED | INSTALLED | REMOVING
RuntimeState
UNKNOWN | AVAILABLE | DEGRADED | UNAVAILABLE | INCOMPATIBLE
TrafficAdmission
DISABLED | SHADOW | CANARY | ENABLED
이 문서의 목표인 AVAILABLE_NOT_COMPOSED는 source catalog에 port, adapter,
contract suite와 문서가 있지만 runtime은 NOT_SELECTED, traffic은 DISABLED인
상태다. provider client/route/job/migration을 만들지 않으므로 RuntimeState도
평가하지 않는다.
INSTALLED는 dependency와 immutable policy가 composition되었다는 뜻일 뿐,
provider가 현재 AVAILABLE하거나 traffic이 ENABLED라는 뜻이 아니다.
INCOMPATIBLE은 contract/API/capability 불일치이며 fail-closed한다.
13.2 composition root
선택 시 composition root만 concrete provider를 안다.
ServerFileCapabilityComposition
objectByteStore
multipartStaging
objectRegistry
idempotencyStore
signedTransferCapability
contentScanner
cleanPromotion
clock
secureRandom
deadlinePolicy
retryPolicy
transferPolicyRegistry
safeObserver
composition 시 다음을 검증하고 immutable snapshot으로 고정한다.
- provider capability와 요구 기능 일치
- hard byte/part/TTL/retry/deadline ceiling
- registry와 provider profile binding
- signing key/audience
- scanner profile
- cleanup owner와 schedule
- idempotency retention/capacity
- metric/trace redaction policy
선택된 필수 dependency가 없거나 capability가 부족하면 startup 또는 feature installation을 fail-closed한다. unavailable provider를 fake로 바꾸어 production을 계속하지 않는다.
13.3 capability probe와 readiness
probe는 다음 판단을 분리해 기록한다.
| probe | 답하는 질문 |
|---|---|
| static config | 필수 값, hard ceiling과 profile schema가 유효한가 |
| connectivity | DNS/TLS/network endpoint에 bounded하게 도달하는가 |
| credential/entitlement | 최소 권한 principal이 필요한 operation을 허용받는가 |
| compatibility | provider API/version/capability가 승인 contract와 일치하는가 |
| operational health | 현재 latency/error/throttle가 admission SLO 안인가 |
- probe는 짧은 timeout, cancellation과 bounded retry를 사용하고 동시 요청은 single-flight로 합친다.
- cached result에는 TTL와 jitter를 두며
state, observedAt, validUntil, adapterVersion, contractVersion, safeReasonCode외 token, endpoint body, locator와 credential을 포함하지 않는다. - probe를 매 product request의 authorization 또는 correctness check로 사용하지 않는다. request는 여전히 exact registry state, generation과 capability를 검증한다.
- last-known-good는 짧은 grace의 운영 신호일 뿐 expiry 뒤 authority가 아니다.
stale result는
UNKNOWN으로 낮춘다. - read/write probe가 side effect를 만들면 별도의 owned canary namespace와 exact cleanup receipt를 사용한다. 일반 customer object를 probe하지 않는다.
DISABLED/SHADOW인 optional capability 장애는 base server readiness를 깨지 않는다.ENABLED이고 해당 제품 경로의 필수 dependency이면 feature readiness와 admission을 fail-closed한다. boot-blocking 여부는 composition policy로 명시한다.
INSTALLED -> CANARY promotion에는 유효한 actual-provider conformance evidence,
owner, policy snapshot, SLO/alert, runbook, cleanup drill과 rollback/kill switch가
필요하다.
13.4 미선택 상태
AVAILABLE_NOT_COMPOSED에서는 다음을 금지한다.
- public HTTP route/controller registration
- provider SDK client 초기화와 credential 요구
- database table/migration 자동 생성
- bucket/container 생성
- cleanup/scan worker와 scheduler 시작
- health/readiness 필수 dependency 등록
- product module이 concrete adapter를 직접 import
- provider endpoint로의 network/DNS 호출과 background capability probe
server template core는 file capability 디렉터리를 완전히 제거해도 build, base test, application startup과 artifact 생성이 통과해야 한다.
13.5 rollout
- 제품 use case와 owner 결정
- 분류, size, retention, scan, fallback 정책 승인
- provider와 deployment profile 선택
- actual provider contract/fault test와 evidence 생성
INSTALLED + UNKNOWN + DISABLED로 composition- bounded operator probe 후
SHADOW - 내부 cohort proxy transfer를
CANARY - direct signed transfer가 필요하면 별도 canary
- quarantine/scan backlog, reconciliation과 cleanup drill
- SLO/alert/kill switch/rollback 확인 후
ENABLED로 점진 확대
rollout 중 evidence expiry, INCOMPATIBLE, integrity failure 또는 cleanup 불능은
자동 promotion을 중단한다. rollback은 code rollback뿐 아니라
TrafficAdmission=DISABLED, credential revoke와 worker drain 절차를 포함한다.
13.6 kill switch
- 신규 upload session 발급 off
- direct signed upload off → bounded proxy 또는 전체 off
- multipart off → 승인된 small simple upload
- 신규 download capability off
- scanner promotion off, quarantine 유지
- provider write off, read-only
- background reconciliation batch 축소/정지
kill switch가 기존 object를 자동 삭제하거나 quarantine을 available로 만들면 안 된다.
14. 제거 가능성
14.1 제거 순서
InstallationState=REMOVING,TrafficAdmission=DISABLED로 전환하고 신규 session/capability와 product write를 중지한다.- in-flight operation을 bounded drain/cancel한다.
OPEN,COMPLETE_REQUESTED,PROVIDER_COMMITTED,QUARANTINE_RECORDED,ABORT_REQUESTEDsession을 reconcile한다.- quarantine/scan/reconciliation backlog와 orphan inventory를 0 또는 승인된 handoff 상태로 만든다.
AVAILABLEobject의 새 provider/use case 이전과 read cutover를 완료한다.- route/controller, consumer와 product composition을 제거한다.
- cleanup/scanner workers, queue subscription, webhook, scheduler와 provider lifecycle rule을 중지·제거한다.
- provider-owned IAM principal, signing key, credential와 secret을 revoke하고 config/SDK dependency를 제거한다.
- alert/dashboard/SLO, runbook, on-call ownership과 비용 budget을 제거하거나 새 owner에게 명시적으로 이관한다.
- owned registry/idempotency rows와 provider objects는 별도 승인된 data migration으로 제거한다.
- architecture, contract, startup, network와 dependency inventory gate를 재실행한다.
data 삭제를 code removal과 같은 단계에서 암묵적으로 실행하지 않는다.
일반 object 삭제도 logical tombstone/CAS와 exact provider generation purge를
분리하고 durable purge receipt를 남긴다. retention/legal hold가 있으면 physical
purge보다 우선하며 provider NOT_FOUND만으로 삭제 완료를 추정하지 않는다.
14.2 제거 gate
server 구현 저장소에는 격리 copy 또는 build profile에서 다음을 자동 검증하는 removal test를 둔다.
- file capability source와 provider dependency 제거
- file 전용 route/job/config/migration 제거
- 남은 source import 0개
- base type/compile/lint/unit/integration 통과
- application startup 통과
- dependency/SBOM에 provider SDK 부재
- generated API/schema에 file endpoint 부재
- 기본 health/readiness가 file provider를 요구하지 않음
- startup과 idle 기간 provider DNS/network 호출 0건
- secret/IAM/queue/webhook/scheduler/IaC inventory에 orphan 0건
- provider object와 database data는 삭제 완료 또는 새 owner에게 이관됐다는 별도 signed inventory 존재
15. Frontend와의 향후 계약
현재 frontend runtime과 연결할 때 control-plane API는 최소 다음 의미를 제공해야 한다. 실제 endpoint/DTO는 제품 feature가 소유한다.
whole-object presigned control envelope의 목표 protocol은
PRESIGNED_TRANSFER_V1, Image descriptor envelope은
IMAGE_CDN_DESCRIPTOR_V1, Range resume는
RANGE_RESUMABLE_DOWNLOAD_V1이다. 현재 frontend/server template에 이 세
control plane이 모두 구현됐다는 뜻은 아니다. BFF는 unknown/missing version을
fail-closed하고 N-1 client drain/rollback window를 명시해야 한다.
15.1 browser-managed download
server가 발급하는 capability는 frontend
BrowserManagedDownloadCapability와 다음 의미가 일치해야 한다.
receipt
href
resourceId/ObjectRef
mediaType
safeExtension
maxBytes
expectedSha256?
expiresAt
endpoint는 capability가 주장한 max bytes, object generation, digest와 expiry를
실제 response에 enforce한다. frontend의 BROWSER_HANDOFF는 disk save 완료가
아니다.
15.2 authorized stream download
제품 연결 전 frontend의 AUTHORIZED_STREAM_RESOURCE는 단순 resourceId보다
좁은 server-issued capability를 받도록 확장해야 한다.
AuthorizedStreamCapability
receipt
objectRef
generation
exactLength
expectedDigest
mediaType
expiresAt
stream opener는 이 capability를 검증해 exact provider/server response를
FileByteSource로 변환한다.
15.3 multipart upload
frontend feature가 필요한 최소 의미:
protocol = PRESIGNED_MULTIPART_V1
create -> protocol, sessionRef, request binding, part constraints, expiry,
SHA-256-PARTS-V1 fingerprint
put/sign part -> protocol/session, request + upload binding, part number,
offset, exact length, digest, idempotency key
complete -> protocol/session + ordered verified receipts -> QUARANTINED
abort -> protocol/session -> stable terminal result
status -> protocol/session -> ACTIVE | QUARANTINED | ABORTED | EXPIRED | NOT_FOUND
browser control adapter는 CREATE_SESSION, GET_STATUS, COMPLETE, ABORT의
closed operation set을 composition-owned fixed HTTPS endpoint map으로 실행한다.
part/download capability 발급 adapter도 composition 시 fixed BFF endpoint를 한
번만 받는다. server route가 어떤 path를 선택하든 caller가 endpoint를 URL로
전달하거나 operation을 추가할 수 없는 closed contract를 유지한다.
canonical binding은 UTF-8 newline-separated field sequence의 SHA-256 lowercase hex다.
fingerprint digest fields:
SHA-256-PARTS-V1
fingerprint.byteLength
fingerprint.partSizeBytes
fingerprint.partCount
{partNumber}:{offset}:{byteLength}:{checksumSha256} for each ordered part
request binding fields:
RESUMABLE-UPLOAD-BINDING-V1
uploadKey, purpose, mediaType
fingerprint.algorithm, fingerprint.digestHex
fingerprint.byteLength, fingerprint.partSizeBytes, fingerprint.partCount
upload session binding fields:
RESUMABLE-UPLOAD-SESSION-BINDING-V1
requestBindingSha256, sessionId
fingerprint.algorithm, fingerprint.digestHex
fingerprint.byteLength, fingerprint.partSizeBytes, fingerprint.partCount
각 field/part line은 UTF-8 newline 하나로 연결하고 trailing newline을 붙이지 않는다. server는 complete 때 ledger의 ordered part set으로 fingerprint digest를 재계산한다.
BFF는 part capability 요청의 sessionId로 registry row와 immutable session
snapshot을 조회해 위 값을 다시 만든다. client-provided digest는 비교 입력이지
authorization, ownership, current state 또는 part eligibility를 증명하지 않는다.
UPLOAD_PART capability binding에도 exact
protocol: PRESIGNED_MULTIPART_V1을 포함하고 subject scope, expiry, protocol,
exact part plan과 idempotency를 별도로 검증한다.
PUT capability는 expected success status, receipt response header와
expectedResponseByteLength를 binding한다. object store/proxy는 exact
Content-Length를 보내야 한다. 단 204는 response bytes가 0이어야 하고 header
부재를 0으로 정규화한다. frontend는 hard cap과 transfer deadline 안에서 response
body를 EOF까지 drain한 뒤 receipt를 채택하므로 CORS는 custom receipt header를
명시적으로 expose해야 한다.
status의 HTTP 404/410은 각각 NOT_FOUND/EXPIRED_RESOURCE terminal 의미다.
frontend는 stale checkpoint를 CAS 제거하고 restart한다. network/429/모든 5xx는
server가 명시한 bounded Retry-After와 frontend attempt/deadline ceiling 안에서만
재시도된다. complete 내부 DTO는 session/fingerprint binding을 검증하지만
application-facing QUARANTINED 결과에는 state, opaque resource reference,
byte length와 replay 여부만 노출한다.
AVAILABLE 전에는 public URL, normal download capability나 active-content preview를
발급하지 않는다.
15.4 Range resumable download
Range capability는 whole-object download URL을 재사용하는 편의 header가 아니라
별도 RANGE_RESUMABLE_DOWNLOAD_V1 control contract다.
server registry는 logical resource를 다음 immutable representation에 binding한다.
representationBinding
objectRef
immutable generation
strong validator
exact total byte length
media type
whole-object SHA-256
- weak validator, multipart ETag의 digest 추정, last-modified나 file name을 representation identity로 사용하지 않는다.
- object provider의 version ID가 안정적이면 registry generation과 exact provider locator를 server 내부에서 binding한다. 그렇지 않으면 BFF proxy가 immutable generation을 enforce한다.
- control response가 data-plane URL과
If-Rangevalidator를 전달하더라도 raw 값은 adapter-owned in-memory vault에만 있고 application/checkpoint에는 representation binding digest만 남는다. - capability는 exact
Range: bytes=start-end,preconditionMode=STRONG_IF_RANGE | IMMUTABLE_GENERATION_PRECONDITION, mode가 정한 exact header/value,allowWholeObjectFallback, policy-derived allowed status subset, expected total/segment bytes, expiry와 single-use receipt를 묶는다. - BFF/object provider가 이 제약을 실제로 강제하지 못하면 direct signed URL을 사용하지 않고 BFF relay를 사용한다.
data-plane 의미:
| 응답 | server 의미 | frontend 처리 계약 |
|---|---|---|
206 |
같은 representation의 exact requested segment | exact Content-Range, length와 validator 검증 뒤 write |
200 |
byte 0 full representation 또는 If-Range mismatch |
기존 partial에 append 금지, 새 generation/restart reconcile |
412 |
별도 If-Match/generation precondition 실패 |
representation replacement로 checkpoint 격리 |
416 |
requested range가 current representation에 유효하지 않음 | 자동 success 금지, total/destination/full digest 재검증 |
RFC 9110의 일반 If-Range mismatch는 200 full response다. 412를 받으려면
제품 계약이 별도 strong precondition을 명시해야 한다.
capability expiry 시 BFF는 current authorization과 registry를 다시 확인하고 같은 representation binding에 대해서만 새 segment capability를 발급한다. object generation/length/media/digest가 달라지면 기존 partial resume를 거절한다. final 성공은 frontend destination 전체 SHA-256 검증 뒤에만 가능하지만, server도 download capability가 주장한 digest/length가 registry의 authoritative object와 일치하도록 보장한다.
Range checkpoint retention과 seek/truncate 또는 OPFS staging은 browser local 계약이다. server는 raw local path/offset을 authorization proof로 받지 않는다.
15.5 Upload pause와 checkpoint lifecycle
pause는 기본적으로 browser work를 중지하는 local control이며 server abort가 아니다. 제품이 explicit server pause를 만들지 않더라도 status/reconcile은 paused client가 안전하게 돌아올 수 있도록 다음을 보장한다.
- session expiry와 terminal status의 안정적인 의미
- completed part의 ordered checksum과 bounded non-authorizing receipt
- list/status pagination 또는 절대 part-count ceiling
- 같은 idempotency key의 replay 결과
- abort/complete response loss 뒤 authoritative reconcile
- abandoned session TTL과 orphan janitor
frontend checkpoint inventory/retention API에는 presigned URL, server capability, raw provider upload ID와 object key가 나타나지 않는다. checkpoint sweep가 local row를 지웠다고 server session이 즉시 abort됐다고 가정하지 않으며 janitor SLO로 ambiguous orphan을 정리한다.
15.6 Image CDN delivery
backend asset registry는 scan/promotion을 통과한 immutable object generation만 opaque asset ID/revision에 binding한다. descriptor에는 registry-owned named preset, static raster media type, exact natural/rendition dimensions, delivery class와 private capability expiry를 포함한다. arbitrary external source URL 또는 caller transform query를 signer/CDN에 전달하지 않는다.
private capability가 서명하는 allowed preset binding ID는 versioned server-owned immutable preset registry의 key다. CDN/BFF는 각 요청마다 이 ID를 조회하고 width/height/DPR/fit/format/quality query를 registry의 exact candidate와 재계산해 불일치를 거절한다. client query, binding digest 또는 signature의 단순 존재는 transform authorization이 아니다.
public rendition은 revisioned URL과 public, max-age=..., immutable, private
rendition은 short-lived capability와 실제 Cache-Control: no-store를 제공한다.
private response도 CORS에서 browser probe가 읽을 Content-Type,
Content-Length, Cache-Control을 허용해야 하며 credential cookie에 의존하지
않는다. CDN은 application과 다른 HTTPS origin에서 제공한다. 이는
<img crossorigin="anonymous">의 same-origin credential mode가 application
cookie를 보내는 경로를 차단하기 위한 배포 계약이다. frontend는
PNG/JPEG/WebP/AVIF static header metadata와
pixel/decoded-byte budget을 native decode 전에 검증하고, private delivery는
mandatory probe와 fetch/body/decode 전체 timeout을 적용한다.
private descriptor BFF는 fixed endpoint에서
IMAGE_CDN_DESCRIPTOR_V1 strict bounded response를 반환한다. caller는 opaque
asset/preset reference만 제출하고 URL, origin, width, DPR, format, quality, fit,
cache policy와 key ID를 선택하지 않는다.
descriptor 발급과 refresh는 매번 current authorization, promotion state, immutable asset revision, preset registry와 signing key registry를 다시 확인한다. expiry 뒤 stale descriptor 사용을 허용하지 않는다. key rotation은 bounded old/new verification overlap, signer 전환, maximum descriptor lifetime + clock skew + client rollout drain 이후 old key 제거 순서다. emergency revoke는 client signature/expiry만 기다리지 않고 registry/CDN/BFF에서 capability를 거절한다.
generic Query cache나 durable client persistence가 private descriptor URL의 lifetime owner가 아니다. account/logout 뒤 늦은 refresh가 성공하더라도 frontend runtime generation fence가 이를 채택하지 않으며, backend authorization도 old session을 거절해야 한다.
15.7 failure mapping
server의 closed failure를 frontend의 allowlisted failure로 한 곳에서 mapping한다. HTTP status/message를 application에 그대로 전달하지 않는다.
| server meaning | frontend meaning 예 |
|---|---|
| invalid request | INVALID_INPUT |
| size/part ceiling | LIMIT_EXCEEDED |
| revision/idempotency conflict | CONFLICT |
| checksum/length mismatch | INTEGRITY_FAILED |
| expired session/capability | EXPIRED_RESOURCE |
| denied policy/authorization | POLICY_REJECTED 또는 auth failure |
| provider/scanner temporary failure | DEPENDENCY_UNAVAILABLE + server retry directive |
| caller cancellation | CANCELLED |
| server deadline exhausted | DEADLINE_EXCEEDED |
| mutation outcome unknown | 같은 idempotency key로 status/reconcile, 신규 요청 금지 |
16. 제품 선택 시 결정할 항목
다음 표가 채워지기 전에는 endpoint와 composition을 만들지 않는다.
| 결정 | owner |
|---|---|
| 파일의 업무 목적과 aggregate 관계 | product/domain |
| authorization와 existence-hiding 정책 | security/domain |
| provider/region/data residency | platform/security |
| proxy/direct transfer 선택 | platform/product |
| object/part 최대 크기와 concurrency | performance/platform |
| session/capability TTL | security/product |
| checksum algorithm과 encoding | platform |
| MIME/parser/archive/CDR/scan profile | security/product |
| quarantine/available/rejected UX | product |
| retention/legal hold/delete | domain/legal |
| idempotency replay window/capacity | platform/product |
| provider/scanner SLO와 fallback | operations/product |
| logging, audit와 privacy retention | security/operations |
| rollout, kill switch와 removal owner | operations |
17. Server 구현 순서
- 공통 identifier,
Result와 closed failure model - in-memory deterministic fakes와 provider contract suite
ObjectRegistryPort와IdempotencyStorePort- 첫 database adapter 및 concurrency/fault evidence
ObjectByteStorePort와 첫 object provider adapter- streaming counter/digest/deadline/resource wrappers
MultipartStagingPort와 orphan reconciliationSignedTransferCapabilityPort와 expiry/key-rotation tests- optional Range capability와 immutable-generation/provider contract
ContentScannerPort, quarantine와 promotion saga- optional Image descriptor/preset/signing control plane
- optional composition/removal gate
- server template에서는
AVAILABLE_NOT_COMPOSED로 종료 - 실제 제품에서만 domain/use case/controller와 provider composition 추가
18. 완료 기준
- 모든 요청 항목에 port, 불변조건, failure와 lifecycle이 정의됨
- native provider SDK type/exception이 port 밖으로 유출되지 않음
- streaming 경로에 whole-buffer 기본 구현이 없음
- byte length와 digest authority가 proxy/direct 경로별로 명확함
- multipart complete/abort/expiry/response-loss race가 결정적임
- signed capability가 exact operation/resource/limit/expiry에 binding됨
- Range를 선택한 경우 immutable representation, exact
200/206/412/416, reissue와 provider constraint가 같은 contract suite를 통과 - idempotency가 concurrent replay와 fingerprint mismatch를 처리함
- scan verdict가 exact generation/digest에 binding됨
- Image CDN을 선택한 경우
IMAGE_CDN_DESCRIPTOR_V1, preset exact recomputation, key rotation/revocation과 private no-store가 실제 BFF/CDN에서 검증됨 - deadline/retry/cancel 모든 경로에서 resource cleanup이 검증됨
- 동일 provider contract suite가 fake와 실제 선택 provider에 실행됨
- 미선택 상태에 route/job/migration/provider client가 없음
- capability 전체 제거 후 server template core가 정상 동작함
- 실제 제품 domain/use case와 provider 선택이 이 문서에 하드코딩되지 않음
19. 관련 frontend 설계
docs/architecture/browser-data-capability-completion-ledger.mddocs/architecture/browser-file-and-origin-storage.mddocs/architecture/decisions/VD-11-browser-file-and-origin-storage.mddocs/architecture/presigned-transfer-and-image-cdn.mddocs/architecture/decisions/VD-12-presigned-transfer-and-image-cdn.mddocs/architecture/decisions/VD-14-resumable-download-and-background-transfer.mddocs/architecture/decisions/VD-16-browser-transfer-composition-and-image-delivery.mddocs/architecture/frontend-ports-adapters-and-boundaries.mddocs/architecture/optional-adapter-recipes.mddocs/operations/browser-file-storage-recovery.mddocs/operations/browser-transfer-recovery.md
20. 참고 표준과 보안 가이드
- RFC 9110, HTTP Semantics: https://www.rfc-editor.org/rfc/rfc9110
- RFC 9530, Digest Fields: https://www.rfc-editor.org/rfc/rfc9530
- OWASP File Upload Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html