Files
tech-log-backend/docs/superpowers/specs/2026-07-28-objectstorage-production-capability-design.md

4204 lines
164 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Object Storage Production Capability Deep Design
- 작성일: 2026-07-28
- 상태: Approval Gate A 승인 반영, Batch AD 및 Batch E pause checkpoint를 main에 통합, R2 미구현
- 독립 재리뷰: 완료 — 아키텍처 및 문서 실행성 blocker 0, high 0
- LLM Wiki capture: public vault의
`raw/branch-notes/chore-main-worktree-capability-integration.md`에 main 통합 기록 완료
- 기준: Java 21, Spring Boot 4.0.0, Gradle 멀티모듈 Clean Architecture
- 대상 leaf: `adapter-outbound-objectstorage`
- 현재 구현 수준: provider-neutral kernel + `filesystem-local-dev` managed single R1,
S3 managed/direct primitives, exact MinIO mutation profiles `UNSUPPORTED`,
legacy whole-object filesystem/S3-MinIO 격리
- 상위 문서:
[Production Capability Platform Design](2026-07-26-production-capability-platform-design.md)
- 구현 계획:
[Object Storage Production Capability Implementation Plan](../plans/2026-07-28-objectstorage-production-capability.md)
- 참고 설계:
[Redis Production Capability Deep Design](2026-07-26-redis-production-capability-design.md),
[Fileserver Production Capability Deep Design](2026-07-26-fileserver-production-capability-design.md),
[HTTP Client Production Capability Deep Design](2026-07-27-httpclient-production-capability-design.md)
## 0. 구현 상태
### Approval Gate A 결정 — 2026-07-29
사용자의 구현 계속 승인에 따라 Batch E의 public/sample 계약을 다음과 같이 고정한다.
- production scanner는 주장하지 않는다. 스캔 경계는 test fake만 제공하고 첫 sample
destination profile은 `integrity-verified-reference`로 한다.
- canonical surface는 additive
`POST /posters/{id}/imagePublications`와 별도 authorized GET status resource다. 저장소의
AIP-122 경로 규칙에 따라 collection segment에는 kebab-case를 사용하지 않는다.
POST는 항상 동일한 bounded `202` reservation을 반환하며 READY 결과를 같은 idempotency
response에 섞지 않는다.
- 정확한 full-file SHA-256은 canonical Base64 형식의 bounded multipart field로 받고 TX1 전에
semantic fingerprint에 포함한 뒤 staging 중 다시 검증한다.
- `Idempotency-Key`는 필수다. 첫 committed UploadIntent가 stable operation ID를 할당하며
generic idempotency reservation과 intent는 같은 TX1에서 원자적으로 commit한다.
- sample authorization은 기존 `poster:write` permission을 유지한다. 존재하지 않는 Poster
owner 속성을 발명하지 않는다.
- legacy `/posters/{id}/image`는 명시적 compatibility profile에서만 유지한다. canonical과
legacy controller는 동시에 활성화하지 않는다.
- 기존 `poster.image-attached`/`imageKey` 이벤트는 변경하거나 제거하지 않는다. canonical
흐름은 새 versioned V2 event를 사용하며 외부 zero-consumer를 주장하지 않는다.
- OpenAPI 승인은 새 publication/status surface에만 한정한다. 기존 DTO의 locator-shaped
필드는 compatibility window 동안 그대로 둔다.
- 통합 타임라인의 V8은 additive, forward-only다. V7 Poster schema의 `image_key`와 legacy
reader를 유지하고 canonical writer는
READY opaque reference만 기록한다. rollback은 구 binary가 additive columns를 무시하는
binary-only window이며 down migration이나 자동 object mutation은 없다.
2026-07-28 Batch D 체크포인트 기준 다음 범위가 구현되었다.
- `application-core`의 framework/provider-neutral object-storage identity, opaque reference,
bounded content callback, digest/range, request/receipt/outcome, capability requirement;
- managed, staged, direct single/multipart, scan-maintenance, purge-maintenance를 분리한 narrow port;
- 모든 mutation의 `ObjectOperationKey`, strict reference grammar/checksum, transient grant redaction;
- application contract의 framework/provider/transport/persistent-locator purity 검사;
- legacy port/receipt의 removal-boundary deprecation과 sample legacy import freeze;
- deterministic `data/v1`/`control/v1` namespace, reference/handle/fingerprint/policy codec,
operation epoch와 immutable route/policy snapshot;
- strict canonical JSON control envelope와 operation/manifest/reference/multipart record,
reserve/replay/conflict/pending-effect state kernel;
- bounded immutable local create, exact inspect/version, full/range transfer, conditional retire,
create resolution, symlink/path confinement, restrictive permissions와 single-process CAS;
- constructor-bound `app.object-storage` settings, compile-before-construction provider
contribution/assembler, destination/retained-route routers, disabled/unselected/invalid/close
side-effect matrix;
- explicit canonical legacy subgroup와 old alias isolation, sample local/test explicit activation;
- exact nine-card readiness registry. `filesystem-local-dev` managed single upload/download만 R1,
나머지는 R0;
- pinned AWS SDK `2.30.0` conditional/checksum/expected-owner API characterization과 explicit
`NettyNioAsyncHttpClient` policy/factory/lifecycle;
- exact AWS/MinIO binding, bounded async transport, managed S3 single/multipart/control primitives,
response-loss resolution, selected-only provider composition;
- durable direct single/multipart session and grant records, S3 presigner lifecycle, exact part
acknowledgement, completion/abort fences, terminal exact-version replay;
- digest-pinned MinIO contract/fault lanes and protected AWS compile-only lane. Exact MinIO는
create-only PUT/complete 조건을 무시하므로 managed/direct mutation profile을
`UNSUPPORTED`로 유지하며 readiness를 올리지 않는다.
증거는 [Batch A checkpoint](../../evidence/object-storage/batch-a-checkpoint.md),
[Batch B checkpoint](../../evidence/object-storage/batch-b-checkpoint.md),
[Batch C checkpoint](../../evidence/object-storage/batch-c-progress.md),
[Batch D checkpoint](../../evidence/object-storage/batch-d-checkpoint.md),
[Batch E pause checkpoint](../../evidence/object-storage/batch-e-pause-checkpoint.md)에 기록했다.
격리 브랜치에서는 private vault 경로를 잘못 선택해 capture가 차단됐지만, main 통합 작업은
사용자가 지정한 public vault의
`raw/branch-notes/chore-main-worktree-capability-integration.md`에 캡처했다.
legacy runtime 범위는 별도로 유지된다.
- `application-core`의 범용 `ObjectStoragePort`;
- caller가 지정한 raw key에 `byte[]`를 put/get/delete/exists하는 계약;
- local filesystem adapter;
- synchronous AWS SDK v2 `S3Client`를 사용하는 S3/MinIO adapter;
- 명시적 legacy-only filesystem 또는 S3 Spring composition;
- filesystem unit test, mocked S3 unit test, Testcontainers MinIO integration test;
- module registry가 허용한 `application-core`, `shared-contract` 의존성.
아직 구현되지 않은 runtime/provider 범위:
- staged upload, quarantine, scan, publication;
- public presigned upload/download endpoint와 inbound authorization;
- R2 수준의 exhaustive multipart/direct response-loss·race·restart qualification;
- lifecycle, versioning, retention, legal hold 검증;
- multi-node linearizable control CAS와 production unknown-outcome reconciliation;
- production credential, encryption, TLS, retention/Object Lock qualification;
- orphan/abandoned multipart/retired object reaper;
- database와 object storage 사이의 crash-safe workflow;
- 운영 metric, trace, audit, runbook;
- AWS sandbox fault/security evidence.
따라서 현재 MinIO round-trip test가 통과하더라도 S3 production readiness를 의미하지 않는다.
Batch B의 R1은 exact local functional topology 증거다. Multi-node CAS, power-loss durability,
S3/MinIO qualification, R2 또는 운영 준비 완료를 뜻하지 않는다.
## 1. 설계 판정
현재 구현은 개발 편의를 위한 blob CRUD 예제다.
```text
MultipartFile.getBytes()
-> UploadPosterImageCommand(byte[])
-> DB write transaction 안에서 ObjectStoragePort.put(raw key, byte[])
-> provider final object overwrite
-> aggregate에 raw key 저장
-> file:// 또는 s3:// 내부 locator를 HTTP 응답으로 반환
```
목표는 bucket/key CRUD wrapper가 아니다. 목표는 다음 capability다.
> 제한된 크기의 content를 bounded streaming으로 저장·검증하고, private immutable
> reference를 통해 공개 상태와 version을 추적하며, direct transfer와 multipart의
> 불확정 결과까지 재조정할 수 있는 production object publication capability
선택한 핵심 구조는 다음과 같다.
1. Application은 bucket, path, endpoint가 아닌 `ObjectDestinationId`를 선택한다.
2. 모든 mutation은 안정적인 `ObjectOperationId`와 canonical request fingerprint를 가진다.
3. caller는 raw provider key를 만들지 않는다. Adapter가 managed namespace 아래 immutable key를
생성한다.
4. Application과 domain은 provider locator 대신 opaque `ObjectReference`를 저장한다.
5. upload/download는 동기식 bounded chunk callback을 baseline으로 하며 adapter가 resource
lifecycle을 소유한다.
6. 업로드 완료, integrity 검증, malware scan, business attachment, public visibility를 서로 다른
상태로 모델링한다.
7. publication은 public ACL 변경이 아니라 private object에 대한 durable reference/manifest의
상태 전이로 구현한다.
8. provider 응답 유실은 임의 retry가 아니라 `INDETERMINATE`로 분류하고 먼저 reconcile한다.
9. ETag를 whole-object checksum 또는 portable version으로 간주하지 않는다.
10. database와 object storage 사이의 원자적 transaction을 주장하지 않는다.
11. provider 지원 수준은 설정 이름이 아니라 startup qualification과 CI evidence로 판정한다.
12. 사용하지 않는 provider는 client, thread, scheduler, directory 또는 network side effect를
만들지 않는다.
## 2. 상위 설계 및 기존 capability 설계와의 관계
상위 설계의 §13.3은 다음 cross-capability baseline을 이미 정했다.
- streaming, head, range;
- checksum과 conditional mutation;
- bounded presigned transfer와 multipart cleanup;
- encryption, TLS, endpoint, region, credentials;
- lifecycle, versioning, retention;
- quarantine와 scan 전 publication 금지;
- payload/metadata limit;
- explicit staged lifecycle;
- database rollback이 object write를 되돌린다고 가정하지 않기.
이번 문서는 그 방향을 구현 계획으로 변환할 수 있도록 다음 결정을 추가한다.
- 정확한 port 분리와 framework-free callback signature;
- logical destination, operation, object, reference, version의 identity;
- immutable data key와 mutable control record의 경계;
- state machine, request fingerprint, result taxonomy;
- managed upload, direct upload, multipart, download의 protocol;
- provider-neutral 보장과 AWS/MinIO/filesystem별 차이;
- provider capability descriptor와 startup qualification;
- presigned URL의 bearer-secret 취급 및 검증 후 publish;
- database attachment workflow의 commit 순서와 crash gap;
- version-aware delete, retention hold, orphan cleanup;
- exact readiness card와 R0/R1/R2/R3 증거;
- 기존 `ObjectStoragePort`와 sample-portfolio migration;
- idempotency response offload 계약과 sibling adapter boundary.
기존 심화 설계에서 재사용하는 방식은 다음과 같다.
| 기존 설계 | 재사용하는 결정 | Object Storage에서 달라지는 점 |
| --- | --- | --- |
| Redis | exact activation, provider/card readiness, fail-closed binding | data plane이 대용량 stream이며 object mutation의 unknown outcome을 별도로 다룬다. |
| Fileserver | stable operation ID, staged publication, immutable naming, reconciliation | rename 대신 object+manifest/reference 전이를 사용하고 multipart/presign이 추가된다. |
| HTTP Client | body lifecycle, timeout budget, retry amplification, secret-safe observability | provider SDK retry와 multipart part 단위 resource budget을 함께 제한한다. |
이 문서와 상위 문서가 충돌하면 Object Storage 구현 세부에는 이 문서를 적용한다. 모듈
dependency edge에는 언제나 `src/config/architecture/modules.json`이 우선한다.
## 3. 현재 코드의 증거 기반 진단
### 3.1 Application contract
| 영역 | 현재 구현 | 운영상 의미 |
| --- | --- | --- |
| Identity | caller supplied `String key` | tenant, destination, ownership, generation 경계가 없다. |
| Upload | `put(String, byte[], String)` | content 전체를 heap에 적재하고 overwrite한다. |
| Download | `Optional<byte[]> get(String)` | large object와 range/backpressure를 지원하지 않는다. |
| Existence | `boolean exists(String)` | not-found와 forbidden/provider failure를 충분히 구분하지 못한다. |
| Delete | unconditional `void delete(String)` | version, precondition, retention hold, unknown outcome이 없다. |
| Receipt | key, size, content type, provider `URI` | `file://``s3://bucket/key`가 application/HTTP로 유출된다. |
| Error | primitive/SDK exception 혼합 | retry와 reconciliation 결정을 application이 할 수 없다. |
### 3.2 Filesystem adapter
현재 `FilesystemObjectStorageAdapter`는 다음 문제를 가진다.
- constructor에서 base directory를 즉시 생성한다;
- `Files.write(final, bytes)`로 기존 파일을 truncate/overwrite한다;
- temp, exclusive create, file force, directory force가 없다;
- `readAllBytes`로 전체 content를 heap에 적재한다;
- content type과 checksum을 영속화하지 않는다;
- `normalize().startsWith(baseDir)` lexical check만 사용한다;
- nested symlink와 TOCTOU 탈출을 막지 못한다;
- `"."`, `"a/.."`처럼 root alias로 normalize되는 key가 가능하다;
- root alias delete가 base directory 자체를 대상으로 삼을 수 있다;
- file permission, quota, inode, retention, orphan cleanup이 없다;
- absolute `file://` URI를 receipt에 넣는다.
따라서 이 provider는 현재 local developer fixture이지 persistent production store가 아니다.
### 3.3 S3/MinIO adapter
현재 `S3ObjectStorageAdapter`는 다음 문제를 가진다.
- synchronous `S3Client``RequestBody.fromBytes`/`getObjectAsBytes`만 사용한다;
- put이 unconditional overwrite다;
- checksum, size precondition, metadata schema가 없다;
- exact version/ETag을 receipt에 보존하지 않는다;
- conditional create/delete와 reconciliation이 없다;
- presign과 multipart가 없다;
- SSE/KMS, expected bucket owner, ownership/BPA qualification이 없다;
- finite API call/attempt/acquire/read/write timeout을 명시하지 않는다;
- provider SDK exception이 application boundary를 통과할 수 있다;
- missing object mapping이 operation마다 일관되지 않다;
- `s3://bucket/key` URI 생성이 성공한 write 뒤에 별도로 실패할 수 있다;
- startup `HEAD bucket` 실패가 404이면 runtime identity로 bucket을 생성할 수 있다.
같은 adapter가 endpoint override와 path-style 설정만으로 AWS S3와 MinIO를 “동일한 backend”로
취급한다. 이는 protocol happy path 재사용에는 유용하지만 consistency, checksum, conditional
request, versioning, retention, encryption, error 의미론의 동등성을 증명하지 않는다.
### 3.4 Settings와 activation
현재 설정은 다음 production-unsafe default를 가진다.
```yaml
backend: filesystem
base-path: ./.data/objectstorage
bucket: ca-skeleton
endpoint: http://localhost:9000
region: us-east-1
path-style-access: true
auto-create-bucket: true
```
- filesystem bean이 `matchIfMissing=true`로 활성화된다;
- S3 설정이 없으면 local plaintext MinIO를 가리킨다;
- access key 하나만 있으면 secret key pair validation 없이 static provider를 만든다;
- production bucket을 runtime startup에서 생성할 수 있다;
- `app-bootstrap`은 registry상 objectstorage leaf에 의존하지 않지만 sample application의 넓은
component scan에서는 configuration이 발견된다;
- Docker runtime의 read-only root와 `./.data/objectstorage` default가 맞지 않는다;
- canonical env/settings/secrets registries에 object storage key가 등록되지 않았다.
### 3.5 Sample workflow
`sample-portfolio`의 image upload는 다음 failure gap을 가진다.
```text
begin DB transaction
load Poster
overwrite deterministic final object key
attach raw key
save Poster
enqueue outbox
commit DB transaction
```
- DB rollback이 이미 완료된 object overwrite를 복원하지 못한다;
- 기존 image를 overwrite했으므로 compensation으로 delete해도 이전 version을 복구하지 못한다;
- upload 시간 동안 DB transaction과 connection을 점유한다;
- concurrent transaction loser도 final object를 바꿀 수 있다;
- `MultipartFile.getBytes()`가 inbound와 application 양쪽에서 전체 heap materialization을 만든다;
- client가 보낸 content type을 검증 없이 신뢰한다;
- delete use case는 Poster만 삭제하고 object lifecycle을 처리하지 않는다;
- aggregate와 public DTO에 raw key가 저장/노출된다;
- scan, checksum, quarantine, ready 상태가 없다.
### 3.6 별도 persistence idempotency blob seam
`adapter:outbound:persistence-jpa`에는 idempotency response body를 외부 object store로 offload할
수 있다는 별도 interface가 있다. Object Storage leaf가 그 sibling adapter interface를
구현하면 registry edge와 Clean Architecture 방향을 위반한다.
이번 baseline에서 이 용도는 제외한다. 향후 필요하면:
1. application 의미인 response-blob 계약을 `application-core`에 둔다;
2. persistence adapter와 objectstorage adapter가 각각 application port만 의존한다;
3. orchestration은 application service 또는 composition root가 담당한다;
4. objectstorage가 persistence leaf의 type을 import하지 않는다.
## 4. 범위와 명시적 비범위
### 4.1 전체 설계 범위
- bounded streaming managed upload/download;
- single contiguous range read;
- metadata/head와 exact version;
- SHA-256 content digest와 provider transport checksum;
- immutable create와 conditional mutation;
- private staged object와 durable publish reference;
- server-mediated transfer;
- short-lived presigned PUT/GET;
- adapter-owned managed multipart와 optional direct multipart;
- quarantine/scan integration seam;
- version-aware retirement/delete;
- orphan, abandoned multipart, indeterminate operation reconciliation;
- filesystem-local-dev, AWS S3 general-purpose, version-pinned MinIO provider cards;
- TLS, credentials, ownership, encryption, endpoint, lifecycle startup validation;
- resource budgets, timeout, retry, graceful shutdown;
- readiness, metrics, trace, audit, CI evidence;
- sample image attachment migration protocol.
`filesystem-local-dev`는 이 전체 설계의 R0/R1 개발 provider일 뿐 R2 대상이 아니다. R2 claim은
exact AWS S3 또는 qualified MinIO/deployment provider와 §32의 개별 card 조합에만 부여한다.
### 4.2 Optional capability
다음은 destination이 요구하고 provider card가 증명할 때만 활성화한다.
- browser POST policy;
- SSE-KMS 또는 DSSE-KMS;
- Object Lock retention/legal hold;
- provider-side copy;
- provider checksum algorithm 추가;
- filesystem-local-persistent;
- direct multipart upload;
- customer-managed public download domain;
- provider notification을 이용한 reconciliation hint;
- cross-region replication을 고려한 secondary verification.
Optional capability 부재를 silent emulation하지 않는다. 요청 시
`UNSUPPORTED_CAPABILITY`로 실패한다.
### 4.3 비범위
- generic bucket CRUD/list console;
- runtime bucket 생성, lifecycle/IAM/KMS/IaC provisioning;
- public-read ACL 또는 website hosting;
- CDN/cache invalidation;
- generic file server 또는 mounted drop-zone;
- inbound object-created event consumer;
- scanner/AV engine 자체 구현;
- media transcoding, thumbnail 생성, EXIF business policy;
- arbitrary provider endpoint를 request마다 선택하는 기능;
- database와 object store의 distributed transaction 또는 exactly-once claim;
- backup/restore 시스템 자체;
- Glacier restore workflow;
- S3 Express One Zone/directory bucket;
- Multi-Region Access Point와 cross-region failover;
- provider replication을 application consistency로 일반화;
- unlimited multi-range response;
- S3 Select/object SQL;
- unbounded user-specified metadata/tag;
- raw object key를 public API로 제공하는 기능.
## 5. HARD invariants
다음 항목은 구현 선택이 아니라 위반 시 중단하는 불변식이다.
1. `domain-core`에는 Spring, AWS SDK, `Path`, `URI`, stream, transport DTO를 넣지 않는다.
2. `application-core` port에는 AWS SDK request/response, `S3Client`, provider exception을 노출하지
않는다.
3. inbound `MultipartFile`, servlet stream, WebFlux publisher를 application command로 전달하지
않는다.
4. controller가 bucket/key를 만들거나 SDK/repository/object persistence entity를 호출하지 않는다.
5. caller가 provider-relative raw key, bucket, base path, endpoint를 선택하지 않는다.
6. Destination ID는 bounded registered value이며 untrusted inbound/tenant input이 임의 destination을
선택하지 않는다.
7. object reference는 locator도 authorization token도 아니다.
8. presigned URL은 bearer secret으로 취급하고 log, trace, metric, audit payload에 기록하지 않는다.
9. 모든 production mutation은 stable operation ID와 canonical fingerprint를 가진다.
10. R2 write fingerprint는 exact length + full content digest 또는 검증 가능한 immutable source
revision을 포함한다.
11. 같은 operation ID에 다른 fingerprint가 오면 conflict이며 기존 결과를 반환하지 않는다.
12. 기본 upload는 immutable create-only다. unconditional overwrite는 baseline에 없다.
13. successful SDK response와 business publication을 같은 상태로 간주하지 않는다.
14. ETag를 portable checksum 또는 multipart 전체 MD5로 간주하지 않는다.
15. partial/truncated upload·download 또는 consumer failure를 success receipt로 만들지 않는다.
16. content size, chunk size, part size/count/concurrency, metadata 수와 길이에 finite limit가 있다.
17. timeout/retry 후 mutation 결과가 확정되지 않으면 phase-specific `INDETERMINATE`이며 blind
retry하지 않는다.
18. database transaction 안에서 remote object transfer를 실행하고 rollback 원자성을 주장하지
않는다.
19. unpublished/quarantined object에 public/direct download grant를 발급하지 않는다.
20. scan verdict가 필요한 destination은 `SCAN_CLEAN` 전 publish하지 않는다.
21. object lifecycle/retention/hold를 application business 삭제로 우회하지 않는다.
22. versioned bucket의 delete marker 생성을 physical purge라고 보고하지 않는다.
23. cleanup은 owned namespace와 known schema만 처리하고 newer/unknown record를 삭제하지 않는다.
24. LIST 결과만으로 direct lookup, ownership, completion truth를 판정하지 않는다.
25. production startup은 bucket, policy, lifecycle, public access setting을 자동 수정하지 않는다.
26. disabled capability는 bean/client/thread/scheduler/network/directory side effect가 0이다.
27. filesystem과 S3-compatible provider가 AWS S3와 동등하다고 일반화하지 않는다.
28. module dependency는 registry의 `allowed_dependencies`만 따른다.
29. provider/card별 증거 없이 module 전체를 R2라고 부르지 않는다.
30. readiness는 exact required destination/provider/capability mismatch에서 fail closed한다.
31. metric/tag/log에 object key, bucket, tenant/user ID, original filename, content, URL을 넣지 않는다.
32. verification과 Wiki capture 또는 명시적 capture 차단 사유 없이 완료를 주장하지 않는다.
## 6. 대안 검토
### A. 현재 CRUD port에 InputStream만 추가
기각한다.
- caller와 adapter 사이 close ownership이 모호하다;
- repeatability와 retry 가능성을 표현하지 못한다;
- raw key/overwrite/locator leak이 남는다;
- direct upload, multipart, staged publish를 같은 의미로 섞게 된다.
### B. 범용 `ObjectStoragePort` 하나에 모든 operation 추가
기각한다.
- read-only use case도 delete/presign/multipart 권한을 가진 interface를 주입받는다;
- provider optional capability가 nullable method와 runtime branch로 퍼진다;
- 테스트와 readiness가 “module on/off” 한 단계로 뭉개진다.
Operation family별 작은 port를 사용하고 destination capability compile 단계에서 조합한다.
### C. Application에 AWS presigner와 multipart upload ID 노출
기각한다.
- provider 교체와 테스트가 불가능해진다;
- provider upload ID와 part ETag가 domain state로 퍼진다;
- security/expiry/header 조건을 adapter 밖에서 조립하게 된다.
Application은 opaque `DirectTransferSessionId`, `PartReceiptToken`, typed grant만 본다.
### D. 업로드 후 final key로 copy하여 publish
기본 전략으로 기각한다.
- 대용량 object copy 비용과 latency가 추가된다;
- copy response loss의 unknown outcome이 하나 더 생긴다;
- KMS/metadata/version semantics가 복잡해진다.
기본은 immutable private data object를 그대로 두고 durable reference/manifest만 publish한다.
보안 등급이나 destination이 physical namespace 분리를 요구할 때만 optional copy capability를
사용한다.
### E. object tag 또는 ACL 변경을 publication truth로 사용
기각한다.
- tag/ACL policy와 cache/authorization coupling이 커진다;
- S3-compatible provider 의미가 일치하지 않는다;
- public access 실수의 blast radius가 크다.
모든 bucket은 private이고, application authorization 후 server stream 또는 short-lived grant로
전달한다.
### F. DB transaction 안에서 upload하고 실패 시 delete
기각한다.
- delete도 실패하거나 indeterminate일 수 있다;
- overwrite된 기존 object는 복구되지 않는다;
- DB lock/connection 보유 시간이 content 크기에 비례한다.
외부 transfer와 짧은 DB transaction을 단계별로 분리하고 durable recovery state를 둔다.
### G. provider별 Gradle leaf 즉시 분리
초기에는 보류한다.
현재 registry의 19개 leaf와 dependency direction을 유지하며 한 physical leaf 안에서 package,
settings, test suite, readiness card를 분리한다. 다음 조건 중 둘 이상이 생기면 별도 설계로
leaf split을 검토한다.
- provider별 독립 release cadence;
- SDK/security patch 주기가 현저히 다름;
- runtime image에서 선택하지 않은 SDK를 제거해야 함;
- credential/IAM 또는 deployment owner가 분리됨;
- provider별 test/CI 비용이 기본 pipeline을 과도하게 지연함;
- registry edge가 실제로 달라짐.
### H. filesystem을 S3 emulator로 승격
기각한다.
Filesystem은 local development와 provider-neutral contract 일부를 빠르게 검증하는 수단이다.
ETag, multipart, presign, versioning, retention, KMS를 흉내 내어 R2 증거로 사용하지 않는다.
## 7. 목표 아키텍처
```text
adapter:inbound:web
- multipart parsing / request validation
- bounded ingress bridge
- authorization mapping
|
v
sample/application orchestration
- business consent and ownership
- attachment state
- DB transaction boundaries
|
v
application-core object storage ports
- publication / inspection / transfer / deletion / direct transfer
- framework-free IDs, request, receipt, error
|
v
adapter:outbound:objectstorage
+-- binding compiler / capability registry
+-- provider-neutral operation kernel
+-- durable control-plane codec/store
+-- reconciliation / cleanup engine
+-- filesystem-local-dev provider
+-- AWS S3 general-purpose provider
`-- version-pinned MinIO provider
|
v
private provider data plane + private control namespace
```
Inbound stream과 outbound storage stream을 직접 결합하지 않는다. Application callback 경계가
다음 책임을 분리한다.
- inbound adapter: transport framing, multipart limit, client disconnect;
- application: business authorization, destination 선택, operation identity;
- objectstorage adapter: chunk budget, hashing, SDK lifecycle, retry, stage/publish;
- provider: durable byte/object primitive.
## 8. 모듈과 계층 소유권
### 8.1 `domain-core`
- provider 독립적인 business entity와 invariant만 소유한다;
- object storage concept가 business에 필요하면 opaque string wrapper 수준의 domain reference만
둘 수 있다;
- checksum, bucket, key, presign, multipart, scan vendor result를 소유하지 않는다.
### 8.2 `application-core`
- outbound port와 framework-free value object;
- logical destination, operation ID, object reference, version token;
- content producer/consumer callback;
- common request/result/error taxonomy;
- application-level deadline와 cancellation signal;
- capability requirement.
Business별 media type 허용, 사용자 소유권, 공개 승인, attachment 교체 정책은 sample 또는 실제
application layer가 소유한다. 공통 adapter limit는 business policy의 대체물이 아니다.
### 8.3 `adapter:outbound:objectstorage`
- settings binding과 exact validation;
- provider selection과 effective capability compilation;
- internal key/reference/manifest codec;
- SDK/client/presigner lifecycle;
- streaming, hashing, conditional operation;
- operation journal과 reconciliation;
- cleanup/retention execution;
- provider error normalization;
- health/metric/trace/audit instrumentation.
Malware clean/malicious 판정 정책을 adapter configuration이나 mapper에 넣지 않는다.
### 8.4 `adapter:inbound:*`
- HTTP body/multipart limit;
- upload command 생성;
- request content type/name을 untrusted input으로 다룸;
- callback을 통해 bounded chunk를 application에 전달;
- direct transfer grant를 안전한 response DTO로 매핑;
- raw reference 또는 URL logging 방지.
### 8.5 `sample-portfolio`
- image attachment business lifecycle;
- Poster ownership과 permission;
- 허용 media type/size/image decode 정책;
- pending/ready/retired attachment 상태;
- DB transaction/outbox orchestration;
- sample consumer test.
Production leaf가 sample type에 의존하지 않는다.
### 8.6 `app-bootstrap`
- 실제 runtime에서 objectstorage leaf를 classpath에 넣을지 결정;
- required destination/card와 environment profile을 composition;
- startup readiness exposure;
- business use case를 구현하지 않는다.
## 9. Application 계약
### 9.1 Port 분리
초기 target interface family는 다음 의미를 가진다. 이름은 구현 계획에서 Java naming 규칙에
맞춰 확정하되 의미를 합치지 않는다.
```java
public interface ManagedObjectPublicationPort {
ObjectPublishReceipt publish(
ObjectPublishRequest request,
ObjectContentProducer producer);
}
public interface ObjectInspectionPort {
Optional<ObjectDescriptor> inspect(ObjectReference reference);
}
public interface ObjectTransferPort {
ObjectReadReceipt transfer(
ObjectReadRequest request,
ObjectContentConsumer consumer);
}
public interface ObjectRetirementPort {
ObjectMutationReceipt retire(ObjectRetireRequest request);
}
public interface ObjectPurgeMaintenancePort {
ObjectMutationReceipt purge(ObjectPurgeRequest request);
}
public interface ObjectOperationResolutionPort {
ObjectOperationResolution resolve(ObjectOperationKey operationKey);
}
public interface ObjectPublicationHandoffPort {
ObjectHandoffReceipt claimForPublication(ObjectHandoffClaimRequest request);
ObjectHandoffReceipt renewClaim(ObjectHandoffRenewRequest request);
ObjectMutationReceipt releaseClaim(ObjectHandoffReleaseRequest request);
ObjectMutationReceipt authorizeAbort(ObjectAbortAuthorization request);
}
public interface DirectObjectUploadPort {
DirectUploadGrant createUploadGrant(DirectUploadGrantRequest request);
DirectUploadCompletionReceipt completeUpload(DirectUploadCompletionRequest request);
}
public interface DirectObjectDownloadGrantPort {
DirectDownloadGrant createDownloadGrant(DirectDownloadGrantRequest request);
}
public interface DirectMultipartUploadPort {
MultipartSession startMultipart(MultipartStartRequest request);
PartUploadGrant createPartGrant(PartUploadGrantRequest request);
PartReceiptToken acknowledgePart(MultipartPartAcknowledgement request);
MultipartReceipt completeMultipart(MultipartCompleteRequest request);
ObjectMutationReceipt abortMultipart(MultipartAbortRequest request);
}
```
Quarantine가 필요한 workflow에는 single-call `publish`만으로 부족하다. 다음 staged family를
분리한다.
```java
public interface StagedObjectPublicationPort {
ObjectStageReceipt stage(ObjectStageRequest request, ObjectContentProducer producer);
ObjectVerificationReceipt verifyIntegrity(ObjectVerifyRequest request);
ObjectPublishReceipt finalizePublication(ObjectFinalizeRequest request);
ObjectMutationReceipt abort(ObjectAbortRequest request);
}
public interface ObjectScanMaintenancePort {
ObjectReadReceipt transferForScan(
ObjectScanReadRequest request,
ObjectContentConsumer consumer);
ObjectMutationReceipt recordScanVerdict(ObjectScanVerdictRequest request);
}
```
`ManagedObjectPublicationPort.publish`는 compiled destination이 scan-free publication을
명시한 경우에만 사용한다. Scan-gated destination을 이 편의 port로 호출하면
`UNSUPPORTED_CAPABILITY`가 아니라 configuration/programming error로 fail closed하며 반드시
staged family를 사용한다.
모든 mutation request와 resolution은 공통 `ObjectOperationKey`를 포함한다. Operation record의
physical route를 current provider default로 추론하지 않는다. §10.2의 destination route token,
epoch registry, retained binding revision을 통해 exact control namespace를 찾는다.
Direct operation의 의미:
- `completeUpload`: client completion claim을 신뢰하지 않고 exact HEAD/checksum/size/encryption을
검증한 뒤 staged receipt를 만든다;
- `createDownloadGrant`: application authorization 뒤 published exact version에 대한 bounded GET
grant를 만든다;
- `acknowledgePart`: client가 반환한 bounded provider completion claim을 검증하고 server-side
opaque part token/ledger로 바꾼다;
- `completeMultipart`: server ledger의 part token만 받아 provider complete와 reconciliation을
수행한다.
Scan operation은 normal staged publication과 다른 maintenance 권한 port를 사용한다.
`ObjectScanMaintenancePort`의 의미:
- `transferForScan`은 unpublished exact version만 scanner workflow에 bounded read한다;
- `recordScanVerdict`는 object version, scan operation, scanner policy/version precondition이
일치할 때만 `CLEAN`, `MALICIOUS`, `INDETERMINATE` verdict를 기록한다;
- stale/duplicate verdict는 current object에 적용하지 않는다.
`ObjectScanMaintenancePort``ObjectPurgeMaintenancePort`는 일반 business use case에 주입하지
않는다. Scanner workflow용 maintenance composition과 physical purge용 privileged composition도
서로 다른 concrete router/client 권한으로 유지한다. Maintenance
composition 또는 명시적으로 승인된 privileged administration workflow만 사용한다.
`ObjectOperationResolutionPort`는 read-only recovery service가 공유한다.
`ObjectPublicationHandoffPort`는 application DB/outbox와 adapter reaper 사이의 destructive
cleanup fence다. Objectstorage adapter가 persistence repository를 직접 조회하지 않는다.
구현 시 interface를 과도하게 세분화하지 않되 다음 권한 경계는 유지한다.
- inspect/read;
- managed write/stage/finalize;
- scanner unpublished-read/verdict maintenance;
- direct upload, direct download grant, multipart;
- business retirement;
- privileged physical purge;
- maintenance/reconciliation.
Maintenance port는 application business service에 주입하지 않고 운영 job/composition에만
노출한다.
### 9.2 Identity type
필수 identity:
| Type | 의미 | 생성 주체 |
| --- | --- | --- |
| `ObjectDestinationId` | logical storage/security/retention 목적지 | application/config |
| `ObjectOperationEpoch` | operation namespace rotation/rejection epoch | composition/application ID factory |
| `ObjectOperationId` | mutation 한 건의 stable idempotency identity | application |
| `ObjectOperationKey` | destination, epoch, operation을 묶은 모든 mutation의 exact key | application |
| `ObjectId` | immutable data object identity | adapter |
| `ObjectStageHandle` | unpublished exact object를 stage/scan/finalize에만 쓰는 opaque handle | adapter |
| `ObjectReference` | application이 저장하는 opaque published reference | adapter |
| `ObjectVersionToken` | exact immutable generation/version precondition | adapter |
| `DirectTransferSessionId` | direct upload workflow identity | adapter |
| `MultipartPartNumber` | bounded 1-based logical part number | application/adapter validation |
| `PartReceiptToken` | provider part result를 숨긴 opaque token | adapter |
모든 ID는:
- null/blank를 허용하지 않는다;
- canonical text form을 가진다;
- log용 hash/token을 별도로 제공한다;
- provider locator를 encode하지 않는다;
- parsing이 bounded이고 exception message에 secret/raw content를 넣지 않는다.
`ObjectOperationId`는 HTTP request ID와 다르다. 동일 business mutation retry에서 유지되고, 새
사용자 의도에는 새 ID를 사용한다.
`ObjectStageHandle`은 published reference가 아니며 public DTO/download port에 사용할 수 없다.
`ObjectStageReceipt`가 이를 반환하고 finalize success가 처음으로 `ObjectReference`를 만든다.
Destination ID의 provider/namespace binding은 한 번 published operation이 생기면 in-place로
재지정하지 않는다. Provider migration은 새 destination route token/binding revision을 만들고
기존 binding을 read/reconcile/drain 상태로 보존한다. 그렇지 않으면
`destinationId + operationId` retry가 과거 control record를 찾지 못한다.
### 9.3 Content callback
Java standard `InputStream`을 port에 그대로 넘기지 않는다. Baseline은 synchronous callback이다.
```java
@FunctionalInterface
public interface ObjectContentProducer {
void produce(
ObjectContentProductionContext context,
ObjectChunkSink sink)
throws ObjectContentProductionException;
}
public interface ObjectChunkSink {
void write(byte[] bytes, int offset, int length)
throws ObjectChunkWriteException;
}
@FunctionalInterface
public interface ObjectContentConsumer {
void consume(
ObjectContentReadContext context,
ObjectChunkSource source)
throws ObjectContentConsumptionException;
}
public interface ObjectChunkSource {
int read(byte[] destination, int offset, int length)
throws ObjectChunkReadException;
}
```
계약:
- context는 read-only `CancellationView`, remaining deadline/budget checkpoint를 제공한다;
- read context는 body 전에 validated descriptor, exact version, delivered range/length를 제공한다;
- callback이 반환되면 producer/consumer가 provider resource를 보관할 수 없다;
- adapter가 sink/source를 닫고 invalidation한다;
- source는 EOF에 `-1`, positive request에서 progress가 없으면 bounded zero-read 후 protocol
failure를 반환한다;
- offset/length/array bounds를 호출 전에 검증한다;
- producer의 input array는 `write` 반환까지만 유효하며 adapter가 반환 뒤 reference를 보관하지
않는다;
- consumer destination array는 `read`를 호출한 consumer가 소유하며 adapter가 보관하지 않는다;
- configured max chunk보다 큰 write를 쪼개거나 거부한다;
- producer가 던진 application 오류와 storage 오류를 분리한다;
- producer는 한 operation attempt에서 기본적으로 한 번만 호출한다;
- 재호출이 필요하면 request가 repeatable임을 명시하고 별도 factory를 사용한다;
- consumer failure, cancellation, disconnect는 read success가 아니다;
- adapter는 application callback을 SDK event-loop thread에서 실행하지 않는다;
- callback이 blocking임을 contract에 명시한다.
향후 reactive port가 필요하면 별도 capability로 설계한다. `Flow.Publisher`, Reactor type 또는
AWS `AsyncRequestBody`를 이 baseline port에 노출하지 않는다.
### 9.4 Publish request
`ObjectPublishRequest`의 최소 필드:
- `ObjectOperationKey operationKey`;
- `ObjectMediaType declaredMediaType`;
- `ObjectContentIdentity contentIdentity`;
- `ObjectPublicationRequirement publicationRequirement`;
- `ObjectRetentionRequirement retentionRequirement`;
- `ObjectEncryptionRequirement encryptionRequirement`;
- `OperationDeadline deadline`;
- bounded correlation/audit context.
넣지 않는 필드:
- bucket;
- raw key/path;
- region/endpoint;
- AWS storage class enum;
- KMS raw key ARN;
- public ACL;
- arbitrary metadata map;
- original filename;
- inbound DTO.
Destination binding이 provider와 namespace, maximum size, checksum, encryption, retention,
direct-transfer 허용 여부를 결정한다.
R2 `ObjectContentIdentity`는 다음 중 하나다.
- exact byte length + expected full SHA-256;
- adapter가 검증 가능한 immutable source reference/revision + expected length/digest.
Maximum size만 있고 payload digest가 없는 one-shot upload는 다른 payload로 operation ID를
오용했을 때 conflict를 검출할 수 없다. 이 경로는 R0/R1 compatibility로만 허용하고 R2
publication card에 포함하지 않는다.
### 9.5 Read request
`ObjectReadRequest`:
- exact `ObjectReference`;
- optional expected `ObjectVersionToken`;
- `ObjectReadRange`;
- digest verification mode;
- maximum delivered bytes;
- deadline/cancellation.
Public `ObjectTransferPort`는 caller가 publication/scan requirement를 낮추는 field를 받지 않는다.
항상 compiled destination minimum인 `PUBLISHED`와 required `SCAN_CLEAN`을 강제한다. Unpublished
exact-version read는 별도 narrow scan/maintenance port와 bean만 제공한다.
Baseline range는 하나의 contiguous `(offset, length)`다.
- offset은 0 이상;
- length는 1 이상이며 destination maximum 이하;
- object end를 넘는 range의 exact 결과를 정의한다;
- suffix/multi-range HTTP 문법은 inbound에서 canonical form으로 변환한다;
- provider range response의 실제 offset/length/content-range를 검증한다.
### 9.6 Descriptor와 receipt
`ObjectDescriptor`:
- opaque reference;
- exact version token;
- logical size;
- declared/detected media type;
- content digest algorithm/value;
- publication state;
- scan state;
- encryption profile ID;
- retention state;
- created/published timestamp;
- schema version.
`ObjectPublishReceipt`:
- operation ID;
- request fingerprint;
- opaque reference;
- exact version;
- size;
- digest;
- media type;
- terminal outcome;
- applied timestamp;
- effective capability descriptor revision.
Receipt에는 bucket, provider key, filesystem path, endpoint, raw ETag, upload ID를 넣지 않는다.
Provider-specific evidence는 private operation record에만 저장한다.
### 9.7 Capability requirement
Request는 필요한 보장을 typed enum/set으로 표현한다.
- immutable create;
- exact version read;
- conditional retirement;
- SHA-256 verification;
- scan-gated publication;
- direct upload;
- direct multipart;
- retention hold;
- server-side encryption profile;
- response-loss reconciliation.
Binding compiler는 destination의 required capability와 provider의 effective capability를
startup에서 대조한다. Runtime request가 compiled binding보다 강한 보장을 요구하면 호출 전에
`UNSUPPORTED_CAPABILITY`로 거부한다.
Runtime request의 scan, encryption, retention, checksum, publication requirement는 destination
minimum을 강화할 수만 있고 낮출 수 없다. Weaker request는 stronger destination policy로
승격하거나 ambiguous하면 fail closed한다.
## 10. Logical key, object reference, namespace
### 10.1 Internal data key
Provider key는 adapter 내부에서 생성한다. 예시 grammar:
```text
data/v1/<destination-token>/<shard>/<object-id>/<generation>
```
요구사항:
- ASCII lower-case의 제한된 alphabet;
- segment 길이와 전체 길이 제한;
- `.`/`..`, empty segment, slash alias 금지;
- percent/Unicode normalization ambiguity 금지;
- tenant/user/original filename/email 같은 PII 금지;
- object ID에서 deterministic shard 계산;
- destination별 private prefix 고정;
- generation은 immutable create마다 새 값;
- canonical encoder와 parser에 property test;
- provider별 key normalization 차이를 adapter kernel에서 제거.
### 10.2 Control namespace
```text
control/v1/operations/<destination-token>/<operation-epoch>/<shard>/<operation-id>
control/v1/references/<destination-token>/<shard>/<reference-id>
control/v1/manifests/<destination-token>/<shard>/<object-id>/<revision>
control/v1/multipart/<destination-token>/<shard>/<session-id>
control/v1/reaper-cursors/<destination-token>/<job-id>
control/v1/operation-epochs/<destination-token>/<operation-epoch>
```
Data와 control prefix는 IAM/policy와 lifecycle에서 분리한다. Runtime identity가 broad bucket
list/delete 권한을 갖지 않도록 operation별 최소 prefix 권한을 설계한다.
`destination-token`은 stable, non-secret route identity다. 동일 token의 provider/namespace를
in-place로 바꾸지 않는다. Migration은 새 token을 만들며 old token의 binding revision을
read/reconcile/retire 기간 동안 보존한다.
### 10.3 Public opaque reference
예시 외형:
```text
osr1.<route-token>.<random-object-token>.<check-digits>
```
Reference는:
- provider/bucket/key를 복호화할 수 없는 opaque value;
- 오타 탐지용 check digits;
- schema/version prefix;
- destination route token;
- 충분한 entropy;
- application DB에 저장 가능한 bounded string;
- secret이 아니지만 log에서는 hash/token 처리;
- authorization을 대체하지 않음;
- public download URL이 아님.
Reference를 받은 사용자는 application authorization을 통과해야만 stream 또는 presigned grant를
받는다.
Route registry는:
- token -> destination/binding revision history를 durable하게 보존;
- destination display-name rename과 route identity를 분리;
- live reference/operation/tombstone가 남아 있으면 token 삭제·재사용 금지;
- provider migration 시 old route를 read/reconcile/retire 상태로 유지;
- unknown/removed route를 current default provider로 보내지 않음;
- route alias/tombstone retention을 reference maximum lifetime보다 길게 유지
한다. Startup validation은 token collision/reuse와 required old binding 부재를 hard fail한다.
### 10.4 Original filename와 user metadata
Original filename은 object key로 사용하지 않는다. Business상 필요하면:
- inbound에서 control character/path separator를 제거한다;
- length를 제한한다;
- public response용 display metadata로 application DB에 저장한다;
- storage adapter control record에는 allowlisted, encoded metadata만 둔다;
- log/metric tag에 넣지 않는다.
Arbitrary `Map<String, String>` metadata는 baseline port에 없다. 필요한 metadata는 typed,
versioned field로 추가한다.
## 11. Operation identity와 request fingerprint
### 11.1 Canonical fingerprint
Fingerprint는 content bytes 자체가 아니라 immutable request intent를 canonical encode한 뒤
SHA-256으로 계산한다.
포함:
- schema version;
- destination ID;
- operation kind;
- declared media type canonical form;
- exact/maximum size expectation;
- exact R2 content identity 또는 explicit R1 compatibility marker;
- publication/scan requirement;
- encryption/retention profile;
- direct/multipart parameter;
- prior reference/version precondition.
제외:
- request ID, trace ID;
- current time;
- provider-generated key/upload ID;
- presigned URL;
- credential;
- transient retry count.
Canonical encoding은:
- field 순서 고정;
- enum canonical name 고정;
- number decimal encoding 고정;
- absence와 empty를 구분;
- Unicode normalization 정책 고정;
- schema version 포함;
- golden vector test 보유.
### 11.2 Same-operation decision
| 기존 operation | 새 요청 | 결과 |
| --- | --- | --- |
| 없음 | valid fingerprint | reserve 후 실행 |
| non-terminal, same fingerprint | retry | current state/continuation 반환 |
| terminal success, same fingerprint | retry | 저장된 receipt 반환, producer 재호출 금지 |
| terminal failure, same fingerprint | retry | 정책에 따라 same failure 또는 explicit new operation 요구 |
| 어떤 상태든 different fingerprint | retry | `OPERATION_CONFLICT` |
| unknown/newer schema | retry | fail closed, manual/upgrade reconciliation |
Operation ID uniqueness만 보고 dedupe하지 않는다. Fingerprint 비교가 필수다.
### 11.3 Content digest와 operation fingerprint 분리
두 digest를 혼동하지 않는다.
- request fingerprint: 같은 사용자 의도인지 판정;
- content digest: 업로드된 byte가 기대한 content인지 판정.
R2 expected content digest는 fingerprint에 포함하고 adapter가 streaming 중 계산한 값과 비교한다.
R1 compatibility 경로는 adapter가 discovered digest를 기록하지만, terminal retry에서 다른
payload 오용을 검출하지 못한다는 보장 한계를 descriptor/receipt에 표시한다. Direct upload는
server가 bytes를 직접 보지 않을 수 있으므로 provider checksum/head 또는 별도 verification
read가 필요하다.
### 11.4 Replay horizon과 tombstone
Destination은 다음보다 긴 `minimumOperationReplayHorizon`을 가진다.
- public API idempotency retry horizon;
- outbox/redelivery/dead-letter recovery horizon;
- maximum worker outage;
- provider indeterminate reconciliation horizon.
Operation identity는 `destination route + operation epoch + operation ID`다.
- active epoch에서는 새 operation을 받을 수 있다;
- active epoch의 per-operation tombstone은 개별 horizon 경과만으로 삭제하지 않는다;
- compaction하려면 epoch를 먼저 `SEALED`로 바꿔 신규 operation을 거부한다;
- seal 뒤 maximum replay horizon과 indeterminate/retention recovery가 모두 끝날 때까지 individual
receipt/tombstone을 유지한다;
- 그 뒤 immutable epoch rejection record를 남기고 per-operation tombstone을 compact할 수 있다;
- sealed/compacted epoch로 온 어떤 operation ID도 exact lookup 결과와 무관하게
`OPERATION_EXPIRED`를 반환한다;
- epoch token은 destination lifetime 동안 재사용하지 않는다.
따라서 tombstone 삭제 뒤 “record 없음”을 새 operation으로 오인하지 않는다. Current active
epoch의 tombstone을 지우고 같은 epoch를 계속 쓰는 구현은 금지한다. Application operation-ID
factory와 outbox는 epoch를 operation과 함께 durable하게 보존한다.
### 11.5 Bounded epoch rotation
```text
WARM -> ACTIVE -> DRAINING -> SEALED -> COMPACTED
```
Finite destination settings:
- `epochMaxAge`;
- `epochMaxOperations`;
- `epochMaxControlBytes`;
- maximum concurrently readable old epochs;
- drain/replay/compaction deadline.
Rotation:
1. 새 epoch를 `WARM`으로 만들고 codec/policy/binding 및 모든 reader가 old/new lookup을 지원하는지
qualification한다.
2. Durable current-write-epoch pointer를 CAS로 새 epoch에 전환한다.
3. Old epoch를 `DRAINING`으로 바꾸고 stale writer admission을 거부한다.
4. maximum in-flight request/outbox dispatch horizon 동안 old operation continuation/replay만
허용한다.
5. unresolved indeterminate operation이 없고 drain fence가 확인되면 `SEALED`한다.
6. replay/retention horizon 뒤 immutable rejection record를 남기고 `COMPACTED`한다.
Pointer unavailable, stale writer, seal race는 fail closed한다. Rollback은 새 epoch에 operation이
없을 때만 pointer를 되돌리거나 또 다른 epoch를 사용한다. Multi-pod cutover, delayed outbox,
partitioned writer, restore를 rolling/fault test한다.
## 12. 분리된 상태 머신
Publication operation, published reference lifecycle, retirement/purge mutation, direct grant
session, multipart session을 한 enum에 합치지 않는다. 각각 별도 record와 stable operation
identity를 가진다.
### 12.1 Publication operation
```text
RESERVED
-> DATA_UPLOAD_IN_PROGRESS
-> DATA_UPLOADED
-> INTEGRITY_VERIFIED
-> SCAN_PENDING
-> SCAN_CLEAN
-> REFERENCE_PUBLISH_IN_PROGRESS
-> PUBLISHED
```
Terminal:
```text
PUBLISHED | ABORTED | QUARANTINED | EXPIRED | FAILED | CORRUPT
```
Scan이 필요 없는 destination은 `INTEGRITY_VERIFIED -> REFERENCE_PUBLISH_IN_PROGRESS`
전이한다. `PUBLISHED`는 이 publication operation의 immutable terminal이다. 이후 retirement가
같은 record를 변경하지 않는다.
### 12.2 Scan sub-state
```text
NOT_REQUIRED
PENDING
CLEAN
MALICIOUS
INDETERMINATE
```
- destination minimum이 scan을 요구하면 caller가 `NOT_REQUIRED`로 낮출 수 없다;
- verdict는 exact object version, scan operation, scanner policy revision, fence를 묶는다;
- `MALICIOUS`는 publication operation을 `QUARANTINED` terminal로 보낸다;
- `INDETERMINATE`는 clean이 아니며 scan retry/운영 판정 전 publish를 막는다.
### 12.3 Published reference lifecycle
```text
PUBLISHED
-> RETIREMENT_PENDING
-> RETIRED
-> PURGE_ELIGIBLE
-> PURGED
```
Reference lifecycle record는 publication receipt와 별도다.
- retirement mutation마다 새 `ObjectOperationId`와 fingerprint를 사용한다;
- physical purge도 retirement와 다른 privileged operation ID를 사용한다;
- `RETIRED`는 business visibility 제거이며 physical purge와 다르다;
- retention/legal hold는 purge operation outcome을 `HELD`로 만들며 reference record를 임의로
`PURGED`로 승격하지 않는다;
- reference가 가리키는 immutable generation은 lifecycle 전체에서 덮어쓰지 않는다.
### 12.4 Direct grant session
```text
SESSION_RESERVED
-> GRANT_PREPARED
-> GRANT_ISSUED
-> UPLOAD_VERIFICATION_IN_PROGRESS
-> DATA_UPLOADED
```
Terminal/branch:
```text
EXPIRED | ABORTED | FAILED
```
Grant generation, constraints digest, signing revision, expiry, outstanding-grant exposure는 session
record가 소유한다. Download grant도 별도 grant-operation record와 published reference revision
precondition을 가진다. Grant response uncertainty는 stable grant state와 pending-effect certainty로
표현한다. 상세 발급/reissue linearization은 §18.6을 따른다.
### 12.5 Multipart session
```text
SESSION_RESERVED
-> INITIATE_IN_PROGRESS
-> ACCEPTING_PARTS
-> COMPLETE_IN_PROGRESS
-> COMPLETED
```
Terminal/branch:
```text
ABORTED | EXPIRED | FAILED | CORRUPT
```
Session record는 provider upload ID, part ledger, grant fence를 private하게 보존한다. `COMPLETED`
되면 publication operation의 `DATA_UPLOADED` evidence로 연결되지만 두 record를 같은 state로
간주하지 않는다.
### 12.6 Pending effect와 certainty
단일 `INDETERMINATE` state로 phase를 지우지 않는다. 모든 external mutation 직전에 control
record를 CAS하여 다음을 기록한다.
```text
stablePhase
pendingEffect {
kind
attemptId
exactTargetAndVersion
desiredRevisionOrState
precondition
requestEvidenceDigest
}
certainty = NOT_SENT | SENT | CONFIRMED | INDETERMINATE
```
`kind` 예:
- `DATA_PUT`;
- `DIRECT_GRANT_ISSUE`;
- `DIRECT_UPLOAD_VERIFY`;
- `DIRECT_DOWNLOAD_GRANT_ISSUE`;
- `MULTIPART_INITIATE`;
- `MULTIPART_PART`;
- `MULTIPART_COMPLETE`;
- `MULTIPART_ABORT`;
- `REFERENCE_CAS`;
- `REFERENCE_RETIRE`;
- `OBJECT_PURGE`;
- `CONTROL_RECORD_CAS`.
따라서 `DATA_PUT_INDETERMINATE`, `REFERENCE_CAS_INDETERMINATE`,
`MULTIPART_COMPLETE_INDETERMINATE`, `OBJECT_PURGE_INDETERMINATE`를 서로 다른 reconciler
branch로 보낸다.
### 12.7 전이 규칙
- terminal operation record는 immutable하게 보존한다;
- state version/CAS와 pending-effect precondition이 일치할 때만 전이한다;
- state마다 allowed predecessor를 고정한다;
- 같은 terminal transition retry는 stored receipt를 반환한다;
- out-of-order callback은 무시하지 말고 conflict/audit한다;
- object data 존재만으로 publication state를 승격하지 않는다;
- state와 provider evidence가 충돌하면 `CORRUPT` 또는 phase-specific indeterminate로 격리한다;
- quarantined/unpublished object는 public transfer/grant 대상이 아니다.
### 12.8 Mutation response loss
Response loss가 발생하면:
1. pending effect와 certainty를 `INDETERMINATE`로 유지한다;
2. 같은 mutation을 blind retry하지 않는다;
3. effect kind에 맞는 exact key/version/upload session/control revision을 조회한다;
4. size, digest, metadata, version, state evidence를 비교한다;
5. `APPLIED`, `NOT_APPLIED`, `PRECONDITION_FAILED`, 계속 `INDETERMINATE` 중 하나로 resolve한다;
6. resolve 결과에 따라 continuation 또는 compensation을 수행한다.
## 13. Durable control plane
### 13.1 필요성
Provider object data만으로 다음을 복구할 수 없다.
- operation ID와 request fingerprint;
- scan/publication 상태;
- provider response loss 전후 의도;
- old/new attachment reference;
- cleanup ownership/age;
- multipart part ledger;
- schema/policy/encryption revision;
- terminal receipt.
따라서 R2에는 durable control plane이 필요하다.
### 13.2 Source of truth
초기 선택은 같은 object storage의 private control namespace에 versioned canonical record를 두는
것이다.
- data plane과 failure domain이 같아 disaster recovery가 단순하다;
- conditional create/CAS를 provider capability로 검증할 수 있다;
- application DB schema와 capability internals를 분리한다.
단, control record와 data object는 여전히 단일 atomic transaction이 아니다. protocol과
reconciliation이 crash gap을 닫는다.
Control plane을 별도 database에 두는 선택도 가능하지만 이 문서의 baseline이 아니다. 바꾸려면:
- ownership과 dependency edge;
- transaction/capacity/failover;
- dual-store recovery;
- backup/restore ordering
을 별도 설계로 승인한다.
### 13.3 Record schema
Operation record v1 최소 필드:
- schema version;
- operation ID;
- canonical request fingerprint;
- operation kind;
- destination route token;
- provider/binding/policy revision;
- canonical non-secret effective policy snapshot와 digest;
- key grammar/codec/checksum/encryption/retention plan revision;
- logical credential/key reference revision;
- internal object ID/key hash;
- opaque reference;
- operation family, stable phase, family-specific state/state version;
- pending effect, attempt ID, precondition, request evidence digest, certainty;
- expected/observed size;
- expected/observed content digest;
- provider checksum algorithm/value;
- provider version/ETag private evidence;
- multipart opaque session and completed-part ledger;
- encryption/retention/scan profile revision;
- timestamps and bounded lease/fence;
- last normalized outcome/error;
- terminal public receipt;
- minimum replay-until/terminal tombstone epoch;
- cleanup ownership/eligibility;
- application handoff/cleanup authorization fence;
- audit correlation token.
Record는 canonical format으로 encode하고:
- checksum/MAC 또는 authenticated encryption 정책;
- maximum record size;
- unknown field policy;
- schema upgrade/downgrade behavior;
- corruption handling;
- golden fixtures
를 테스트한다. Secret, presigned URL, raw credential은 기록하지 않는다.
Reserve 시 effective execution policy를 freeze한다. Same operation retry/reconcile은:
- 저장된 policy snapshot과 digest;
- 저장된 provider/binding revision;
- 저장된 key/codec/checksum/encryption/retention plan;
- 당시의 logical credential/key revision
만 사용한다. Current configuration으로 다시 resolve하거나 silent fallback하지 않는다. 필요한 old
revision을 복원할 수 없으면 `POLICY_REVISION_UNAVAILABLE`로 fail closed하고 manual
reconciliation 대상으로 보낸다.
Old revision은 maximum operation/replay/retention horizon보다 길게 보존한다. 새 policy는 새
operation에만 적용한다. Rolling deployment test는 old revision read/continue와 new revision
write를 함께 검증한다.
### 13.4 Conditional update
R2 provider는 control record에 다음 중 하나를 증명해야 한다.
- create-if-absent와 exact version compare-and-swap;
- immutable revision append + conditional current pointer;
- 동등한 linearizable primitive.
지원하지 않는 provider는 multi-node deterministic recovery를 claim하지 않는다.
LIST는 reaper candidate discovery에만 사용한다. Operation/reference direct lookup의 source of
truth는 deterministic exact key GET/HEAD다.
### 13.5 Schema evolution
- reader는 자신보다 낮은 supported version을 migrate in memory할 수 있다;
- writer는 deployment의 selected write version만 쓴다;
- newer version은 삭제/overwrite하지 않고 `UNSUPPORTED_CONTROL_SCHEMA`로 격리한다;
- rolling deployment 동안 old/new reader compatibility matrix를 CI에서 검증한다;
- downgrade 전에 write-version gate를 낮추는 별도 단계가 필요하다;
- cleanup job은 unknown schema를 보고만 하고 삭제하지 않는다.
## 14. Managed upload protocol
### 14.1 Plan
Adapter는 content producer를 호출하기 전에:
1. operation key의 destination route token, epoch, operation ID로 existing record 또는 sealed
epoch rejection record를 exact lookup한다;
2. existing record가 있으면 frozen binding/policy revision을 복원하고 fingerprint를 비교한다;
3. record가 없을 때만 current destination binding과 capability requirement를 resolve한다;
4. operation ID/fingerprint와 R2 content identity를 검증한다;
5. immutable object ID/key와 opaque reference를 생성한다;
6. size/digest/encryption/retention limit와 effective policy snapshot/digest를 freeze한다;
7. `RESERVED` control record를 create-if-absent한다;
8. winning record를 다시 읽어 same-operation decision을 수행한다;
9. operation-scoped resource budget을 예약한다.
이 단계에는 data write가 없다.
### 14.2 Upload
1. control state를 CAS로 `DATA_UPLOAD_IN_PROGRESS`로 전이하고 `DATA_PUT` pending effect를
`NOT_SENT`로 기록한다.
2. provider immutable-create request를 열기 직전에 pending effect certainty를 `SENT`
전이한다.
3. producer callback의 chunk를 bounded buffer로 전달한다.
4. 동시에 logical byte count와 SHA-256을 계산한다.
5. provider transport checksum을 지원하면 별도로 계산/전송한다.
6. maximum bytes를 넘기기 전에 sink를 중단한다.
7. producer, adapter, SDK 오류를 서로 다른 normalized cause로 기록한다.
8. successful provider response에서 exact version/checksum/encryption evidence를 보존한다.
9. response가 확정되면 pending effect를 confirm/clear하고 `DATA_UPLOADED`로 전이한다.
불확정이면 stable phase는 `DATA_UPLOAD_IN_PROGRESS`, pending effect kind는 `DATA_PUT`,
certainty는 `INDETERMINATE`로 유지한다.
Producer exception 뒤에는 partial provider upload를 abort/cleanup한다. Cleanup 실패가 원래
producer 오류를 덮지 않으며 operation record에 별도 evidence로 남는다.
### 14.3 Integrity verification
검증 우선순위:
1. caller expected SHA-256과 adapter-calculated SHA-256 비교;
2. sent provider checksum과 provider response/head checksum 비교;
3. exact object size 비교;
4. exact immutable version 확인;
5. encryption/retention response attestation 확인;
6. provider 특성상 response evidence가 불충분하면 bounded verification read.
불일치 시 object를 publish하지 않고 `QUARANTINED` 또는 `CORRUPT`로 보낸다. 단순 retry로
정상화하지 않는다.
### 14.4 Scan
Scan이 필요한 destination:
```text
DATA_UPLOADED
-> INTEGRITY_VERIFIED
-> scanState=PENDING
-> scanner application workflow
-> scanState=CLEAN | MALICIOUS | INDETERMINATE
```
`scanState=CLEAN`만 publication state를 `SCAN_CLEAN`으로 진행시킨다. `MALICIOUS`
`QUARANTINED` terminal, `INDETERMINATE``SCAN_PENDING` publication state에 머문다.
Object Storage adapter는 scanner SDK나 business verdict를 소유하지 않는다. 다음 seam만 제공한다.
- unpublished exact version에 대한 authorized bounded read;
- scan operation correlation;
- clean/malicious/indeterminate verdict를 conditional state transition으로 기록;
- stale verdict가 새 version에 적용되지 않도록 version precondition;
- scanner unavailable 시 fail-closed publication.
Scanner capability는 별도 outbound adapter가 application port 뒤에서 구현한다. Objectstorage
leaf가 sibling adapter를 직접 의존하지 않는다.
### 14.5 Publish
기본 publication:
1. required integrity/scan state를 재검증한다;
2. immutable manifest revision을 기록한다;
3. reference current pointer를 conditional create/CAS한다;
4. `PUBLISHED` terminal receipt를 operation record에 보존한다;
5. caller에게 opaque receipt를 반환한다.
Data object key는 이동/복사/ACL 변경하지 않는다. Reference lookup이 private immutable object의
exact version을 가리킨다.
Manifest/pointer write response가 유실되면 reference exact GET과 revision/digest 비교로
reconcile한다.
### 14.6 Abort
Abort는:
- terminal published object를 지우지 않는다;
- non-terminal object와 multipart session만 대상으로 한다;
- operation ID/fingerprint를 확인한다;
- active publication handoff claim이 있으면 destructive abort를 거부한다;
- business-owned verified stage는 DB upload-intent의 terminal abort fence에 묶인
`ObjectAbortAuthorization`을 검증한다;
- conditional state transition으로 single owner를 확보한다;
- provider delete/abort 결과를 reconcile한다;
- retention/hold가 있으면 `HELD`를 반환한다;
- cleanup failure를 숨기지 않는다.
### 14.7 Empty object
Empty object 지원 여부를 destination별로 명시한다.
- generic binary destination은 size 0을 허용할 수 있다;
- poster image 같은 business destination은 application policy로 거부한다;
- checksum은 empty SHA-256의 정상 값으로 계산한다;
- multipart는 empty object에 사용하지 않는다.
## 15. Database attachment workflow
### 15.1 금지 shape
```text
DB transaction {
remote upload
aggregate save
}
```
이 shape는 사용하지 않는다.
### 15.2 권장 workflow
Poster image 교체 예시:
```text
1. authorize + validate request
2. stable attachment operation 생성
3. short DB transaction:
Poster exists/version 확인
versioned HMAC key epoch을 lock하고 raw tenant/principal/Idempotency-Key를
domain-separated alias/digest로 변환
tx.inWrite(() -> IdempotencyExecutor.execute(
sanitizedContext,
create-or-read UploadIntent(operation, expected poster version, content identity, RESERVED),
bounded reservation codec))
generic idempotency COMPLETED reservation과 UploadIntent를 함께 commit
4. object stage/upload outside DB transaction
5. integrity verify + required scan
6. object publication handoff claim을 획득
7. short DB transaction:
UploadIntent가 RESERVED이고 operation/fence가 같은지 CAS
pending attachment(stage handle, operation, expected object version, handoff fence) 저장
UploadIntent를 PENDING으로 전이
outbox AttachmentPrepared(handoff fence) 기록
8. committed UploadIntent worker가 PENDING row를 claim하고 handoff claim을 갱신한 뒤
object publication finalize
9. short DB transaction:
same pending stage handle인지 CAS 확인
finalize receipt의 published reference로 교체해 READY 승격
UploadIntent를 READY로 전이
old reference를 retirement queue/outbox에 추가
outbox AttachmentReady 기록
10. handoff claim release
11. async old-reference retirement
```
UploadIntent는 remote transfer 전에 commit되므로 “DB row가 아직 commit될 수 있는 중인데 absence를
읽고 abort”하는 race를 제거한다. Handoff fence는 secret authorization이 아니라
operation/version/claim generation에 묶인 opaque value다. DB commit 여부를 objectstorage
adapter가 추측하지 않는다.
TX1은 generic idempotency claim, UploadIntent create/read, bounded reservation COMPLETE가 같은
PostgreSQL transaction에 참여한다. Commit 전 crash는 둘 다 남기지 않고, commit 후에는
COMPLETED generic row와 matching intent가 함께 남는다. Scope claim은 transaction을
unique-violation으로 poison하지 않는 PostgreSQL `ON CONFLICT` claim primitive를 사용한다.
Versioned key epoch과 retained HMAC key lookup은 rolling rotation 중 old/new digest가 서로 다른
intent를 만들지 못하게 하며, generic row와 intent에는 raw header/principal/tenant를 저장하지
않는다.
Multipart HTTP 요청의 `RequestFingerprint`는 raw body나 multipart boundary의 hash가 아니다.
`poster-image-publication-fingerprint-v1` canonical codec이 schema/domain separator, Poster ID, expected
aggregate version, destination/profile, normalized media type, declared content length, 그리고
요청 전에 제출된 full-file SHA-256을 length-prefixed bytes로 직렬화한 semantic digest다.
Filename, multipart boundary, part/header 순서와 transport-only header는 제외한다. 같은 의미의
재시도는 같은 fingerprint가 되고, content identity나 business precondition이 바뀌면 반드시
달라지는 golden/property test를 둔다.
HTTP multipart producer는 request lifetime에 묶이므로 47은 successful `202` 반환 전에 같은
request invocation에서 끝난다. Worker는 request body가 없는 `RESERVED` intent를 임의로 stage하지
않는다. TX1 뒤 stage 전 crash는 same-idempotency-key/same-fingerprint retry가 같은 operation에 새
producer를 공급하고, retry가 없으면 bounded expiry/abort/report 대상으로 남긴다. Worker가
비동기로 소유하는 구간은 durable `PENDING` 이후 finalize/READY다.
`AttachmentPrepared`/`AttachmentReady` outbox row는 versioned integration notification/audit다.
Object publication의 canonical work queue와 recovery source of truth는
`poster_image_upload_intent`이며, outbox/broker delivery만을 유일한 wake-up 또는 object operation
journal로 사용하지 않는다.
### 15.3 Crash gap
| Crash 위치 | 남는 상태 | 복구 |
| --- | --- | --- |
| TX1 commit 후 stage 전 | COMPLETED reservation + DB RESERVED intent, no object | same-key/same-fingerprint HTTP retry가 새 request producer로 same operation stage; retry가 없으면 expiry/abort report |
| upload 중 | DB RESERVED intent + pending/partial provider effect | exact provider evidence가 있으면 resolver가 reconcile; bytes 재현이 필요하면 HTTP retry producer만 continuation; worker가 request bytes를 invent/replay하지 않음 |
| verified 후 handoff 전 | DB RESERVED intent + exact verified stage evidence | worker가 exact stage/operation claim; staged evidence가 없으면 worker가 stage하지 않음 |
| handoff 후 pending DB commit 전 | active claim + DB RESERVED intent | worker가 pending CAS; claim expiry 후에만 abort CAS |
| pending DB commit 후 finalize 전 | DB PENDING + active/frozen claim | UploadIntent worker가 same operation finalize/claim renew |
| finalize 후 ready DB commit 전 | published reference + pending DB | worker가 reference inspect 후 ready CAS |
| ready DB 후 old retire 전 | new ready + old published | retirement outbox 재처리 |
| retire response loss | old state indeterminate | exact reference/version reconcile |
Verified/staged object는 age만으로 reaper가 삭제하지 않는다. Application maintenance가
operation ID로 business DB/UploadIntent와 관련 outbox evidence를 조회한 뒤:
- pending/ready면 claim을 획득·갱신하거나 finalize를 재개;
- `UploadIntent=RESERVED`이고 active claim이 없으면 DB CAS로 `ABORT_AUTHORIZED` terminal과
fence를 먼저 기록한 뒤 exact object/version `ObjectAbortAuthorization`을 발급;
- worker의 late `RESERVED -> PENDING` CAS는 `ABORT_AUTHORIZED` 뒤 실패하며 새 operation으로
restage/manual recovery;
- object는 있는데 matching UploadIntent가 없으면 auto-delete하지 않고 corruption quarantine;
- DB/outbox 확인이 불가능하면 keep/quarantine하고 alert
한다. DB absence read만으로 abort authorization을 만들지 않는다. 이 protocol은 orphan leak을
data loss보다 우선하며 cleanup-vs-late-commit/claim-expiry race를 fault test한다.
### 15.4 Truth priority
- business visibility의 source of truth는 DB attachment `READY` 상태다;
- object availability의 source of truth는 published reference manifest다;
- 둘 중 하나만 ready이면 workflow가 non-terminal이다;
- HTTP download는 DB authorization/READY 확인 뒤 object reference를 resolve한다;
- DB row 삭제만으로 object가 물리 삭제되었다고 간주하지 않는다.
### 15.5 Concurrency
동일 Poster에 concurrent image upload가 오면:
- 각 upload는 새 immutable reference를 만든다;
- DB aggregate version/pending attachment CAS로 winner를 결정한다;
- loser object는 publish하지 않거나, 이미 publish되었으면 TX3에서 intent를
`SUPERSEDED` terminal로 전이하면서 exact losing reference/version retirement row를 같은
transaction에 기록한다. CAS 예외로 TX 전체를 rollback해 cleanup evidence를 잃지 않는다;
- deterministic final key overwrite를 하지 않는다;
- retry는 same operation ID를 유지한다;
- user가 새 image를 선택한 새 의도에는 새 operation ID를 사용한다.
### 15.6 Delete
Poster 삭제:
1. short DB transaction에서 business 삭제와 reference retirement intent/outbox를 기록한다;
2. object retirement worker가 visibility를 제거한다;
3. retention 정책 뒤 physical purge eligibility를 계산한다;
4. exact version conditional purge를 시도한다;
5. hold/retention은 정상 `HELD` 결과로 보존한다.
Object delete 실패 때문에 이미 승인된 business delete transaction을 장시간 붙잡지 않는다.
### 15.7 Legacy object adoption
기존 raw locator를 opaque reference로 채택하는 기능은 일반 upload/publish port에 섞지 않는다.
Deprecated administrative migration seam과 명시적인 maintenance profile에서만
`REPORT_ONLY` 또는 `APPLY`로 실행한다. `REPORT_ONLY`가 먼저 exact legacy namespace inventory와
HEAD/read SHA-256 evidence를 bounded manifest로 만들며 mutation은 하지 않는다.
`APPLY`는 그 immutable manifest와 별도의 canonical Ed25519 이중 승인 문서가 정확히 일치해야
한다. 승인 문서는 schema version, adoption operation ID, manifest SHA-256, legacy namespace
digest, target destination/namespace, literal mode `APPLY`, 유효 시간, nonce, 서로 다른 trusted
approver 두 명을 묶는다. Permission-checked configured key file 외의 key는 신뢰하지 않는다.
Verifier는 두 서명과 모든 binding을 확인하고 nonce replay record를 exact operation/digest에
CAS한 뒤, manifest bytes를 다시 hash한 다음에만 per-row inspect/digest/adoption CAS를 허용한다.
동일 terminal operation replay만 idempotent하며 다른 binding의 nonce 재사용은 거부한다.
Scheduled maintenance runner는 web `SecurityContext`에 의존하지 않고 격리된 execution
identity와 administrative capability를 명시적으로 전달한다. Normal context에는 migration
port, verifier, replay store, runner bean이 하나도 없다. 적용 중 DB CAS loser나
missing/corrupt/unknown-version object는 durable evidence와 reconciliation 대상으로 남기며,
자동 overwrite/copy/delete를 하지 않는다. Legacy object retirement는 별도 exact-reference
retirement authorization 뒤에 수행한다.
## 16. Read, HEAD, range
### 16.1 Inspect
`inspect(reference)`는:
1. reference syntax/check digits를 검증한다;
2. deterministic reference control key를 exact GET한다;
3. supported schema와 `PUBLISHED` 상태를 확인한다;
4. exact immutable data key/version을 얻는다;
5. 필요 시 provider HEAD로 size/checksum/version/encryption을 확인한다;
6. normalized descriptor만 반환한다.
`Optional.empty`는 reference가 존재하지 않을 때만 사용한다. 다음은 typed error다.
- malformed reference;
- forbidden destination;
- quarantined/retired;
- provider unavailable;
- corrupt manifest;
- unsupported schema.
### 16.2 Server-mediated download
- application authorization이 먼저다;
- adapter가 exact version/range precondition으로 provider read를 연다;
- configured buffer와 delivered-byte limit를 적용한다;
- consumer callback이 반환/실패하면 provider response body를 확실히 close/cancel한다;
- full read digest verification은 전체 object를 모두 읽었을 때만 success다;
- range read는 whole-object digest를 “검증 완료”로 표시하지 않는다;
- optional chunk/range digest가 manifest에 있을 때만 range integrity를 별도 증명한다;
- short read, excess read, wrong content-range는 provider protocol failure다;
- client disconnect는 partial delivery outcome이며 storage success와 구분한다.
### 16.3 Direct download
Presigned GET은:
- DB authorization과 published/scan-clean 확인 후 발급한다;
- exact immutable key/version과 response header profile에 묶는다;
- short TTL과 maximum download size policy를 적용한다;
- URL을 log/audit payload에 저장하지 않는다;
- revocation이 필요한 resource에는 사용하지 않거나 매우 짧은 TTL을 사용한다;
- reference retire 후에도 이미 발급한 URL이 만료 전 유효할 수 있음을 계약에 명시한다;
- AWS는 request 시작 시 expiry를 평가하므로 expiry 직전 시작한 transfer가 이후 계속될 수 있고
connection이 끊겨 재시도하면 실패할 수 있음을 client contract에 명시한다;
- AWS card는 bucket policy의 bounded `s3:signatureAge` upper bound를 qualification 후보로 둔다.
One-time URL이라고 부르지 않는다.
### 16.4 HTTP Range mapping
Inbound HTTP adapter가 `Range`를 지원할 때:
- single range만 baseline으로 허용한다;
- unsatisfiable range는 application typed outcome을 HTTP 416으로 매핑한다;
- `If-Range`, ETag 같은 HTTP transport semantics를 application port의 raw header로 넘기지 않는다;
- public ETag을 provider ETag 그대로 쓰지 않고 application version token으로 생성한다;
- exact `Content-Length``Content-Range`를 descriptor/receipt로 검증한다.
## 17. Checksum, media type, metadata
### 17.1 Digest model
```text
ObjectDigest {
algorithm: SHA_256
encoding: LOWERCASE_HEX
value: exactly 64 lowercase hexadecimal characters
scope: FULL_CONTENT
}
```
R2 baseline business digest는 SHA-256이다. Provider transport checksum은 별도 value object다.
```text
ProviderChecksum {
algorithm: CRC32 | CRC32C | CRC64NVME | SHA1 | SHA256 | qualified extension
checksumType: FULL_OBJECT | COMPOSITE
scope: OBJECT | PART(partNumber)
encoding: BASE64
value
}
```
Algorithm만 같은 composite multipart checksum을 full-content SHA-256과 비교하지 않는다.
- content digest: application-level immutable content identity/integrity;
- transport checksum: provider request/response corruption detection;
- ETag: provider-specific entity/version evidence;
- operation fingerprint: user intent identity.
서로 대체하지 않는다.
### 17.2 ETag 규칙
- single-part/plain object에서도 ETag을 portable MD5 계약으로 노출하지 않는다;
- multipart ETag은 whole-object MD5가 아니다;
- encryption/provider 구현에 따라 의미가 달라질 수 있다;
- ETag은 private conditional evidence로 보존할 수 있다;
- public API version은 opaque `ObjectVersionToken`이다.
### 17.3 Direct upload checksum
Grant에는:
- required checksum algorithm;
- checksum header/field;
- declared exact/maximum size;
- content type;
- key/version precondition
을 묶는다. Upload 뒤에는:
1. client completion claim을 신뢰하지 않는다;
2. exact HEAD로 size/checksum/version을 확인한다;
3. provider가 full SHA-256을 증명하지 못하면 bounded verification read 또는 scan pipeline을
수행한다;
4. 검증 전에는 publish하지 않는다.
### 17.4 Media type
- inbound `Content-Type`은 declared value일 뿐이다;
- application이 destination별 allowlist를 적용한다;
- scan/content-sniff 단계에서 detected media type을 별도로 기록할 수 있다;
- declared/detected mismatch 정책은 business/application 소유다;
- browser-executable type은 download response의 disposition/CSP/nosniff 정책과 함께 다룬다;
- metadata value에 CR/LF/control character를 허용하지 않는다.
### 17.5 Metadata budget
Typed metadata마다:
- field count;
- key/value byte length;
- character set;
- canonical encoding;
- redaction;
- persistence location;
- public exposure 여부
를 고정한다. Provider user-metadata 최대치에 기대어 application input을 무제한 허용하지 않는다.
## 18. Direct upload grant
### 18.1 Threat model
Presigned request는 URL을 가진 주체가 제한된 provider operation을 실행할 수 있는 bearer
credential이다.
따라서:
- authenticated/authorized business intent 뒤에만 생성한다;
- TLS 외 endpoint를 production에서 허용하지 않는다;
- query string을 access log/APM/error message에서 redact한다;
- browser history/referrer 노출을 줄이는 client contract를 제공한다;
- TTL은 destination마다 짧은 upper bound를 둔다;
- underlying temporary credential 만료보다 길게 발급하지 않는다;
- provider/client clock skew budget을 빼고 grant expiry가 session expiry보다 먼저 오도록 compile한다;
- production signer/provider의 NTP/clock health가 허용 skew를 넘으면 신규 grant admission을
fail closed한다;
- signing identity 권한보다 강한 grant를 만들 수 없음을 qualification한다;
- CORS는 exact origin/method/header allowlist로 pre-provision한다.
### 18.2 Presigned PUT
Grant에 고정할 항목:
- method;
- exact immutable internal key;
- expiry;
- content type;
- checksum header;
- encryption headers;
- expected owner/provider profile;
- atomic create-only precondition;
- exact provider-enforced content length 또는 더 엄격한 hard byte ceiling;
- named profile이 요구하는 Object Lock mode/retain-until/legal-hold header;
- allowed signed headers.
R2 `direct-put` card에서 atomic create-only는 필수다. 같은 URL이 expiry 전 재사용되거나 concurrent
사용되어도 immutable key를 덮어쓸 수 없어야 한다. AWS profile은 signed
`If-None-Match: *`와 bucket-policy enforcement를 qualification한다. Provider가 이를 지원하지
않으면 `direct-put=UNSUPPORTED`이며 trusted ingress 또는 server-mediated upload로 보낸다.
Presigned PUT만으로 exact body size를 모든 client/provider 조합에서 강제했다고 주장하지 않는다.
R2 direct admission은 다음 중 하나가 실제 fault/security test로 hard ceiling을 증명해야 한다.
- signed exact `Content-Length`가 provider/client/proxy 조합에서 강제됨;
- browser POST policy의 `content-length-range`;
- controlled ingress proxy가 body를 provider write 전에 제한함.
그 증거가 없으면 server-mediated upload를 사용한다. Upload 후 HEAD/quarantine/delete는
publication integrity와 cleanup 수단일 뿐 storage/transfer cost DoS에 대한 admission bound가
아니다. 사후 검증만 가능한 profile은 별도 R1 soft-limit profile로 낮추고 maximum provider
exposure를 명시한다.
AWS direct upload와 per-object Object Lock을 조합하는 profile은:
- retention/legal-hold header를 exact signed condition으로 묶고;
- Object Lock upload에 요구되는 `Content-MD5` 또는 qualified SDK checksum algorithm을 강제하고;
- exact version의 retention/legal-hold를 scoped `GetObjectRetention`/`GetObjectLegalHold` evidence로
검증한다.
이 조합을 구현하지 않으면 bucket-default retention만 사용하거나 named direct+retention profile을
`UNSUPPORTED`로 둔다. Generic 403을 `HELD`로 매핑하지 않는다.
### 18.3 POST policy
Browser POST는 optional capability다.
Pinned AWS SDK 2.30.0 `S3Presigner`는 이 문서가 요구하는 browser POST-policy signer를 제공한다고
가정하지 않는다. 별도 audited SigV4 POST policy signer와 golden/security test가 없으면 해당
provider profile은 `directPost=UNSUPPORTED`다.
- exact bucket/key;
- content-length-range;
- content type prefix가 아닌 exact/좁은 allowlist;
- checksum;
- encryption;
- success status;
- expiration
을 policy condition으로 고정한다. Policy/fields도 bearer secret으로 redact한다.
### 18.4 Completion
Client가 direct upload 완료 API를 호출하면 application은:
1. session ID와 operation ID/fingerprint를 확인한다;
2. expected object exact HEAD를 수행한다;
3. size/checksum/encryption/version을 검증한다;
4. required scan을 수행한다;
5. 그 뒤에만 publish 또는 DB pending attachment로 진행한다.
Completion endpoint를 여러 번 호출해도 same terminal receipt를 반환한다.
### 18.5 Public signing endpoint
S3-compatible deployment는 application이 접근하는 internal endpoint와 browser가 접근할 public
signing endpoint가 다를 수 있다.
Binding은 둘을 분리한다.
- control endpoint: adapter SDK network target;
- presign endpoint: client가 실제 도달할 authority/scheme;
- approved host/scheme allowlist;
- path-style/virtual-host signing mode;
- proxy forwarded-host를 무조건 신뢰하지 않음.
Request header나 arbitrary URL로 presign authority를 선택하지 않는다.
### 18.6 Grant issue, response loss, reissue
Grant response도 bearer credential delivery이므로 stable session/request와 durable generation을
가진다.
```text
SESSION_RESERVED
-> GRANT_PREPARED(generation, constraintsDigest, signingTime, expiry, credentialRevision)
-> GRANT_ISSUED
-> UPLOAD_VERIFICATION_IN_PROGRESS
-> DATA_UPLOADED | EXPIRED | ABORTED
```
Rules:
- grant 발급 자체가 stable grant-operation ID와 fingerprint를 가진다;
- URL을 만들기 전에 constraints와 generation을 CAS로 `GRANT_PREPARED`한다;
- signing 뒤 반환 전에 `GRANT_ISSUED` evidence를 기록하되 URL 자체는 저장/log하지 않는다;
- response loss retry는 same session/fingerprint/generation을 lookup한다;
- frozen signing input과 credential revision으로 byte-identical grant를 안전하게 재생성할 수 있을
때만 같은 grant를 반환한다;
- 재생성이 불가능하면 CAS로 새 generation을 발급하며 old URL이 revoke됐다고 가정하지 않는다;
- frozen revision이 없거나 active-generation/exposure limit 때문에 안전한 replay/reissue가
불가능하면 `GRANT_REPLAY_UNAVAILABLE`로 fail closed한다;
- old/new generation은 같은 immutable key, exact content identity, hard size, create-only
constraint만 가진다;
- active generation 수와 worst-case expiry/in-flight exposure를 제한한다;
- grant response의 operation ID/fingerprint가 다르면 conflict;
- completion은 어느 generation을 사용했는지 client claim만 믿지 않고 exact object를 검증한다.
Single direct upload abort/reaper는 모든 issued generation의 expiry + clock skew + qualified
provider in-flight horizon 전에는 terminal delete/absence를 주장하지 않는다. Finite horizon이
없으면 controlled ingress를 사용하거나 indeterminate로 유지한다.
Direct download grant는 expected reference lifecycle revision에 대해 `GRANT_ISSUED` record를
conditional CAS한 시점을 linearization point로 삼는다.
- retirement가 먼저 linearize되면 CAS가 실패하고 생성한 URL은 폐기하며 응답하지 않는다;
- grant issue가 먼저 linearize되면 이후 retirement가 일어나도 이미 issued URL의 잔여 유효성을
인정한다;
- URL response가 network에서 늦게 도착해도 ordering은 control CAS로 판정한다.
Direct download reissue도 old URL의 expiry 전 유효성을 인정하고 active grant/audit count에
포함한다. Secret delivery ACK loss를 “발급되지 않음”으로 취급하지 않는다.
## 19. Multipart protocol
### 19.1 사용 기준
Multipart는 다음 조건에서만 사용한다.
- object size/profile이 configured threshold 이상;
- provider card가 exact capability를 지원;
- part count/size/concurrency budget이 계산 가능;
- abort/reconciliation/lifecycle backstop이 준비됨.
Small object를 무조건 multipart로 보내지 않는다.
### 19.2 Session
`startMultipart`:
1. operation/fingerprint reserve;
2. immutable internal key 생성;
3. provider multipart create;
4. provider upload ID를 private encrypted/control record에 저장;
5. opaque session ID와 bounded parameters 반환.
반환 필드:
- opaque session ID;
- minimum/maximum part size;
- maximum part count;
- maximum concurrent grants/uploads;
- session expiry;
- required checksum;
- final expected size/digest requirement.
Provider upload ID를 public API에 노출하지 않는다.
### 19.3 Part upload
- part number는 1부터 시작하는 bounded integer;
- final part를 제외한 minimum size 규칙을 provider card가 제공;
- client는 arbitrary key/upload ID를 지정하지 않는다;
- part grant는 exact session/key/part/checksum/exact content length/expiry에 묶인다;
- grant expiry는 session expiry보다 짧거나 같다;
- out-of-order part upload는 허용하되 complete는 consecutive `1..N`을 오름차순으로 고정한다;
- final part만 configured minimum보다 작을 수 있다;
- part별 current grant generation은 하나이며 acknowledge 뒤 신규 grant를 발급하지 않는다;
- same part 재발급은 exact length와 full part digest가 같을 때만 새 generation으로 허용한다;
- prior grant는 revoke됐다고 보지 않으며 expiry + skew + qualified in-flight margin 전에는 새
generation acknowledge/complete를 허용하지 않거나 새 multipart session으로 교체한다;
- provider가 signed part checksum을 강제하지 못하면 direct multipart R2를 비활성화하거나
controlled ingress를 사용한다;
- direct mode에서 grant issuance count, declared bytes, active expiry window, worst-case replay
exposure를 tenant/destination/global budget으로 제한한다;
- 하나의 presigned part URL이 expiry 전 반복/concurrent 사용될 수 있으므로 provider-side 실제
request concurrency/transfer bytes가 bounded됐다고 주장하지 않는다;
- strict transport admission은 revocable controlled ingress 또는 provider-enforced primitive가
필요하다.
Browser direct multipart acknowledgement:
1. CORS가 exact origin/method/request headers와 필요한 `ETag`/checksum response
`Expose-Headers`만 허용한다.
2. Client는 grant와 함께 받은 acknowledgement nonce, part number, bounded provider ETag/checksum
claim을 server에 보낸다.
3. `acknowledgePart`는 session/grant nonce/expiry/fence/exact part length/checksum을 검증한다.
4. Provider `ListParts` 또는 exact provider evidence로 part 존재와 ETag/checksum을 검증한다.
5. 검증된 provider evidence를 private ledger에 CAS로 기록한다.
6. Server는 provider value를 숨긴 `PartReceiptToken`을 반환한다.
Forged, stale, wrong-session, superseded-attempt claim은 conflict/security audit이며 ledger에 넣지
않는다. Complete request는 public provider ETag 목록이 아니라 server가 발급한 opaque part
token만 받는다.
`COMPLETE_IN_PROGRESS` CAS 전에:
1. 신규 grant/acknowledge admission을 닫는다.
2. 모든 relevant grant generation의 expiry/in-flight horizon을 만족하거나 controlled ingress
drain evidence를 얻는다.
3. paginated `ListParts`를 다시 읽는다.
4. current ledger revision, ETag, algorithm/type/scope checksum, exact length와 비교한다.
5. 불일치하면 complete하지 않고 re-acknowledge/reconcile한다.
Complete CAS 뒤 신규 grant/acknowledge는 거부한다. Final object full SHA-256이 expected content
identity와 다르면 publish하지 않고 quarantine한다.
### 19.4 Complete
Complete 전:
- required part numbers의 연속성;
- duplicate/missing part;
- each opaque token/session binding;
- expected total size;
- aggregate checksum policy;
- session expiry/state
를 검증한다.
Complete response loss는 “실패했으므로 다시 complete”가 아니다.
Canonical transition:
```text
ACCEPTING_PARTS
-> COMPLETE_IN_PROGRESS
pendingEffect.kind = MULTIPART_COMPLETE
certainty = NOT_SENT | SENT | INDETERMINATE
-> COMPLETED
```
Indeterminate complete는 `COMPLETE_IN_PROGRESS`와 pending-effect certainty를 유지한 채 exact key
HEAD, multipart state, size/checksum/version을 비교해
`APPLIED`/`NOT_APPLIED`/계속 `INDETERMINATE`로 resolve한다.
AWS conditional complete가 concurrent delete/write와 경합해 `409 Conflict`를 반환하면 기존
upload ID에 complete만 재시도하지 않는다. Official provider semantics가 요구하는 경우 새
`CreateMultipartUpload`부터 전체 session을 재시작하며, original operation record에는 old session
abort/cleanup과 replacement session link를 보존한다. Conditional complete에서 `404`, `409`,
`412`를 각각 provider/card evidence에 따라 분리한다.
### 19.5 Abort와 orphan
- abort admission 전에 새 part grant 발급을 막고 application-known attempt를 fence한다;
- 이미 발급한 presigned part URL과 provider가 수락한 in-flight request는 application fence로
revoke됐다고 주장하지 않는다;
- application abort는 logical하게 idempotent;
- already completed session을 abort success로 오인하지 않는다;
- abort response loss를 reconcile한다;
- abort record는 `pendingEffect.kind=MULTIPART_ABORT`와 certainty를 유지한다;
- provider가 in-flight part의 late success를 허용하면 `Abort -> paginated ListParts -> 필요 시
Abort 반복`으로 part가 없음을 확인한다;
- earliest terminal check는 `latest issued grant expiry + qualified clock skew + qualified maximum
provider in-flight/request duration` 뒤다;
- presigned expiry는 request-start admission이며 transfer cutoff가 아니므로 expiry만으로
in-flight 종료를 증명하지 않는다;
- provider가 maximum in-flight horizon을 증명하지 못하면 controlled/revocable ingress를
사용하거나 abort certainty를 `INDETERMINATE`로 유지하고 repeated reaper + lifecycle
backstop만 claim한다;
- `NoSuchUpload`는 exact final object/session/control state와 함께 해석해 completed session을
aborted로 오인하지 않는다;
- maximum abort attempt/deadline 뒤 empty evidence가 없으면 `MULTIPART_ABORT_INDETERMINATE`로
유지한다;
- session TTL 뒤 reaper가 exact control state를 claim한다;
- provider lifecycle의 incomplete-multipart abort rule을 backstop으로 설정한다;
- lifecycle rule만을 유일한 cleanup으로 사용하지 않는다;
- reaper와 lifecycle 사이 race를 terminal state/evidence로 처리한다.
Finite deterministic abort를 R2 profile이 요구하면 위 maximum horizon 또는 revocable controlled
ingress evidence가 필수다. Incomplete-multipart lifecycle age도 이 horizon과 reconciliation
margin보다 길어야 한다.
### 19.6 Provider limits
AWS가 제공하는 maximum object/part limits를 portable application default로 그대로 사용하지 않는다.
Destination은 훨씬 낮은 안전한 limit를 고정하고, provider card가 이를 만족하는지만 판정한다.
## 20. Conditional mutation과 outcome/error
### 20.1 Mutation outcome
모든 mutation은 다음 outcome을 사용한다.
- `APPLIED`;
- `ALREADY_APPLIED`;
- `NOT_APPLIED`;
- `PRECONDITION_FAILED`;
- `INDETERMINATE`;
- `HELD`;
- `UNSUPPORTED`.
`void`, boolean 한 개, generic success/failure로 축약하지 않는다.
### 20.2 Preconditions
지원하는 logical precondition:
- create if absent;
- operation record state/version equals;
- reference absent;
- reference points to expected object/version;
- object exact version equals;
- retire only published;
- purge only retired and retention elapsed;
- multipart session state equals;
- request fingerprint equals.
Provider primitive가 이 precondition을 안전하게 구현하지 못하면 해당 card를 R2로 활성화하지
않는다. check-then-act를 atomic conditional mutation처럼 보고하지 않는다.
### 20.3 Error taxonomy
Application-visible typed category:
- `INVALID_REQUEST`;
- `OBJECT_NOT_FOUND`;
- `OBJECT_NOT_PUBLISHED`;
- `OBJECT_QUARANTINED`;
- `OPERATION_CONFLICT`;
- `OPERATION_EXPIRED`;
- `PRECONDITION_FAILED`;
- `RANGE_NOT_SATISFIABLE`;
- `OBJECT_TOO_LARGE`;
- `METADATA_TOO_LARGE`;
- `MEDIA_TYPE_NOT_ALLOWED`;
- `CHECKSUM_MISMATCH`;
- `RETENTION_HELD`;
- `UNSUPPORTED_CAPABILITY`;
- `POLICY_REVISION_UNAVAILABLE`;
- `BINDING_SECURITY_MISMATCH`;
- `DEPENDENCY_ACCESS_DENIED`;
- `CAPACITY_EXHAUSTED`;
- `THROTTLED`;
- `DEPENDENCY_UNAVAILABLE`;
- `TIMEOUT`;
- `CANCELLED`;
- `CONTENT_PRODUCTION_FAILED`;
- `CONTENT_CONSUMPTION_FAILED`;
- `DIRECT_SESSION_EXPIRED`;
- `GRANT_REPLAY_UNAVAILABLE`;
- `SCAN_INDETERMINATE`;
- `PROVIDER_PROTOCOL_VIOLATION`;
- `PUBLISH_INDETERMINATE`;
- `CORRUPT_CONTROL_RECORD`;
- `UNSUPPORTED_CONTROL_SCHEMA`;
- `INTERNAL_ERROR`.
각 category는:
- safe public code/message;
- retryability;
- reconciliation requirement;
- health impact;
- metric outcome;
- audit severity
를 table-driven mapping으로 가진다.
### 20.4 Provider error mapping
AWS/MinIO/filesystem raw exception은 adapter 안에서:
- operation;
- provider error/status/code;
- bytes sent/received 여부;
- response presence;
- request id의 safe hash;
- exact precondition;
- retry attempt;
- mutation certainty
를 고려해 normalize한다.
HTTP 404만 보고 모두 not-found로 매핑하지 않는다. Wrong owner/bucket/permission/endpoint가
404처럼 보일 수 있는 경우 startup binding과 operation context를 함께 사용한다.
### 20.5 Retry
- validation, checksum mismatch, conflict, held는 retry하지 않는다;
- throttling/unavailable은 bounded retry 대상일 수 있다;
- body producer가 non-repeatable이면 upload transport retry를 제한한다;
- immutable create의 response loss는 reconcile 먼저;
- multipart part는 exact part identity/checksum으로 retry 가능 여부를 판정한다;
- complete/delete/reference CAS는 unknown outcome 규칙을 따른다;
- SDK internal retry도 total amplification budget에 포함한다.
Provider-wide `max-attempts`만으로 mutation 안전을 결정하지 않는다. Put, complete, reference CAS,
delete, abort별 retry policy와 physical attempt telemetry를 고정한다. Mutation에서:
- SDK가 2회 이상 physical attempt를 수행했거나;
- response body/ack가 유실되었거나;
- AWS `CompleteMultipartUpload`처럼 initial HTTP 200 뒤 embedded error가 가능한 operation이면
final SDK exception/success 하나만 보고 certainty를 확정하지 않고 operation-specific evidence와
reconciliation 규칙을 적용한다.
## 21. Provider model
### 21.1 Provider identity
다음 exact provider type을 사용한다.
- `filesystem-local-dev`;
- `filesystem-local-persistent` optional;
- `aws-s3-general-purpose`;
- `s3-compatible-minio-<distribution>-<qualified-release-token>`.
`s3`, `s3-compatible`, `filesystem` 같은 넓은 이름 하나로 production 보장을 선언하지 않는다.
### 21.2 Capability descriptor
Provider startup qualification 결과:
```text
ObjectStorageCapabilityDescriptor
providerType
providerVersion
bindingRevision
qualificationTimestamp
evidenceRevision
evidenceExpiresAt
operations {
managedUpload: CapabilityEvidence
managedDownload: CapabilityEvidence
rangeRead: CapabilityEvidence
controlPlaneCas: CapabilityEvidence
directPut: CapabilityEvidence
directPost: CapabilityEvidence
directGet: CapabilityEvidence
managedMultipart: CapabilityEvidence
directMultipart: CapabilityEvidence
retirement: CapabilityEvidence
exactPurge: CapabilityEvidence
}
guaranteeAxes {
visibility
crashDurability
consistency
mutationOutcomeCertainty
versionIdentity
authoritativeAbsence
}
namedOperationProfiles {
transferMode
checksumAlgorithm/type/scope/encoding
encryptionProfile
retentionProfile
immutableCreate
exactSizeEnforcement
responseLossReconciliation
}
limits
qualificationEvidenceRevision
```
`CapabilityEvidence`:
```text
status = SUPPORTED | UNSUPPORTED | UNVERIFIABLE
source = STATIC_ATTESTATION | STARTUP_PROBE | CI_QUALIFICATION
evidenceDigest
observedAt
validUntil
providerAndDeploymentIdentity
limitations
```
Flat boolean의 AND로 조합 capability를 승인하지 않는다. 예를 들어 각각의 checksum, direct
multipart, SSE-KMS, retention이 지원되어도 그 조합이 지원된다는 뜻이 아니다. Destination은
exact named operation profile을 요구하고 binding compiler는 그 profile 전체에 대한
`SUPPORTED` + unexpired evidence만 수용한다.
Direct upload profile이 per-object retention을 요구하면 Object Lock/retention header와
permission도 signed grant condition에 포함되어야 한다. 그렇지 않은 direct+retention 조합은
별개 기능이 각각 supported여도 `UNSUPPORTED`다.
Descriptor 값은 code default가 아니라:
- provider/version allowlist;
- static deployment attestation;
- safe startup probe;
- integration/qualification evidence
를 합성한 결과다.
`UNVERIFIABLE`, expired evidence, guarantee axis가 requirement보다 낮은 상태는
`SUPPORTED`로 취급하지 않는다. Atomic visibility와 crash durability, consistency와 outcome
certainty를 한 “durable/strong” boolean으로 합치지 않는다.
### 21.3 Common semantic subset
Common R2 baseline 후보:
- private immutable create;
- exact GET/HEAD;
- bounded single range;
- SHA-256 logical digest;
- stable opaque reference;
- control record conditional mutation;
- response-loss reconciliation;
- version-aware retire/purge;
- encryption-at-rest evidence;
- finite timeout/resource budget.
Provider가 하나라도 증명하지 못하면 destination requirement를 낮춰야 하는 것이 아니라 해당
provider/destination binding이 startup에서 실패한다.
### 21.4 Provider-specific capability
다음은 common subset이 아니다.
- AWS Object Lock;
- AWS DSSE-KMS;
- provider native checksum 조합;
- MinIO의 specific retention/lifecycle behavior;
- filesystem atomic/durability primitive;
- provider copy;
- provider notification;
- provider replication/region behavior.
Application이 optional capability를 요구할 때 exact provider card를 통해서만 접근한다.
## 22. Filesystem providers
### 22.1 `filesystem-local-dev`
목적:
- local sample와 unit/contract test;
- network 없는 개발;
- object/reference/control codec 빠른 검증.
제한:
- R0/R1까지만;
- single process/node;
- production profile 금지;
- presign/multipart/KMS/versioning/Object Lock 미지원;
- local disk 소실을 durable storage로 간주하지 않음;
- container image layer 또는 read-only root default 사용 금지.
활성화하려면 explicit dev/test profile과 absolute configured root가 필요하다.
### 22.2 `filesystem-local-persistent`
운영에서 local persistent disk가 정말 요구될 때 별도 card로 qualification한다.
필수 조건:
- dedicated mounted volume identity 검증;
- mount missing 시 local directory fallback 금지;
- restrictive root owner/permission;
- no-follow directory traversal;
- exclusive create;
- temp/control record atomic replace;
- file `force`와 directory durability strategy;
- disk/inode/free-space alert;
- quota와 reaper;
- single-node 또는 external fencing 범위 명시;
- backup/restore와 fsck/corruption runbook.
NFS/shared mount는 이 card에 포함하지 않는다. 필요하면 Fileserver 설계와 별도 provider 설계를
한다.
### 22.3 Path safety
Filesystem key resolution은 string normalize만으로 끝내지 않는다.
- application raw key 입력 자체를 제거;
- canonical internal segments만 사용;
- root를 startup에서 real path로 pin;
- intermediate symlink/reparse point 거부;
- supported platform에서 `SecureDirectoryStream` 또는 directory-handle-relative operation 사용;
- temp/data/control root 분리;
- root alias와 empty final segment 거부;
- create/delete 시 exact file type과 link count 정책;
- recursive delete 금지;
- cleanup traversal 중 mount/device boundary 정책;
- TOCTOU race fault test.
지원 플랫폼에서 필요한 safe primitive를 제공하지 않으면 provider card를 낮춘다.
### 22.4 Write/durability
Managed write:
1. target directory 아래 private temp/exclusive file 생성;
2. restrictive permission 적용;
3. bounded stream과 hash;
4. file flush/force;
5. immutable final name으로 no-replace publish;
6. 필요한 directory force;
7. manifest/control CAS;
8. temp cleanup.
Atomic visibility와 crash durability는 별도 descriptor field다. `ATOMIC_MOVE` 하나로 둘을 모두
증명하지 않는다.
### 22.5 Filesystem metadata
Content type, digest, exact size, generation, state를 filename/xattr에만 의존하지 않는다. Versioned
private manifest를 사용한다. Xattr은 optional evidence일 뿐 portable truth가 아니다.
## 23. AWS S3 general-purpose provider
### 23.1 범위
초기 AWS card는 regional general-purpose bucket에 한정한다.
- directory bucket/S3 Express 제외;
- access point/MRAP 제외;
- Requester Pays와 MFA Delete는 initial card에서 제외하고 필요 시 별도 permission/cost/operator
workflow card로 qualification;
- private bucket;
- versioning required 여부는 destination profile에 명시;
- active published object는 online-readable storage-class profile에 고정;
- restore workflow가 없는 initial card는 current data를 archive retrieval이 필요한 tier로 전환하는
lifecycle을 거부;
- lifecycle, ownership, BPA, encryption은 pre-provisioned;
- runtime은 object/control prefix operation만 수행.
### 23.2 Consistency
AWS S3가 현재 제공하는 strong read-after-write/list consistency는 이 exact provider card에만
적용한다.
- PUT/DELETE/HEAD/GET 후 object data lookup;
- control record conditional protocol;
- LIST maintenance discovery.
Bucket configuration, IAM propagation, DNS/network, replication 의미를 같은 consistency로
확장하지 않는다. MinIO 또는 다른 compatible store로 일반화하지 않는다.
### 23.3 Client 선택
초기 managed streaming transport는 Java-based `S3AsyncClient`를 우선 검토한다. 그러나 R2
multipart orchestration을 SDK의 opaque automatic multipart에 맡기지 않는다.
선정 이유:
- unknown content length streaming 지원;
- standard SDK HTTP/timeouts/retry/metrics와 통합;
- provider client lifecycle을 한 곳에서 소유.
R2 managed multipart는 adapter가 low-level:
- `CreateMultipartUpload`;
- `UploadPart`;
- `ListParts`;
- `CompleteMultipartUpload`;
- `AbortMultipartUpload`
를 직접 호출하고 provider upload ID, part evidence, pending effect, complete/abort certainty를 durable
control record에 보존한다. SDK-managed automatic multipart가 이 evidence/hook를 public하게
노출하지 않으면 R2 card에 사용할 수 없다.
Pinned 2.30.0의 automatic multipart는 upload ID/part ledger를 application adapter에 노출하지
않고 실패 cleanup을 deterministic reconciliation protocol로 제공하지 않는다. 따라서 별도
qualification 전에는 R1 convenience/transport evidence로만 취급한다. Maximum size와 integer
overflow를 포함한 boundary test 없이 large-object fallback으로 사용하지 않는다.
CRT-based client는 high-throughput optional card다. 다음을 별도로 qualification한 뒤에만 사용한다.
- SDK/HTTP configuration 차이;
- retry/timeout 의미;
- metric visibility;
- native library packaging;
- memory/direct-buffer footprint;
- shutdown and cancellation;
- checksum/multipart behavior.
“더 빠르다”는 이유만으로 baseline을 교체하지 않는다.
### 23.4 SDK version
현재 build는 AWS SDK BOM `2.30.0`을 pin한다. 구현 계획은:
- 승인된 exact version의 API/bug/security evidence를 다시 확인;
- 2.30.0부터 적용되는 default upload checksum calculation behavior를 characterization;
- `requestChecksumCalculation`, `responseChecksumValidation`, explicit algorithm을 provider
profile에 고정;
- AWS/MinIO/presign별 default CRC32와 explicit SHA-256 compatibility를 test;
- pinned 2.30.0 `CompleteMultipartUploadRequest.Builder.mpuObjectSize(Integer)` 경계를 반영해
FULL_OBJECT multipart checksum profile이 `Integer.MAX_VALUE`를 넘으면 startup에서 거부하거나,
audited raw-header path/승인된 SDK upgrade 뒤에만 허용;
- 2 GiB 경계와 overflow test;
- dependency lock 갱신;
- provider qualification version 기록;
- version upgrade compatibility/fault test;
- deprecated/changed conditional header support 확인
을 포함한다. 이 문서가 미래 SDK의 API 존재를 보장하지 않는다.
### 23.5 Conditional request
Immutable data/control create에 `If-None-Match: *` 또는 해당 SDK의 exact conditional primitive를
사용한다. CAS/delete에는 expected ETag/version/precondition을 사용한다.
- request builder가 header를 제공하는지 exact pinned SDK에서 검증;
- proxy/gateway가 header를 보존하는지 integration test;
- conditional complete의 `404`/`409`/`412`와 transient/permission 오류를 분리;
- AWS가 `409` 뒤 multipart 전체 재시작을 요구하는 operation은 기존 upload ID complete retry
금지;
- unsupported provider는 check-then-put으로 downgrade하지 않음;
- bucket policy로 conditional write를 강제할 수 있으면 deployment control에 포함.
AWS bucket policy가 conditional create를 강제할 때 `PutObject`/`CompleteMultipartUpload`에는
조건을 요구하되, conditional header를 받지 않는 `CreateMultipartUpload`/`UploadPart` 같은
`s3:ObjectCreationOperation` 단계는 official policy shape에 맞게 exempt한다. 그렇지 않으면
multipart를 403으로 막을 수 있다. ETag `If-Match` write/delete에 필요한 scoped
`s3:GetObject` permission도 qualification한다.
### 23.6 Expected bucket owner
모든 supported request에 expected bucket owner를 설정한다. Startup binding의 account/bucket
attestation과 함께 confused-deputy/misrouting을 줄인다.
Expected-owner mismatch는 not-found가 아니라 hard configuration/security failure다.
### 23.7 HEAD, checksum, authoritative absence
- checksum을 HEAD/GET response로 요구할 때 `ChecksumMode.ENABLED`를 명시한다;
- checksum evidence는 algorithm/type/scope를 함께 읽고 business full digest와 무조건 비교하지
않는다;
- SSE-KMS checksum 조회에 필요한 `kms:Decrypt`와 provider 문서가 요구하는 KMS permission을
qualification한다;
- HEAD 요청에 PUT용 encryption header를 보내지 않는다;
- 403/404만으로 absent, forbidden, wrong owner를 단정하지 않는다.
AWS `HeadObject`는 missing key에서 caller의 `s3:ListBucket` 권한에 따라 404 또는 403을 반환할 수
있다. Descriptor에 `authoritativeAbsence` evidence를 둔다.
- destination prefix로 제한한 `s3:ListBucket`와 version workflow에 필요한
`s3:ListBucketVersions` 권한으로 negative lookup을 증명하거나;
- absence를 끝까지 `INDETERMINATE`로 유지한다.
Startup probe는 existing sentinel뿐 아니라 missing-key negative lookup과 forbidden-key
differentiation을 검증한다. MinIO도 exact permission/error behavior를 별도 card로 test한다.
### 23.8 Encryption
Destination이 다음 named encryption profile 중 하나를 요구한다.
- `sse-s3`;
- `sse-kms:<logical-key-profile>`;
- `dsse-kms:<logical-key-profile>` optional.
Application request에 raw KMS key ARN을 넣지 않는다. Binding compiler가 logical profile을
pre-approved key와 encryption context로 resolve한다.
검증:
- single PUT에는 selected SSE request header;
- multipart에는 `CreateMultipartUpload`에 selected SSE-KMS/DSSE 설정;
- `UploadPart`와 `CompleteMultipartUpload`는 create 설정을 상속하며 SSE-C처럼 동일 KMS request
header를 반복 전송하지 않음;
- part/complete/final response와 HEAD encryption attestation;
- response/head encryption mode;
- KMS key identity/version policy;
- `GenerateDataKey`, `Decrypt` 등 exact operation에 필요한 KMS permission;
- copy 시 source/destination encryption;
- KMS throttling/error mapping;
- presign에 필요한 signed header.
SSE-C는 baseline에서 제외한다. Key material을 application memory/header/log에 전달하지 않는다.
### 23.9 Ownership와 public access
Production requirement:
- Block Public Access;
- bucket-owner-enforced object ownership;
- ACL disabled;
- public bucket policy 없음;
- access logging/CloudTrail data event 정책은 risk profile에 따라 활성화;
- runtime principal은 exact bucket/prefix/action 최소 권한;
- maintenance principal은 runtime principal과 분리 가능;
- public delivery는 application authorization + stream/presign만 사용.
### 23.10 Versioning
Versioning이 required인 destination:
- startup attestation/probe로 enabled 확인;
- exact version ID를 private evidence에 보존;
- read/retire/purge가 version-aware;
- versioned read/purge role에 scoped `s3:GetObjectVersion`/`s3:DeleteObjectVersion` permission;
- delete marker와 object version을 구분;
- lifecycle noncurrent-version retention을 검증;
- suspended 상태를 enabled로 간주하지 않음.
AWS bucket에 versioning을 처음 enable한 직후의 propagation window는 `enabled` 조회 한 번으로
readiness를 승인하지 않는다. IaC attestation에 activation timestamp를 넣고 provider가 권고한
soak 기간 뒤 sentinel create/read/delete/version test를 통과해야 신규 admission을 연다.
### 23.11 Retention/Object Lock
Object Lock card는:
- exact provider/version의 enablement constraint와 irreversible setting;
- governance/compliance mode;
- default retention;
- legal hold permission;
- versioning;
- bypass-governance 권한 부재 또는 엄격한 별도 break-glass;
- clock/reference time;
- audit
을 qualification한다.
AWS general-purpose bucket은 current provider semantics에 따라 existing bucket enablement를 별도
qualification한다. Older MinIO/other distribution의 creation-time-only 제약을 AWS에
일반화하거나, 반대로 current AWS 동작을 old MinIO에 일반화하지 않는다.
Application delete가 hold를 만나면 `HELD`이며 success purge로 보고하지 않는다.
### 23.12 Lifecycle
Pre-provisioned lifecycle은 다음을 backstop한다.
- incomplete multipart expiration;
- noncurrent version retention;
- expired delete marker;
- separately copied immutable terminal-audit archive가 replay/retention horizon을 지난 뒤 만료되는
narrow prefix.
Lifecycle만으로 application attachment 상태를 판정하지 않는다. Rule ID와 expected digest를
deployment attestation에 고정한다.
Incomplete-multipart lifecycle age는 maximum active session + grant expiry + clock-skew +
reconciliation margin보다 길어야 한다. 정상 장기 upload를 lifecycle이 먼저 abort하지 않는지
fault test한다.
Baseline의 published/staged/quarantined data가 같은 immutable `data/v1` prefix를 사용하므로,
provider lifecycle은 control state를 보고 staged object만 안전하게 골라낼 수 없다. 따라서
staged/quarantined data cleanup은 fenced reconciler만 수행한다. Lifecycle을 여기에 적용하려면
state-safe immutable prefix 또는 immutable lifecycle marker가 publication과 race 없이 유지된다는
별도 protocol/fault evidence가 먼저 필요하다. Bucket policy도 lifecycle engine의 잘못된
deletion을 application state로 막는 대체 수단이 아니다.
Live/current/non-terminal `control/v1` record에는 provider age-based lifecycle을 적용하지 않는다.
Lifecycle은 pending effect, replay-until, handoff fence, schema, live reference를 이해하지 못한다.
Control cleanup은 fenced reconciler가 exact state와 horizon을 검증해 수행한다.
## 24. MinIO provider
### 24.1 Exact qualification
MinIO는 “S3-compatible”이라는 이유로 AWS card를 상속하지 않는다.
Card는 최소 다음을 pin한다.
- exact product/distribution;
- tested server version/range;
- deployment topology;
- Java SDK version;
- versioning;
- retention/Object Lock;
- lifecycle;
- checksum behavior;
- conditional request behavior;
- presign/path-style/virtual-host behavior;
- multipart complete/abort semantics;
- error code mapping;
- consistency/failure assumptions.
### 24.2 Current Testcontainers evidence
현재 MinIO integration test는:
- OSS image `minio/minio:RELEASE.2024-01-16T16-07-38Z`;
- bucket create;
- byte[] put/get;
- exists/delete round trip
정도의 functional topology 증거다. 이는 R1이며 다음을 증명하지 않는다.
- streaming heap bound;
- conditional race;
- node/process/network failure;
- response-loss reconciliation;
- multipart orphan;
- versioning/retention;
- TLS/credentials/ownership;
- presigned public endpoint;
- rolling upgrade;
- backup/restore.
§41의 current MinIO AIStor 문서는 미래 AIStor card를 설계하기 위한 primary reference이며 이
OSS 2024 image의 동작 증거가 아니다. Current OSS card는 exact image digest, release/source
provenance, AWS-SDK-based contract/fault test로 별도 qualification한다. 현재 adapter는 MinIO
Java SDK가 아니라 AWS SDK를 사용하므로 MinIO Java SDK 문서도 current implementation evidence로
사용하지 않는다.
### 24.3 Endpoint
- production은 HTTPS;
- certificate/hostname verification을 끄지 않는다;
- internal/public presign endpoint를 분리;
- path-style 설정은 exact deployment와 DNS에 맞춤;
- arbitrary endpoint override 금지;
- loopback/RFC1918 endpoint 허용은 explicit environment policy;
- region/signature expectation을 startup에서 검증;
- redirect를 provider equivalence로 따라가지 않는다.
### 24.4 Feature downgrade 금지
MinIO가 특정 checksum/conditional/retention behavior를 지원하지 않으면:
- 해당 capability evidence를 `UNSUPPORTED` 또는 증거가 불충분하면 `UNVERIFIABLE`로 둔다;
- 필요한 destination binding은 startup 실패;
- AWS semantics를 client-side check-then-act로 흉내 내지 않는다;
- 별도 protocol이 안전하다면 provider-specific 설계와 fault evidence 후 추가한다.
## 25. Configuration schema
### 25.1 Canonical prefix
새 canonical prefix:
```text
app.object-storage
```
기존 `ca-skeleton.objectstorage.*`는 migration 기간 legacy alias로만 탐지한다.
- canonical과 legacy가 동시에 존재하면 startup 실패;
- legacy를 silent precedence로 덮지 않는다;
- migration warning에 secret/value를 출력하지 않는다;
- removal release를 문서화한다.
### 25.2 Top-level
예시:
```yaml
app:
object-storage:
enabled: false
required-destinations: []
providers: {}
destinations: {}
maintenance:
enabled: false
```
`enabled` default는 `false`다. Provider type과 destination은 default가 없다.
### 25.3 Provider binding
개념 예시:
```yaml
app:
object-storage:
enabled: true
providers:
poster-s3:
type: aws-s3-general-purpose
bucket: ${OBJECT_STORAGE_POSTER_BUCKET}
region: ${AWS_REGION}
expected-owner: ${OBJECT_STORAGE_EXPECTED_OWNER}
credentials:
mode: default-chain
endpoint:
control: null
public-presign: null
addressing: virtual-hosted
encryption-profiles:
poster-default:
type: sse-kms
key-ref: poster-object-key
timeouts:
api-call: 20s
api-attempt: 8s
connect: 2s
tls-negotiation: 3s
acquire: 1s
read: 10s
write: 10s
pool:
max-concurrency: 64
max-pending-acquires: 128
retry:
strategy: standard
max-attempts: 3
```
숫자는 예시이며 performance/fault test 없이 production default로 복사하지 않는다.
### 25.4 Destination binding
```yaml
app:
object-storage:
destinations:
poster-image:
provider-ref: poster-s3
namespace: poster-image-v1
allowed-operations:
- managed-upload
- server-download
- direct-download
maximum-object-bytes: 10485760
required-content-digest: sha-256
publication:
mode: scan-gated-reference
encryption-profile: poster-default
retention-profile: poster-standard
direct-upload-profile: disabled
```
실제 business media allowlist와 image decode 정책은 sample application typed settings/policy가
소유할 수 있다. Adapter destination의 maximum bytes와 checksum/encryption은 infrastructure
safety ceiling이다. 둘 다 존재하면 더 엄격한 값을 적용한다.
### 25.5 Settings type
- immutable nested records 또는 constructor-bound settings;
- Bean Validation;
- `Duration`, `DataSize`, typed enum;
- provider별 sealed/validated variant;
- unknown property fail 정책;
- duplicate normalized destination/provider ID 거부;
- secret은 `String` field로 직접 바인딩하지 않고 secret reference/credential provider 사용;
- `toString`, validation message, actuator configprops에서 secret redaction;
- endpoint URI의 scheme/userinfo/query/fragment 검증.
### 25.6 Invalid configuration
다음은 startup hard failure다.
- enabled인데 provider/destination 없음;
- required destination 누락;
- unknown provider type/version;
- destination의 provider ref 누락;
- plaintext production endpoint;
- endpoint에 userinfo/query/fragment;
- static credential pair 일부만 존재;
- production static literal credential;
- AWS provider의 expected owner 누락 또는 MinIO/filesystem provider의 required deployment
identity/mount attestation 누락;
- bucket/base root invalid;
- auto-create production option;
- unsupported capability requirement;
- size/part/retry/timeout가 0, 음수, overflow 또는 전체 budget과 모순;
- same namespace collision;
- public presign host가 allowlist 밖;
- scan-required인데 scan seam 없음;
- retention required인데 provider card 불충족;
- legacy/canonical key 동시 사용.
## 26. Activation, lifecycle, bootstrap
### 26.1 Exact activation
Activation 순서:
```text
settings bind/validate
-> enabled?
-> provider definitions compile
-> destination references compile
-> required capabilities compare
-> safe qualification/attestation verify
-> clients/presigners create
-> maintenance jobs register
-> readiness card publish
```
Disabled일 때:
- filesystem root 생성 없음;
- AWS credential resolution 없음;
- client/event-loop/thread 없음;
- DNS/network 없음;
- scheduler 없음;
- health indicator 없음;
- warning spam 없음.
### 26.2 No default provider
`matchIfMissing=true`를 제거한다. Local development도 명시적으로:
```yaml
app.object-storage.enabled: true
app.object-storage.providers.local.type: filesystem-local-dev
```
를 선택한다.
### 26.3 Provisioning boundary
Runtime startup은 다음을 만들거나 바꾸지 않는다.
- bucket;
- KMS key;
- IAM policy;
- lifecycle;
- versioning;
- Object Lock;
- Block Public Access;
- ownership controls;
- CORS.
IaC/deployment pipeline이 미리 provision한다. Runtime은 safe read/probe 또는 signed attestation으로
검증한다.
### 26.4 Startup qualification
모든 runtime principal에 broad configuration read 권한을 주지 않는다. 두 mode를 지원한다.
1. safe probe mode:
- expected owner가 있는 exact bucket/head;
- reserved sentinel prefix에 create/head/get/delete;
- conditional create/CAS;
- checksum/encryption response;
- optional multipart probe;
2. deployment attestation mode:
- IaC가 생성한 canonical capability document;
- bucket/account/region/policy/lifecycle/encryption/versioning digest;
- signer identity와 expiry;
- runtime은 signature/digest와 minimal data-plane probe만 검증.
Probe object는 dedicated namespace, short TTL, bounded size, audit tag를 사용한다. Production user
namespace를 오염시키지 않는다.
Attestation은 background에서 expiry 전에 refresh한다.
- bounded last-known-good grace는 signed policy에 명시된 경우에만 사용;
- grace 중 readiness는 degraded이며 신규 write/direct grant admission을 막을 수 있다;
- expiry 뒤 required destination은 신규 mutation/grant를 fail closed;
- 이미 published exact-version read는 별도 read-only continuity policy와 live safe probe가
허용할 때만 유지;
- refresh failure가 current binding으로 silent recompile을 일으키지 않음;
- expiry/refresh/last-known-good 사용을 metric/audit한다.
### 26.5 Bootstrap dependency
현재 `app-bootstrap` registry는 objectstorage leaf에 production dependency를 허용하지 않는다.
실제 production runtime에 이 capability를 포함하려면:
1. use case/runtime owner를 확정한다;
2. registry의 `allowed_dependencies`를 설계 승인 후 갱신한다;
3. Gradle dependency를 추가한다;
4. architecture verification을 통과한다;
5. required destination startup test를 추가한다.
Classpath scan에 우연히 발견되는 configuration을 composition 근거로 사용하지 않는다.
### 26.6 Lifecycle
- client/presigner/event-loop executor ownership 명시;
- Spring context stop 시 새 operation admission 중지;
- grace period 동안 in-flight managed upload/download 완료;
- direct session은 durable state라 process shutdown과 분리;
- grace 초과 operation을 cancellation/indeterminate로 기록;
- SDK client/executor close;
- maintenance lease release;
- shutdown hook 하나에만 의존하지 않음.
## 27. Security design
### 27.1 Credential
Production 우선순위:
1. workload identity/instance/container role;
2. short-lived assumed role;
3. approved external credential process;
4. static credential은 local/test 전용.
- default credential chain의 exact allowed source를 environment별로 검토;
- developer credential source가 production에서 우연히 선택되지 않게 한다;
- credential expiration/refresh failure를 metric/readiness에 반영;
- access key ID조차 일반 log에 출력하지 않는다;
- credential provider 객체는 provider configuration이 소유한다.
### 27.2 Least privilege
역할 분리 후보:
- runtime managed transfer;
- presign issuer;
- reconciliation/cleanup;
- scanner read/quarantine;
- deployment qualification;
- break-glass retention administration.
각 역할은 bucket-wide wildcard 대신 destination prefix와 action을 제한한다. Presign issuer 권한은
grant 가능한 최대 권한의 상한이다.
### 27.3 Network
- TLS 1.2 이상;
- hostname/certificate verification;
- outbound DNS/host/port allowlist;
- VPC endpoint/private network 사용 시 policy와 DNS qualification;
- proxy 사용 시 CONNECT/authority/credential leak 검증;
- endpoint override는 static approved binding만 허용;
- SSRF-style request-controlled host/key/presign authority 금지.
### 27.4 Content safety
- extension과 client media type을 신뢰하지 않는다;
- maximum decompressed/archive expansion 같은 business risk는 scanner/application이 제한;
- dangerous format은 quarantine;
- scan engine failure/timeout은 clean이 아니다;
- malicious object는 isolation/retention/audit 정책에 따라 처리;
- public download에 `Content-Disposition`, `X-Content-Type-Options` 등 inbound response 정책 적용;
- active content를 same-origin inline으로 제공하지 않는다.
### 27.5 Confidentiality
- 모든 provider object/control record private;
- encryption at rest profile required;
- control record에 secret/presigned URL/PII 최소화;
- sensitive metadata는 application DB 또는 encrypted manifest;
- object/reference/key를 metric tag로 금지;
- debug body logging 금지;
- heap dump/core dump risk와 buffer zeroing 필요성을 data classification별로 검토.
### 27.6 Audit
감사 event:
- operation reserved/terminal;
- direct grant issued/expired;
- scan verdict;
- publication/retirement/purge;
- retention held/break-glass;
- qualification mismatch;
- cleanup decision;
- corruption/indeterminate manual resolution.
Audit에는 safe hashed operation/reference token, destination, action, actor/correlation, outcome,
policy revision을 기록한다. URL, raw key, filename, content는 기록하지 않는다.
### 27.7 Threat-control-evidence matrix
| Threat | Required control | Required evidence |
| --- | --- | --- |
| raw key/path traversal or symlink escape | generated canonical key, handle-relative no-follow filesystem access | property/race/security test |
| overwrite/reused direct URL | immutable key, atomic create-only, bucket policy | concurrent/replay provider test |
| oversized upload cost DoS | provider-enforced hard length or bounded ingress | excess-body security/cost-bound test |
| URL/credential leakage | redaction, short TTL, secret-safe telemetry | captured log/trace/audit negative test |
| malicious active content | quarantine, exact-version scan fence, fail-closed publish | clean/malicious/stale/timeout workflow test |
| confused bucket/account/endpoint | expected owner/deployment identity, endpoint allowlist, TLS | wrong-owner/host/cert startup test |
| checksum substitution | typed full/composite/part checksum, expected SHA-256 | algorithm/scope mismatch test |
| mutation ACK loss | pending effect, phase-specific indeterminate reconciliation | dropped-response fault test |
| DB/object split-brain | pending DB state, outbox handoff fence, explicit abort authorization | crash-at-every-gap test |
| forged multipart part claim | nonce/session/fence, ListParts verification, opaque token | forged/stale/cross-session test |
| cleanup data loss | supported schema, exact version, handoff/retention fence, report-only | cleanup-vs-late-commit fault test |
| privilege/hold bypass | retire/purge port split, least privilege, no default governance bypass | IAM/retention negative test |
| stale policy/route | frozen policy revision, retained route registry, no current fallback | rolling migration/revision removal test |
| body/resource exhaustion | aggregate buffers/concurrency/timeouts/cancellation | heap/direct-memory/FD/slow-peer test |
## 28. Resource budget, timeout, retry, cancellation
### 28.1 Budget dimension
Provider/global/destination/tenant별로 제한한다.
- maximum object bytes;
- maximum delivered range bytes;
- chunk/buffer bytes;
- concurrent managed uploads/downloads;
- concurrent multipart sessions;
- parts per session;
- concurrent parts per session;
- total in-flight part bytes;
- HTTP connection concurrency;
- pending connection acquire;
- control record size;
- pending operation count;
- cleanup batch size;
- scan backlog;
- presign issue rate.
`byte[]` 전체 materialization을 없애도 concurrent buffer 곱이 heap/direct-memory를 초과할 수
있으므로 aggregate budget test가 필요하다.
Managed/server-mediated path는 actual in-flight resource를 admission control한다. Direct presigned
path는 grant issuance와 worst-case replay exposure만 제어하며 provider-side actual request
concurrency/bytes를 strict bound했다고 주장하지 않는다.
### 28.2 Deadline decomposition
하나의 “timeout” 필드로 합치지 않는다.
- application operation deadline;
- connection acquire;
- DNS/connect;
- TLS negotiation;
- SDK API call;
- SDK API attempt;
- socket read/write idle;
- producer/consumer stall;
- scan;
- reconciliation;
- graceful shutdown;
- presign/session expiry.
Child timeout의 합과 retry backoff가 parent deadline을 넘지 않게 compile한다.
### 28.3 Retry amplification
최악의 physical attempt:
```text
application retry
x operation-kernel retry
x SDK retry
x multipart part count
x concurrent workers
```
Binding compiler가 최대 증폭을 계산하고 upper bound를 넘으면 startup 실패시킨다. Metric은
logical operation과 physical SDK attempt를 분리한다.
### 28.4 Backpressure
- bounded executor/queue;
- semaphore admission;
- connection acquire queue 상한;
- multipart concurrency 상한;
- producer가 sink보다 빠르게 무한 buffer하지 않음;
- consumer가 느리면 provider read를 bounded 방식으로 늦춤;
- overload는 `CAPACITY_EXHAUSTED`/429·503 mapping;
- admission 거부가 thread starvation보다 먼저 발생.
### 28.5 Cancellation
Cancellation point:
- before producer start;
- between chunks;
- while SDK future/HTTP body active;
- between multipart parts;
- scan wait;
- reconciliation wait.
Cancellation 뒤:
- provider request cancel/response body close;
- partial single upload/multipart abort;
- operation state persist;
- resources/semaphore release;
- caller에게 `CANCELLED` 또는 mutation certainty에 따른 `INDETERMINATE`;
- cancellation을 success로 기록하지 않음.
### 28.6 Retryable producer
Managed upload의 producer를 SDK가 임의로 재호출하게 하지 않는다.
- single-pass producer는 one logical data production;
- transport retry가 body replay를 요구하면 adapter-owned bounded spool 또는 explicit repeatable
producer factory가 있어야 한다;
- spool은 private filesystem/object, quota, encryption, cleanup을 갖춘 별도 optional 전략;
- repeatability가 없으면 mutation을 reconcile하거나 new operation을 요구한다;
- input servlet stream을 재사용 가능하다고 가정하지 않는다.
## 29. Reconciliation, cleanup, LIST
### 29.1 Reconciler
Reconciler는:
- exact operation ID/reference/session으로 lookup;
- state/version CAS lease;
- bounded batch/deadline;
- provider exact HEAD/GET;
- normalized evidence comparison;
- deterministic continuation/compensation;
- terminal receipt restoration;
- audit/metric
을 수행한다.
Application retry path의 inline resolve와 background worker가 같은 kernel을 공유한다.
### 29.2 Candidate discovery
Discovery source:
- durable queue/outbox;
- deterministic age-partitioned control prefix;
- bounded LIST with continuation cursor;
- retry/dead-letter registry.
LIST의 문제:
- large namespace 비용;
- pagination;
- concurrent add/delete;
- provider별 ordering/consistency;
- permission 제한.
따라서 LIST는 후보를 놓치지 않도록 반복하는 maintenance 수단이며 단일 object truth가 아니다.
### 29.3 Cleanup eligibility
삭제 전 모두 만족:
- owned namespace;
- supported schema;
- operation terminal/expired 상태;
- minimum age;
- active lease/fence 없음;
- published reference가 가리키지 않음;
- active application handoff claim 없음;
- verified/staged business object이면 application이 DB/outbox를 조회한 뒤 발급한 exact
`ObjectAbortAuthorization`;
- provider retention/legal hold 없음;
- exact version precondition;
- report-only 결과와 delete 계획 audit.
Age, LIST 부재, expired worker lease만으로 `ObjectAbortAuthorization`을 대체하지 않는다.
Application 확인이 불가능하면 destructive cleanup을 보류한다.
### 29.4 Cleanup mode
- `disabled`;
- `report-only`;
- `delete`.
Production 첫 활성화는 report-only 기간과 샘플 검토 뒤 delete로 전환한다. Mode 변경은 audit하고
blast-radius limit를 둔다.
### 29.5 Unknown/newer object
Unknown prefix, malformed record, newer schema, missing ownership evidence는:
- 삭제하지 않는다;
- metric/audit/alert;
- quarantine candidate report;
- manual or upgraded reconciler 대상.
### 29.6 Notifications
Provider object event는 reconciliation을 빠르게 하는 hint일 수 있다.
- event 중복/순서 뒤바뀜/유실을 허용;
- exact state lookup 후 처리;
- event payload를 truth로 사용하지 않음;
- inbound event verification은 inbound/messaging adapter 소유;
- objectstorage outbound leaf가 messaging sibling adapter를 의존하지 않음.
## 30. Versioning, retention, retirement, purge
### 30.1 Lifecycle 용어
- detach: business entity가 reference를 더 이상 사용하지 않음;
- retire: reference를 download/publication 대상에서 제거;
- delete marker: provider current view 변경;
- purge: exact physical object version 제거;
- expire: policy age 도달;
- held: retention/legal hold로 purge 금지.
용어를 섞지 않는다.
### 30.2 Immutable replacement
Replace는:
1. 새 immutable object/reference publish;
2. DB CAS로 새 reference 선택;
3. old reference retire;
4. retention 후 old exact version purge.
같은 key overwrite가 아니다.
### 30.3 Retention source
Retention은:
- business minimum;
- security/quarantine;
- audit/control record;
- provider lifecycle;
- legal hold
의 합성이다. 가장 긴/강한 requirement를 적용한다. Application이 짧은 TTL을 보내 provider
compliance retention을 줄일 수 없다.
### 30.4 Physical purge
- exact object version/token;
- current reference graph 확인;
- retention/hold 조회;
- conditional delete;
- delete response loss reconcile;
- versioned provider에서 delete marker와 version delete 구분;
- manifest/control record tombstone 보존;
- audit/replay/retention horizon 뒤에도 §11.4의 epoch seal/rejection record protocol을 거쳐
control record를 정리.
AWS versioned bucket에서 noncurrent version의 exact purge identity는 `versionId`다. `If-Match`가
current version에 대해 평가되는 의미를 noncurrent-version CAS로 일반화하지 않는다.
- unversioned/current object: provider가 증명한 ETag `If-Match`와 exact key;
- noncurrent version: exact `versionId`, reference graph/retention fence, provider-specific delete
evidence;
- delete marker: marker version ID를 별도 type으로 구분.
Provider가 concurrent safety를 증명하지 못하면 purge card를 `UNVERIFIABLE`로 둔다.
### 30.5 Quarantine retention
Malicious object는 즉시 public visibility에서 격리하지만 physical delete 시점은 security/audit
policy가 결정한다. Scanner verdict evidence와 content 접근 권한을 최소화한다.
## 31. Observability
### 31.1 Metrics
예시:
- `object_storage_operation_total{destination,provider,operation,outcome}`;
- `object_storage_operation_duration_seconds{destination,provider,operation}`;
- `object_storage_bytes_total{destination,provider,direction,outcome}`;
- `object_storage_inflight{destination,provider,operation}`;
- `object_storage_sdk_attempt_total{provider,operation,outcome}`;
- `object_storage_reconciliation_total{provider,resolution}`;
- `object_storage_indeterminate_current{destination,provider,operation}`;
- `object_storage_orphan_candidate_current{destination,provider,type}`;
- `object_storage_multipart_session_current{destination,provider,state}`;
- `object_storage_cleanup_total{provider,mode,outcome}`;
- `object_storage_credential_refresh_total{provider,outcome}`;
- `object_storage_capability_mismatch_total{provider,capability}`;
- `object_storage_buffer_bytes{provider,direction}`;
- `object_storage_pool_pending_acquire{provider}`.
허용 tag:
- logical destination;
- exact provider type;
- operation family;
- normalized outcome/error;
- capability/card revision의 bounded token.
금지 tag:
- bucket/key/path;
- object/reference/operation raw ID;
- tenant/user;
- filename/media metadata;
- endpoint;
- presigned URL;
- provider request ID.
### 31.2 Trace
한 logical operation span 아래:
- binding;
- admission;
- producer/consumer;
- provider attempt;
- integrity verify;
- control transition;
- scan wait;
- reconciliation
span을 둔다. Content와 URL을 attribute로 넣지 않는다. Provider request ID가 필요하면 bounded hash와
restricted debug log로만 연결한다.
### 31.3 Logging
- terminal transition과 operator action은 structured info/audit;
- transient attempt는 rate-limited debug;
- payload/body, URL, credential, raw key 금지;
- exception message sanitize;
- same operation retry log storm 억제;
- cleanup report는 bounded sample + aggregate count.
### 31.4 Health
- liveness는 object storage를 호출하지 않는다;
- readiness는 required destination/card만 평가한다;
- optional destination 장애는 degraded로 노출하되 전체 readiness 정책은 composition에서 결정;
- health request마다 bucket/list/write를 하지 않는다;
- cached qualification + low-rate sentinel probe;
- dependency outage, credential refresh, control corruption, capacity saturation을 구분.
### 31.5 SLO
Provider/card별:
- managed upload success/latency;
- published download success/latency;
- direct completion verification latency;
- indeterminate resolution age;
- orphan backlog age;
- multipart abandon age;
- scan pending age;
- credential expiry horizon
를 정의한다. Provider SDK success rate만으로 business publication SLO를 계산하지 않는다.
## 32. Readiness card
### 32.1 Card ID
한 module card가 아니라 capability별 card:
- `object-storage-managed-upload-single`;
- `object-storage-managed-upload-multipart`;
- `object-storage-managed-download`;
- `object-storage-direct-upload-single`;
- `object-storage-direct-upload-multipart`;
- `object-storage-direct-download`;
- `object-storage-quarantine-publication`;
- `object-storage-retention`;
- `object-storage-reconciliation`.
Registry entry는 exact provider type/version과 destination profile을 dimension으로 가진다.
### 32.2 R0
- application port/value object compile;
- adapter/provider seam compile;
- settings disabled by default;
- no framework/provider type leak;
- unit tests for validation/fingerprint/state.
### 32.3 R1
- local-dev and version-pinned MinIO functional topology;
- bounded managed stream;
- exact head/range;
- checksum happy/mismatch;
- immutable create/conflict;
- control record codec/CAS basic;
- lifecycle/close tests;
- sample consumer contract.
R1은 production security/failure recovery를 뜻하지 않는다.
### 32.4 R2
모든 R2 card의 공통 evidence:
- production-like TLS/credential/ownership/encryption;
- startup qualification;
- connection/DNS/TLS/read/write timeout;
- throttling and retry amplification;
- request/response loss;
- process kill/crash recovery;
- rolling schema compatibility;
- observability/redaction;
- no silent test skip.
Card별 추가 evidence:
| Card | Required evidence |
| --- | --- |
| managed-upload-single | bounded producer/heap, immutable create, checksum, hard size, single-put response-loss reconciliation |
| managed-upload-multipart | adapter-owned session/upload ID/part ledger, bounded buffers/parts, complete/abort/409/response-loss, orphan cleanup, full digest |
| managed-download | exact published version, full/range read, bounded consumer/heap, truncated/slow/failed consumer, response-body close |
| direct-upload-single | atomic create-only, provider-enforced hard size ceiling, signed checksum/header, expiry/CORS/public endpoint, completion HEAD/verification |
| direct-upload-multipart | session/part ledger, CORS exposed evidence, acknowledge handshake, part budget, complete/abort/409/response-loss, orphan cleanup |
| direct-download | authorization-before-grant, published exact version, expiry/signature-age, response headers, URL redaction/revocation limitation |
| quarantine-publication | unpublished narrow read, scanner fence/policy, clean/malicious/indeterminate, handoff/DB crash gaps, cleanup authorization race |
| retention | versioning, exact version purge, delete marker, lifecycle, retention/legal hold, privileged purge separation |
| reconciliation | pending-effect phase, authoritative absence, conditional race/CAS, process kill, replay tombstone, backup/restore reconciliation |
Card가 요구하지 않는 multipart, presign, retention evidence를 억지로 요구하지 않는다. 반대로 다른
card의 evidence를 가져와 해당 card가 준비됐다고 주장하지 않는다. Exact named operation profile
조합이 matrix의 모든 relevant evidence를 만족해야 한다.
### 32.5 R3
- multi-node failover/fencing;
- rolling provider/application upgrade;
- regional/cluster disaster recovery;
- restore ordering and integrity audit;
- sustained scale/soak;
- quota/capacity exhaustion;
- credential/KMS rotation under load;
- provider version upgrade/rollback;
- operational game day and runbook evidence.
### 32.6 Card claim rule
Readiness claim은 다음 형태다.
```text
card + provider exact type/version + destination profile + evidence revision
```
예:
```text
object-storage-managed-upload-single
/ aws-s3-general-purpose
/ poster-image-v1
/ R2
/ evidence-2026-08-...
```
“Objectstorage R2”처럼 범위를 생략한 표현은 금지한다.
## 33. Test strategy
### 33.1 Application-core unit/property
- ID/reference syntax and check digits;
- operation key/epoch seal/rejection/rotation;
- canonical request fingerprint golden vector;
- content digest representation;
- state transition table;
- same operation/same fingerprint;
- same operation/different fingerprint;
- range arithmetic/overflow;
- deadline/budget validation;
- typed error/outcome exhaustiveness;
- framework/AWS type absence.
### 33.2 Provider-neutral contract
모든 qualifying provider에 같은 semantic suite:
- empty/one-byte/chunk-boundary/maximum-size upload;
- immutable create and conflict;
- exact descriptor/version;
- full and range read;
- short/slow/failing producer;
- slow/failing consumer;
- checksum match/mismatch;
- cancel/resource close;
- same operation terminal replay;
- different fingerprint conflict;
- conditional retire/delete;
- indeterminate resolve;
- control schema compatibility.
Provider unsupported capability test는 silent skip 대신 descriptor와 expected `UNSUPPORTED`를
검증한다.
### 33.3 Filesystem
- traversal/root alias/absolute path/Unicode ambiguity;
- symlink swap and nested symlink;
- exclusive create race;
- process kill before/after force/publish/control CAS;
- disk full, inode exhaustion, permission denied, read-only mount;
- mount identity mismatch;
- cleanup unknown/newer schema;
- file descriptor leak;
- restrictive permission.
### 33.4 MinIO
- version-pinned Testcontainers;
- actual streaming;
- conditional create/control CAS;
- multipart part/complete/abort;
- presigned PUT/GET and signed headers;
- grant response-loss/reissue/retirement linearization;
- checksum/head;
- versioning/lifecycle/retention where card claims;
- network cut via Toxiproxy;
- process restart;
- concurrent operations;
- endpoint/path-style/public presign;
- no Docker이면 selected readiness task가 성공으로 끝나지 않음.
Developer fast test는 Docker 없이 skip할 수 있지만 readiness task는 required environment 부재를
failure로 처리한다.
### 33.5 AWS sandbox
AWS-only R2 evidence:
- actual account/region/bucket owner;
- TLS/VPC endpoint if used;
- workload role/temporary credential refresh;
- Block Public Access/object ownership;
- SSE-KMS/DSSE profile;
- conditional write/delete;
- strong consistency assumption test boundary;
- versioning/delete marker/noncurrent purge;
- lifecycle/incomplete multipart;
- Object Lock/retention optional card;
- KMS and S3 throttling;
- credential/permission revocation;
- presigned URL expiration and signed checksum;
- request IDs captured safely.
Sandbox resource provisioning/cleanup은 IaC와 unique namespace를 사용한다.
### 33.6 Fault/concurrency
- two writers same operation/same fingerprint;
- same operation/different fingerprint;
- two new operations replacing same attachment;
- DB UploadIntent reserve/pending/abort fence race;
- direct grant response loss, multiple outstanding generations, expiry/in-flight horizon;
- same multipart part reissue, stale late request, acknowledge/complete race;
- operation epoch multi-node cutover/seal/rollback;
- response dropped after put/complete/delete/reference CAS;
- DB crash at every §15 gap;
- worker lease expiry and takeover;
- cleanup versus late finalize;
- retention activated during purge;
- scanner delayed/duplicate/stale verdict;
- rolling old/new control schema.
### 33.7 Performance/resource
- object size가 커져도 heap이 size와 선형 증가하지 않음;
- configured chunk/pool/multipart aggregate bound;
- direct memory/FD/thread stability;
- slow producer/consumer;
- connection pool saturation;
- retry storm;
- large concurrent range download;
- graceful shutdown with in-flight operation;
- long soak with cleanup/reconciliation.
### 33.8 Security/config
- disabled zero-side-effect context;
- canonical/legacy conflict;
- secret redaction;
- invalid endpoint/owner/credential pair;
- no plaintext production endpoint;
- no auto-create;
- unsupported required capability startup failure;
- presigned URL absent from logs/traces;
- raw key/path/URI absent from public receipt;
- ACL/public access configuration mismatch;
- malicious filename/metadata/control character.
## 34. Gradle, CI, supply chain
### 34.1 Focused task
현재 owner leaf:
```bash
cd src
./gradlew :adapter:outbound:objectstorage:test --console=plain
```
향후 task 후보:
```text
:adapter:outbound:objectstorage:objectStorageUnitTest
:adapter:outbound:objectstorage:objectStorageFilesystemContractTest
:adapter:outbound:objectstorage:objectStorageMinioContractTest
:adapter:outbound:objectstorage:objectStorageMinioFaultTest
:adapter:outbound:objectstorage:objectStorageAwsQualificationTest
:adapter:outbound:objectstorage:objectStorageSecurityTest
:adapter:outbound:objectstorage:objectStorageResourceTest
```
Task 이름과 exact selected provider/card를 machine-readable readiness registry에 연결한다.
### 34.2 CI lanes
- PR fast: unit, architecture, filesystem, mocked mapping;
- PR container: MinIO common contract;
- scheduled fault: MinIO/Toxiproxy/process restart;
- protected AWS: sandbox qualification/security;
- release gate: required production cards, schema compatibility, supply-chain scan;
- soak/game day: R3 evidence.
### 34.3 Architecture verification
필수:
```bash
./gradlew verifyCleanArchitectureDependencies
./gradlew test
./gradlew check
```
추가 ArchUnit/compile checks:
- `application-core`에 `software.amazon.awssdk`, Spring, `Path`, inbound type 없음;
- objectstorage leaf가 sibling adapter/sample/bootstrap을 의존하지 않음;
- controller가 repository/SDK/persistence entity를 사용하지 않음;
- raw inbound DTO가 application/domain으로 유출되지 않음.
### 34.4 Dependency
- AWS SDK BOM/lock exact pin;
- async HTTP implementation 선택과 transitive dependency review;
- CRT 사용 시 native binary provenance/SBOM;
- CVE/license scan;
- checksum/crypto provider 정책;
- Testcontainers/MinIO image digest pin;
- dependency update 후 provider qualification 재실행;
- unused provider dependency를 runtime image에서 제거할 필요가 생기면 leaf split 검토.
## 35. Migration plan
### Phase 0 — Truth and characterization
변경:
- 현재 CRUD/overwrite/default activation/sample transaction을 characterization test;
- existing settings/env/runtime usage inventory;
- public `file://`/`s3://` response 소비자 확인;
- current MinIO test를 R1로 명명.
Acceptance:
- 현재 동작과 위험이 evidence로 고정;
- production readiness claim 없음.
Rollback:
- code behavior change 없음.
### Phase 1 — Framework-free semantic contract
변경:
- ID/reference/version/digest/range/error/outcome;
- streaming callback;
- managed publication/inspection/transfer port;
- existing port는 deprecated compatibility seam;
- default disabled settings skeleton.
Acceptance:
- application-core pure unit/architecture test;
- byte[] path를 새 business flow가 사용하지 않음;
- provider type leak 0.
Rollback:
- old port consumer 유지, 새 binding disabled.
### Phase 2 — Provider-neutral kernel and local R1
변경:
- key/reference/control codec;
- operation fingerprint/state machine;
- local-dev provider;
- bounded stream/checksum/immutable create;
- single-node reconciliation;
- disabled zero-side-effect composition.
Acceptance:
- provider-neutral contract;
- filesystem security/crash tests;
- R1 card만 게시.
Rollback:
- new capability disabled, old local example 유지.
### Phase 3 — S3 managed transfer common subset
변경:
- async S3 client;
- finite timeout/pool/retry;
- streaming put/get/head/range;
- conditional data/control operation;
- checksum/encryption/version evidence;
- exact AWS/MinIO provider package 분리.
Acceptance:
- MinIO contract/fault R1;
- AWS sandbox common-subset partial target evidence, Phase 6 전 R2 card claim 금지;
- heap/resource bound.
Rollback:
- destination provider binding을 qualified previous provider로 전환;
- operation/reference schema backward-readable.
### Phase 4 — Direct transfer and multipart
변경:
- presigner;
- opaque session/part token;
- multipart ledger/complete/abort/reconcile;
- public signing endpoint;
- browser POST optional.
Acceptance:
- URL/header/expiry/redaction;
- complete response-loss;
- orphan cleanup/lifecycle backstop;
- provider/card exact evidence.
Rollback:
- direct card disabled;
- managed server upload/download 유지;
- existing sessions drain/expire/reconcile.
### Phase 5 — Staged scan/publication and sample migration
변경:
- staged port;
- scan verdict seam;
- durable reference publish;
- Poster pending/ready/retired state;
- short DB transaction/outbox worker;
- public DTO opaque reference.
Acceptance:
- every crash gap test;
- unscanned object inaccessible;
- concurrent replacement deterministic;
- old object retirement;
- no raw locator exposure.
Rollback:
- new upload admission 중지;
- pending operation drain/reconcile;
- existing published reference reader 유지;
- old API 제거 전 dual-read compatibility.
### Phase 6 — Production security and maintenance R2
변경:
- exact settings/qualification;
- workload credential/expected owner/TLS;
- ownership/BPA/encryption/versioning/lifecycle;
- reaper/report-only/delete;
- exact reconciliation card를 위한 disposable namespace bounded backup/restore reconciliation;
- readiness/metrics/audit/runbooks.
Acceptance:
- exact required card R2;
- protected fault/security tests;
- cleanup report review;
- no false global claim.
Rollback:
- maintenance delete -> report-only;
- provider/card admission off;
- published reads 유지;
- manual reconciliation queue 보존.
### Phase 7 — R3 and split review
변경:
- multi-node fencing;
- rolling upgrade, regional/cluster disaster-recovery game day, restore under failover, scale;
- provider leaf split decision;
- optional retention/direct/high-throughput cards.
Acceptance:
- R3 evidence and game day;
- split/no-split ADR;
- restore integrity audit.
Rollback:
- optional card disable;
- last qualified provider/schema write version.
## 36. Existing API compatibility and removal
### 36.1 Legacy port
기존 `ObjectStoragePort`는 migration 동안:
- `@Deprecated`와 explicit legacy name;
- production profile disabled;
- separate legacy root/bucket prefix;
- public URI 반환을 신규 API가 재사용하지 않음;
- usage metric;
- removal deadline
을 가진다.
Legacy와 new namespace가 겹치면 startup 실패한다.
### 36.2 Existing data
Raw key를 저장한 Poster data migration:
1. raw key inventory;
2. object exact HEAD/digest/media/size;
3. immutable new reference manifest 생성;
4. DB row를 opaque reference로 CAS migration;
5. dual-read 기간;
6. public response에서 locator 제거;
7. legacy object retirement;
8. unknown/missing/corrupt data report.
Migration이 existing key를 무조건 rename/copy/delete하지 않는다. Provider/version/retention에 따라
별도 plan을 생성한다.
### 36.3 API contract
기존 upload response의 `key`/`location` 제거는 breaking change다.
새 response 후보:
```json
{
"reference": "osr1....",
"size": 12345,
"mediaType": "image/png",
"digest": {
"algorithm": "SHA-256",
"value": "..."
},
"state": "READY"
}
```
실제 public field/version은 inbound API 설계와 snapshot test로 승인한다. Presigned URL은 별도
authorization endpoint의 ephemeral response이며 stored object DTO에 영구 포함하지 않는다.
## 37. 구현 계획 작성 전 확정 항목
정본 구현 순서와 아래 결정의 현재 freeze/approval gate는
[Object Storage Production Capability Implementation Plan](../plans/2026-07-28-objectstorage-production-capability.md)에
기록한다. Tasks 124의 구현은 Batch E pause checkpoint까지 진행됐지만 Batch E 전체
combined/root gate는 아직 통과하지 않았다. Tasks 25–30은 미착수다. 이 구현 진행이나
계획 승인이 public API, scanner provider 또는 AWS/IaC 외부 변경 권한을 자동으로 부여하지
않는다.
구현 계획은 다음 결정을 task 단위로 명시해야 한다.
- exact Java type/package 이름;
- old/new port coexistence 기간;
- control record serialization format;
- conditional CAS primitive;
- reference text format과 check digit;
- async S3 HTTP implementation;
- approved AWS SDK version;
- local-dev root;
- exact AWS/MinIO test versions;
- application deadline/cancellation representation;
- scanner capability owner와 port;
- sample Poster schema/outbox migration;
- bootstrap registry edge 필요 여부;
- canonical env/settings/secrets registry 변경;
- machine-readable readiness registry schema;
- cleanup lease/fence;
- API breaking-change versioning.
이 중 architecture 또는 public contract를 바꾸는 선택은 brainstorming/설계 승인 없이 구현
task에서 임의 결정하지 않는다.
## 38. Completion criteria
### 38.1 Design complete
- 현재 코드와 sample workflow의 증거가 기록됨;
- alternatives와 selected architecture가 기록됨;
- port/identity/state/protocol/provider/config/security/test/migration 결정이 연결됨;
- official primary references로 변동 가능한 provider 의미가 뒷받침됨;
- 상위 설계에서 dedicated design으로 링크됨;
- 독립 리뷰에서 blocker/high가 해소됨;
- 문서 검증과 LLM Wiki capture 또는 차단 사유가 기록됨.
### 38.2 Implementation complete
다음이 모두 있어야 하며 이 문서 작성만으로 충족되지 않는다.
- approved implementation plan;
- test-first code;
- focused/common/full architecture verification;
- exact provider/card readiness evidence;
- sample workflow migration;
- settings/env/secrets/runtime docs;
- runbook and observability;
- Wiki branch-note;
- human code review.
### 38.3 R2 complete
Provider/card별 §32.4 evidence와 production-like qualification이 있어야 한다. MinIO byte[] happy
path, mocked AWS test, filesystem unit test만으로는 R2가 아니다.
## 39. 금지하는 완료 표현
다음 표현은 해당 exact evidence 없이 사용하지 않는다.
- “ETag은 object MD5다.”
- “presigned URL은 한 번만 쓸 수 있다.”
- “S3-compatible이므로 AWS S3와 동일하다.”
- “PUT이 성공했으므로 DB와 object가 원자적으로 commit됐다.”
- “DB rollback이 object upload도 취소했다.”
- “versioning이 켜져 있으므로 삭제됐다.”
- “delete가 204라 physical version이 사라졌다.”
- “HEAD bucket이 성공했으므로 모든 capability가 준비됐다.”
- “TLS와 SSE를 켰으므로 secure하다.”
- “auto-create가 편리하므로 production에서도 안전하다.”
- “normalize/startsWith로 symlink 공격을 막았다.”
- “MinIO test가 통과했으므로 AWS production ready다.”
- “multipart complete timeout이므로 적용되지 않았다.”
- “retry하면 정확히 한 번 upload된다.”
- “LIST에 없으므로 object가 없다.”
- “scan timeout이므로 clean으로 간주한다.”
- “direct upload 완료 callback을 받았으므로 검증됐다.”
- “objectstorage module이 R2다.”
## 40. 운영 runbook 요구
- required destination startup qualification mismatch;
- wrong bucket/account/region/endpoint;
- credential expiry/refresh/rotation;
- KMS deny/throttle/key disabled;
- TLS/certificate/DNS/VPC endpoint failure;
- connection pool/acquire saturation;
- managed upload/download timeout;
- checksum mismatch/corrupt object;
- scan backlog/scanner outage/malicious verdict;
- publication indeterminate;
- control record corruption/newer schema;
- multipart complete indeterminate;
- abandoned multipart growth;
- orphan/staged/quarantined backlog;
- cleanup report-only에서 delete 전환;
- cleanup wrong-scope kill switch;
- versioning suspended/delete marker growth;
- retention/legal hold;
- presigned URL leak;
- grant response loss/reissue exposure;
- public endpoint/CORS drift;
- MinIO node/restart/upgrade;
- AWS regional/provider outage;
- disk/inode/mount loss for filesystem provider;
- graceful shutdown with in-flight operations;
- database pending attachment backlog;
- UploadIntent/handoff fence stuck or abort race;
- operation epoch rotation/seal/compaction stuck;
- backup/restore 후 reference/data/control reconciliation;
- operation/reference lookup hot partition;
- provider SDK upgrade rollback;
- readiness card downgrade.
각 runbook은:
- detection signal;
- safe first action;
- admission/maintenance kill switch;
- evidence collection;
- reconciliation command;
- destructive step의 dry-run/report-only;
- rollback;
- incident/audit link
를 포함한다.
## 41. Primary references
### AWS S3 semantics
- [Amazon S3 User Guide](https://docs.aws.amazon.com/AmazonS3/latest/userguide/)
- [S3 conditional writes](https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-writes.html)
- [S3 conditional deletes](https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-deletes.html)
- [DeleteObject API and conditional headers](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html)
- [HeadObject API](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html)
- [CompleteMultipartUpload API](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html)
- [AbortMultipartUpload API](https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortMultipartUpload.html)
- [Checking object integrity](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity-upload.html)
- [Multipart upload overview](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html)
- [Multipart upload limits](https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html)
- [Abort a multipart upload](https://docs.aws.amazon.com/AmazonS3/latest/userguide/abort-mpu.html)
- [Abort incomplete multipart uploads with lifecycle](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpu-abort-incomplete-mpu-lifecycle-config.html)
- [Presigned URL capabilities and limitations](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html)
- [Signature V4 POST policy](https://docs.aws.amazon.com/AmazonS3/latest/developerguide/sigv4-HTTPPOSTConstructPolicy.html)
- [S3 versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html)
- [S3 versioning enablement examples and propagation note](https://docs.aws.amazon.com/AmazonS3/latest/userguide/manage-versioning-examples.html)
- [S3 delete markers](https://docs.aws.amazon.com/AmazonS3/latest/userguide/DeleteMarker.html)
- [S3 Object Lock](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html)
- [S3 server-side encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingEncryption.html)
- [S3 SSE-KMS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html)
- [S3 Object Ownership](https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html)
- [S3 security best practices](https://docs.aws.amazon.com/AmazonS3/latest/userguide/security-best-practices.html)
- [Expected bucket owner](https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-owner-condition.html)
- [S3 network isolation and TLS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/network-isolation.html)
### AWS SDK for Java 2.x
- [S3 asynchronous multipart client](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/s3-async-client-multipart.html)
- [S3 client examples and client comparison](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/examples-s3.html)
- [CRT-based S3 client](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/crt-based-s3-client.html)
- [S3 checksums](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/s3-checksums.html)
- [API timeout configuration](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/timeouts.html)
- [Retry strategy](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/retry-strategy.html)
- [HTTP client configuration](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/http-configuration.html)
- [SDK metrics](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/metrics.html)
- [SDK troubleshooting](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/troubleshooting.html)
- [SDK best practices](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/best-practices.html)
- [Default credentials provider chain](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/credentials-chain.html)
### MinIO AIStor future-card context and upload security
다음 MinIO 문서는 current AIStor product용이며 현재 OSS 2024 Testcontainers image의 readiness
evidence로 사용하지 않는다.
- [MinIO versioning](https://docs.min.io/aistor/administration/objects-and-versioning/versioning/)
- [MinIO object locking and immutability](https://docs.min.io/aistor/administration/object-locking-and-immutability/)
- [MinIO lifecycle rule patterns](https://docs.min.io/aistor/administration/object-lifecycle-management/lifecycle-rule-patterns/)
- [MinIO Java SDK API](https://docs.min.io/aistor/developers/sdk/java/api/)
- [OWASP File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html)