The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
921 lines
86 KiB
Markdown
921 lines
86 KiB
Markdown
# 09 · adapter-outbound-objectstorage
|
||
|
||
|
||
## SSOT identity — 2026-08-31 재검증
|
||
|
||
- registered leaf id: `adapter-outbound-objectstorage`
|
||
- canonical state `analysisFile`: `analysis/09-adapter-outbound-objectstorage.md` (이 문서) — 이 leaf의 단일 SSOT
|
||
- source path: `src/adapter/outbound/objectstorage` · Gradle `:adapter:outbound:objectstorage`
|
||
- registry `allowed_dependencies`: `["application-core", "shared-contract"]`
|
||
- registry `runtime_memberships`: `["sample-portfolio"]`
|
||
- coverage ledger: `FULL_READ` **206** / `STRUCTURAL_ONLY` **0** / `EXCLUDED` **0** / `UNCLASSIFIED` **0**
|
||
- 최초 분석 revision `a24ece9c` → 재검증 revision `21234e38` · 이 리프의 변경 파일 **0**
|
||
- 재검증 증거: `EVD-333`(소스 드리프트 0), `EVD-334`(lane 재실행)
|
||
|
||
> 재검증이 확인한 것은 대상이 움직이지 않았다는 사실이지, 아래 서술이 옳다는 보증이 아니다.
|
||
> 이번 사이클에서 코드에 대고 다시 확인한 항목은 이 문서의 검증 절과 위 증거가 가리키는 범위다.
|
||
|
||
---
|
||
> 상태: COMPLETE
|
||
> revision: `a24ece9cf797f7ea647e33bf846b115208ed1ba5`
|
||
> 경로: `src/adapter/outbound/objectstorage` · Gradle: `:adapter:outbound:objectstorage`
|
||
|
||
## 0. Denominator와 coverage ledger
|
||
|
||
tracked file **206개** — main 147 (14,336 LOC), test 48 + resource 1 (6,753 LOC), 별도 qualification source set 3개 (6 files, 546 LOC), governance 4. 총 약 21.6k LOC.
|
||
|
||
```json
|
||
{ "id": "adapter-outbound-objectstorage",
|
||
"gradle_path": ":adapter:outbound:objectstorage",
|
||
"allowed_dependencies": ["application-core", "shared-contract"],
|
||
"runtime_memberships": ["sample-portfolio"] }
|
||
```
|
||
|
||
`build.gradle`이 `strictTestLanes`로 세 개의 별도 source set을 선언한다 — `objectStorageMinioContractTest`, `objectStorageMinioFaultTest`, `objectStorageAwsQualificationTest`. AWS SDK v2 BOM은 Spring Boot BOM이 관리하지 않으므로 **모듈 범위**로 import되고, 그 이유가 주석에 적혀 있다("keeps the strict-locking blast radius to this module").
|
||
|
||
패키지 배치(main): `s3` 26 · `control` 24 · `kernel` 23 · `config` 19 · `direct` 13 · `readiness` 8 · `maintenance` 8 · `codec` 7 · `filesystem` 6 · `multipart` 5 · `provider` 4 · 루트 4.
|
||
|
||
### 하위 범위 원장
|
||
|
||
| # | 범위 | main | test | 기타 | 합 | 상태 |
|
||
|---|---|---|---|---|---|---|
|
||
| 1 | governance + `config/**` — opt-in · binding compiler · routing · capability | 19 | 5 | 4 | 28 | **COMPLETE** |
|
||
| 2 | `control/**` — canonical JSON codec + durable record 타입 | 24 | 1 | – | 25 | **COMPLETE** |
|
||
| 3 | `kernel/**` + `codec/**` — operation kernel · state machine · epoch · key/fingerprint codec | 30 | 9 | – | 39 | **COMPLETE** |
|
||
| 4 | `s3/**` — provider binding · client policy · async bridge · provider 구현 | 26 | 14 | – | 40 | **COMPLETE** |
|
||
| 5 | `direct/**` + `multipart/**` — direct transfer · multipart coordinator | 18 | 7 | – | 25 | **COMPLETE** |
|
||
| 6 | `filesystem/**` + `maintenance/**` + `readiness/**` + `provider/**` + 루트 | 30 | 12 | 1 | 43 | **COMPLETE** |
|
||
| 7 | qualification source set 3종 (minio contract / minio fault / aws) | – | – | 6 | 6 | **COMPLETE** |
|
||
| | **TOTAL** | **147** | **48** | **11** | **206** | **7 / 7** |
|
||
|
||
manifest: `evidence/raw/149-objectstorage-module-inventory.txt`.
|
||
|
||
---
|
||
|
||
## 1. Sub-scope 01 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **28 / 28 FULL_READ**
|
||
> 범위: governance 4 + `config/**` main 19 + 전용 test 5
|
||
> 역할: `app.object-storage`를 **비활성 기본값**에서 정확한 불변 바인딩으로 컴파일하고, 선택된 provider만 자원을 만들게 하며, 폐기된 alias를 격리한다
|
||
|
||
manifest와 probe: `evidence/raw/150-objectstorage-config-activation-probes.txt`.
|
||
|
||
## 2. Confirmed — "컴파일이 먼저, 생성은 나중"이 실제 순서다
|
||
|
||
`ObjectStorageProviderContribution`이 두 메서드의 계약을 나눈다.
|
||
|
||
> `describe` **must not resolve credentials, create files, clients, threads, or schedulers**. `create` owns cleanup of every partial allocation before it throws; after a successful return the assembler owns the returned lifecycle exactly once.
|
||
|
||
`ObjectStorageCapabilityAssembler.assemble`이 그 순서를 지킨다 — `compiler.compile(settings)`가 **전부** 끝난 뒤(`:25`)에야 선택된 destination을 돌며 `contribution.create(provider)`를 부른다(`:48`). 그리고 도중에 실패하면 이미 만든 것을 **역순으로** 닫는다(`:53–56`). `AssembledCapability.close()`도 역순이고 `AtomicBoolean`으로 정확히 한 번만 실행된다.
|
||
|
||
README의 "Settings compile fully before any selected provider creates a directory, client, thread, scheduler, or credential lookup"이 코드 구조로 성립한다.
|
||
|
||
컴파일러 자체가 fail-closed다. 비활성이면 빈 바인딩을 돌려주고, 활성인데 provider·destination·default destination 중 하나라도 비면 거부한다. provider마다 `describe`가 돌려준 서술자와 설정을 **대조**한다 — providerType 일치, version 일치, `maximumObjectBytes`가 서술자 상한 이하, `chunkBytes`가 서술자 상한 이하. chunk는 추가로 `1 ≤ chunk ≤ min(maxObject, 16 MiB)`이고 `Integer.MAX_VALUE`를 넘지 못한다. destination은 route token 중복을 거부하고, 요구한 capability를 provider가 `SUPPORTED`로 신고하지 않으면 거부하며, `SCAN_CLEAN`을 요구하는데 scanner seam이 없으면 이름을 대며 거부한다.
|
||
|
||
식별자 검증도 좁다 — `canonicalId`는 64자 이내, `[a-z0-9][a-z0-9_-]*`, 소문자, 그리고 **0x20–0x7e 밖 문자를 전부 거부**한다.
|
||
|
||
## 3. Confirmed — README가 "등록되지 않는다"고 적은 것들이 실제로 등록되지 않는다
|
||
|
||
`150-...` §8.2의 네 주장을 각각 추적했다.
|
||
|
||
| README 주장 | 확인 |
|
||
|---|---|
|
||
| "no direct-grant port is registered" | `RoutingObjectDirectGrantAdapter`는 private 생성자만 가진 빈 클래스이고, leaf 안에서 **자기 파일 밖 참조 0**(grep exit=1) |
|
||
| "Scanner and privileged purge composition remain separate and empty" | `ObjectStorageMaintenanceCapabilityConfig`는 본문이 **없는** `@Configuration`. scanner는 별도 `ObjectStorageScanMaintenanceConfig`에 있고 `app.object-storage.scan-maintenance.enabled=true`로만 켜진다 |
|
||
| "`filesystem-local-dev` is rejected under `prod`/`production`" | `ObjectStorageBindingCompiler:125`에 존재 |
|
||
| "Mixing any old alias with canonical settings fails startup without echoing values" | `LegacyObjectStorageActivationGuard`가 두 prefix가 동시에 있으면 `IllegalStateException`을 던지고, 메시지에 값이 없다 |
|
||
|
||
마지막 것의 구현이 특히 조심스럽다 — `hasPrefix`가 `EnumerablePropertySource`를 순회해 prefix로 **시작하는 이름이 있는지**만 보고, 열거 불가능한 source에 대비해 알려진 키 목록으로 fallback한다. 어느 경로에서도 값을 읽지 않는다.
|
||
|
||
적재 경로는 fileserver와 같다 — `AutoConfiguration.imports`가 없고(`150-...` §8.1, exit=1), `CaSkeletonApplication`의 명시적 `@ComponentScan`이 `dev.caskeleton.adapter.outbound.objectstorage`를 목록에 올린다(`:76`). 그리고 `app.object-storage.*`는 어느 `application.yml`에도 없으므로 실효 기본값은 **속성 부재**다.
|
||
|
||
## 4. Confirmed — legacy가 세 겹으로 격리돼 있다
|
||
|
||
폐기 경로가 셋인데 서로 다른 스위치를 쓰고 서로를 배제한다.
|
||
|
||
| 경로 | 스위치 | 성격 |
|
||
|---|---|---|
|
||
| 선호 임시 활성화 | `app.object-storage.legacy.enabled=true` + 명시적 backend | `ObjectStoragePort`(whole-`byte[]`) 노출 |
|
||
| 구 alias | `ca-skeleton.objectstorage.*` | `LegacyObjectStorageActivationGuard` 조건, canonical과 혼용 시 실패 |
|
||
| 채택(adoption) | `app.object-storage.legacy-adoption.enabled=true` | raw locator 유지보수 전용, 별도 config 클래스 |
|
||
|
||
`ObjectStorageBindingCompiler.rejectLegacyOverlap`가 legacy filesystem 루트와 canonical provider 루트가 **어느 방향으로든 포함 관계**면 거부한다. `LegacyObjectAdoptionSettings`는 `APPLY` 모드일 때 검토된 manifest 경로와 64자리 SHA-256을 요구하고, batch size 1–1000, timeout 5분 이내를 강제한다.
|
||
|
||
legacy runtime은 `AutoCloseable` holder로 감싸 S3 client 수명을 정확히 소유하고, `@Bean(destroyMethod = "close")`로 등록된다.
|
||
|
||
## 5. P3 — production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다
|
||
|
||
CLAUDE.md의 Forbidden 목록에 "local-dev in production"이 있고, 그것을 강제하는 코드는 이것 하나다.
|
||
|
||
```java
|
||
private boolean productionProfileActive() {
|
||
return activeProfiles.stream()
|
||
.map(profile -> profile.toLowerCase(Locale.ROOT))
|
||
.anyMatch(profile -> profile.equals("prod") || profile.equals("production"));
|
||
}
|
||
```
|
||
|
||
이 저장소가 `application-prod.yml`을 싣고 있으므로 **현재 형상에서는 맞는다**. 그리고 `150-...` §8.2c에서 확인했듯 같은 방식으로 production을 판정하는 leaf는 이것 하나뿐이다 — 저장소 전체가 공유하는 production 판별 장치가 없다.
|
||
|
||
문제는 방향이다. 이것은 **거부** 검사인데 판정 근거가 **허용 목록 두 개**다. `prd`, `production-eu`, `live`, `prod-apac` 같은 이름을 쓰는 fork는 이 검사를 통과하고, `filesystem-local-dev`가 production에서 조용히 선택된다 — 그 provider는 README가 "R1-only development provider"라고 적은 것이다. 실패는 startup이 아니라 데이터가 로컬 디스크에 쌓인 뒤에 드러난다.
|
||
|
||
**판정: P3.** 이 저장소 형상에서는 도달하지 않는다. 기록하는 이유는 (a) CLAUDE.md가 금지 항목으로 명시했고 (b) 강제 수단이 두 문자열이며 (c) fork가 프로파일 이름을 바꾸는 것은 평범한 일이기 때문이다. 수정은 production 판별을 명시적 설정(예: `app.object-storage.allow-local-dev=true`를 요구)으로 뒤집는 것 — 이름이 아니라 의도를 묻는 형태다.
|
||
|
||
## 6. P3/기록 — readiness registry가 build의 test 입력인데 leaf 소스가 그 파일명을 참조하지 않는다
|
||
|
||
`build.gradle:45–46`이 `docs/registries/object-storage-readiness.yaml`을 `test` task의 `inputs.file`로 선언한다. 그 파일의 헤더는 소유 test 둘을 이름으로 적는다.
|
||
|
||
```
|
||
# Repository owner test: dev.caskeleton.bootstrap.contract.ContractRegistrySchemaGovernanceTest
|
||
# Semantic owner test: dev.caskeleton.adapter.outbound.objectstorage.readiness.ObjectStorageReadinessRegistryTest
|
||
```
|
||
|
||
그런데 leaf 소스에서 `object-storage-readiness`라는 문자열을 검색하면 **매치가 없다**(`150-...` §8.4c, exit=1). 즉 semantic owner test는 파일명을 상수로 갖지 않고 다른 방식(경로 조립 등)으로 찾는다. 실제 결합 여부는 `readiness` 패키지를 읽는 **sub-scope 06**에서 확정한다 — 여기서는 이월 항목으로만 남긴다.
|
||
|
||
## 7. Confirmed — 후보로 본 unguarded split은 값 타입이 막고 있다
|
||
|
||
`RoutingObjectReadAdapter.load`가 `reference.canonicalText().split("\\.", -1)[1]`로 route token을 꺼낸다. 인덱스 검사가 없어 처음에는 `ArrayIndexOutOfBoundsException` 후보로 봤다.
|
||
|
||
`ObjectReference`를 확인한 결과 생성자가 `ObjectIdentitySupport.requireRouted(canonicalText, "osr1")`로 형태를 강제하므로, 유효하게 만들어진 참조에는 항상 route 구획이 있다(`150-...` §8.4d). 결함이 아니다.
|
||
|
||
## 8. Negative-space probes — sub-scope 01
|
||
|
||
- **8.1 reachability**: auto-configuration 등록 metadata 0, 적재는 명시적 component scan, 두 selector 모두 `application.yml`에 부재(속성 부재가 실효 기본값).
|
||
- **8.2 계약 ↔ 구현**: `describe`/`create`의 부작용 계약과 assembler의 실제 호출 순서(§2). README의 네 가지 "등록되지 않는다" 주장 전수 확인(§3).
|
||
- **8.2b 조건부 형제**: production 판정이 이 leaf에만 있고 저장소 공용 장치가 없음(§5).
|
||
- **8.3 중복 mechanism**: legacy 경로 셋이 서로 다른 스위치를 쓰고 겹침을 명시적으로 거부(§4).
|
||
- **8.4 문서/빌드 drift**: readiness registry의 build 입력 선언과 leaf 소스의 참조 부재(§6, sub-scope 06으로 이월).
|
||
|
||
## 9. Sub-scope 01 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P3** | `filesystem-local-dev` production 거부가 `prod`/`production` 두 리터럴에만 걸려 있다 — 다른 이름을 쓰는 fork는 R1 개발용 provider를 production에서 받는다 | 프로파일 이름을 바꾼 fork |
|
||
| **P3/기록** | readiness registry가 build의 test 입력인데 leaf 소스에 그 파일명 참조가 없다 — 실제 결합은 sub-scope 06에서 확정 | 이월 |
|
||
|
||
## 10. Sub-scope 01 완료 조건
|
||
|
||
- denominator 28 / 28 FULL_READ (`150-...` OWNED FILES)
|
||
- §8.1~§8.4 네 종 probe 수행, README의 네 가지 조립 주장을 각각 코드로 추적
|
||
- 후보 finding 1건(unguarded split)을 값 타입 검증으로 추적해 결함 아님으로 판정(§7)
|
||
- 소스 미변경
|
||
|
||
---
|
||
|
||
## 11. Sub-scope 02 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **25 / 25 FULL_READ**
|
||
> 범위: `control/**` main 24 (2,470 LOC) + 전용 test 1 (339 LOC)
|
||
> 역할: 열한 종의 durable 제어 레코드를 **닫힌 sealed 계열**로 두고, canonical-json-v1 + 외곽 SHA-256 봉투로 인코딩하며, 불변식을 레코드 생성자에서 강제한다
|
||
|
||
manifest와 probe: `evidence/raw/151-objectstorage-control-probes.txt`.
|
||
|
||
## 12. Confirmed — 계열이 닫혀 있고 스키마가 fail-closed다
|
||
|
||
`ObjectControlRecord`는 열한 개 구현만 허용하는 `sealed interface`이고, javadoc이 규칙을 적는다 — "Unknown families and schemas fail closed." codec의 `payload(...)`가 그 계열에 대해 **exhaustive switch**를 쓰므로, 새 레코드를 추가하면 컴파일이 강제로 codec을 갱신하게 만든다.
|
||
|
||
스키마 버전은 `ControlRecordSupport.header`가 `schemaVersion != 1`을 거부한다 — "only control schema version 1 is writable". 더 새로운 스키마를 만나면 덮어쓰지 않고 `UnsupportedObjectControlSchemaException`으로 격리한다("A newer or unknown durable schema that must be quarantined rather than overwritten").
|
||
|
||
`objectstorage.control` 패키지를 leaf 밖에서 참조하는 코드는 **0**이다(`151-...` §8.1, exit=1). CLAUDE.md의 "control-record types leaking into application-core" 금지가 가시성으로 성립한다.
|
||
|
||
## 13. Confirmed — canonical 표현이 "우리가 쓴 것과 바이트가 같은가"로 강제된다
|
||
|
||
`CanonicalJsonReader`는 관용을 두지 않는다.
|
||
|
||
- **필드 순서 고정**: `field(expectedName)`이 읽은 이름과 기대 이름을 비교한다 — 재배열은 실패.
|
||
- **공백 불허**: `expect(char)`가 정확히 그 문자만 소비한다. 공백을 건너뛰는 코드가 없다.
|
||
- **이스케이프 두 개만**: `\"`와 `\\` 외의 이스케이프는 실패.
|
||
- **printable ASCII만**: 0x20–0x7e 밖 문자는 reader·writer·`ControlRecordSupport` 세 곳 모두에서 거부.
|
||
- **숫자 정규형**: 선행 0(`00`, `01`)과 `-0`을 거부.
|
||
- **후행 콘텐츠 불허**: `end()`가 `cursor != input.length()`면 실패.
|
||
|
||
reader가 `new String(bytes, UTF_8)`로 관용 디코딩하는 것은 그 자체로는 malformed 바이트를 U+FFFD로 바꾸지만, U+FFFD(0xFFFD)는 0x7e를 넘으므로 **printable ASCII 검사에서 걸린다**. 즉 관용 디코딩이 뚫리지 않는다 — fileserver R1 저널(sub-scope 03, 08번 문서 §26)에서 같은 관용 디코딩이 열려 있던 것과 대비된다.
|
||
|
||
봉투도 이중으로 잠긴다. `decodeUnchecked`가 Base64를 디코딩한 뒤 **다시 인코딩해 문자열이 같은지** 확인하고(alias 거부), payload의 SHA-256을 `MessageDigest.isEqual`(상수시간)로 비교한다. 크기 상한이 계열별로 셋이다 — 봉투 64 KiB, terminal receipt 16 KiB, part 4 KiB — 그리고 encode·decode 양쪽에서 `enforceFamilySize`가 적용된다.
|
||
|
||
## 14. Confirmed — 레코드가 값을 믿지 않고 관계를 다시 계산한다
|
||
|
||
`ObjectOperationRecord`의 compact 생성자가 대표적이다. 넘겨받은 `policySnapshotDigest`를 그대로 쓰지 않고 **스냅샷에서 다시 계산해 대조**한다 — "policySnapshotDigest does not match the snapshot". `expectedContentIdentity`가 있으면 얼어붙은 정책 상한을 넘는지도 본다.
|
||
|
||
상태 짝도 강제된다.
|
||
|
||
- `(pendingEffect == null) != (effectCertainty == NOT_SENT)` → "pending effect and certainty do not agree"
|
||
- 미해결 pending effect가 있는데 새 것을 만들면 → "an unresolved pending effect already exists"
|
||
- `DATA_UPLOADED`로 전진하려면 업로드 증거가 확인돼 있어야 함
|
||
- terminal 전이는 `ABORTED`/`QUARANTINED`/`FAILED`만 허용
|
||
|
||
`ObjectPublicationHandoffRecord`는 lease를 fence와 함께 다룬다 — fence는 1 이상, `released && abortAuthorized` 동시 참 금지, 그리고 **쓰기 시점에 이미 만료된 활성 lease를 거부**한다("active handoff lease is expired at write time"). claimant는 지문(`hexDigest`)으로만 저장된다.
|
||
|
||
`ObjectStagedObjectRecord`는 scan 증거의 짝을 강제한다 — `(scanOperationId == null) != (scannerPolicyRevision == null)`이면 "scan verdict evidence is incomplete", 그리고 스캔은 `integrityVerified && publicationRequirement == SCAN_CLEAN`일 때만 시작할 수 있다.
|
||
|
||
`ObjectControlStore` 인터페이스에는 **`list`가 없다**. javadoc이 그 부재를 명시한다 — "exact-lookup/create/CAS control storage. **LIST is deliberately absent.**" 열거가 없으면 제어 평면을 훑어 다른 테넌트의 키를 발견하는 경로가 구조적으로 없다.
|
||
|
||
## 15. Negative-space probes — sub-scope 02
|
||
|
||
- **8.1 reachability**: `control` 패키지의 leaf 밖 참조 0(§12).
|
||
- **8.2 계약 ↔ 구현**: canonical 주장과 reader/writer의 실제 거부 목록 대조(§13). 관용 UTF-8 디코딩이 ASCII 검사로 닫히는지 확인.
|
||
- **8.2b 봉투 무결성**: Base64 재인코딩 대조 + 상수시간 digest 비교 + 계열별 크기 상한(§13).
|
||
- **8.3 닫힌 계열**: sealed interface와 exhaustive switch가 새 레코드 추가 시 codec 갱신을 강제(§12). `ObjectControlStore`에 `list` 부재(§14).
|
||
- **8.4 불변식**: 레코드 생성자가 digest를 재계산하고 상태 짝을 강제하는 지점 전수 확인(§14).
|
||
|
||
## 16. Sub-scope 02 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| — | **없음.** canonical 강제가 reader·writer·봉투 세 겹이고, 레코드 불변식이 값이 아니라 관계를 검증하며, 열거 API가 존재하지 않는다 | — |
|
||
|
||
## 17. Sub-scope 02 완료 조건
|
||
|
||
- denominator 25 / 25 FULL_READ (`151-...` OWNED FILES) — main 2,470 LOC 전수 판독
|
||
- §8.1~§8.4 네 종 probe 수행
|
||
- 관용 UTF-8 디코딩 후보를 ASCII 검사로 추적해 결함 아님으로 판정
|
||
- 소스 미변경
|
||
|
||
---
|
||
|
||
## 18. Sub-scope 03 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **39 / 39 FULL_READ**
|
||
> 범위: `kernel/**` 23 (1,463 LOC) + `codec/**` 7 (539 LOC) + 전용 test 9
|
||
> 역할: provider SDK 없이 **예약 → 보류 효과 → 확인 → 단계 전진**을 정확한 CAS 위에서 돌리고, 모든 네임스페이스 키를 단일 인코더로 만든다
|
||
|
||
manifest와 probe: `evidence/raw/152-objectstorage-kernel-codec-probes.txt`.
|
||
|
||
## 19. Confirmed — 다섯 개의 닫힌 전이표가 있고 terminal이 진짜 terminal이다
|
||
|
||
`ObjectOperationStateMachine`이 publication·scan·reference·direct-grant·multipart 다섯 계열의 전이를 각각 switch로 적는다. terminal 처리가 계열마다 명시적이다.
|
||
|
||
| 계열 | terminal 처리 |
|
||
|---|---|
|
||
| publication | `current.terminal()`이면 즉시 거부 |
|
||
| scan | `case CLEAN, MALICIOUS -> false`; `NOT_REQUIRED`는 어디로도 못 감 |
|
||
| reference | `case PURGED -> false` (PUBLISHED → RETIREMENT_PENDING → RETIRED → PURGE_ELIGIBLE → PURGED) |
|
||
| direct grant | terminal 집합 4종을 먼저 계산해 `!currentTerminal` 요구 |
|
||
| multipart | terminal 집합 5종에 대해 동일 |
|
||
|
||
뒤 둘은 **branch 전이**(EXPIRED/ABORTED/FAILED/CORRUPT)를 별도로 허용해, 정상 사슬 어디서든 실패로 빠질 수 있되 terminal에서는 나올 수 없게 한다.
|
||
|
||
`requireNextRevision(current, next)`가 `next == current + 1`을 강제한다 — revision은 건너뛰지도 되돌아가지도 못한다. `ObjectOperationStateMachineTest`가 그 셋을 이름으로 고정한다: `publicationFollowsScanFreeAndScanRequiredPaths`, `terminalOutOfOrderAndStaleRevisionTransitionsFailClosed`, `independentStateFamiliesDoNotImplyEachOther`.
|
||
|
||
## 20. Confirmed — 응답 유실을 "의도를 먼저 적는" 방식으로 다룬다
|
||
|
||
`StagedObjectPublicationKernel.stage`의 순서가 핵심이다.
|
||
|
||
1. `operations.reserve(...)` — 제어 저장소에 조건부 create. 충돌하면 기존 레코드의 `requestFingerprint`를 비교해 **CONFLICT / REPLAY_TERMINAL / REPLAY_NON_TERMINAL**로 분류한다.
|
||
2. `pendingEffect == null`이면 `markEffectSent(...)`로 **외부 mutation 전에** 의도를 durable하게 적는다 — kind(`DATA_PUT`), attemptId, 대상 증거 해시, 원하는 상태, precondition(`create-if-absent`), 요청 증거 다이제스트.
|
||
3. `resolveOrCreate(providerOperation, producer)` — provider 호출.
|
||
4. 성공하면 `confirmEffect(...)`, 그 다음에야 `advancePublication(DATA_UPLOADED)`.
|
||
|
||
`PendingObjectEffect`의 javadoc이 그 성격을 못박는다 — "Bounded, **non-secret** exact intent persisted before external mutation". 모든 필드가 길이 제한이 있고 대상은 해시로만 적힌다.
|
||
|
||
`ObjectOperationKernel.replace`는 read → `stored.record().equals(expected)` 비교 → `compareAndSet(key, mutation(stored.version(), replacement))` 순서다. 즉 낙관적 비교와 저장소 CAS를 겹쳐 쓴다. test `pendingEffectIsDurableBeforeIoAndResponseLossRemainsPhaseSpecific`와 `everyPendingMutationIsResolvedOnceWithoutBlindMutationReplay`가 이 성질을 고정한다 — 후자의 이름이 규칙을 그대로 말한다.
|
||
|
||
## 21. Confirmed — 모든 키가 단일 인코더에서 나오고 route를 벗어날 수 없다
|
||
|
||
`ObjectControlKeyCodec`과 `ObjectDataKeyCodec`이 각각 "Sole encoder"를 자칭하고, 저장소에서 `"control/v1/"`·`"data/v1/"` 리터럴은 이 두 파일에만 있다(`152-...` §8.3). 키 형태는 `control/v1/<family>/<route>/<shard>/<identity>`이고 shard는 identity의 SHA-256 앞 두 자리다.
|
||
|
||
route 격리가 구조적이다 — `requireMatchingRoute(route, routedIdentity)`가 routed identity의 route 구획을 파싱해 현재 route와 다르면 거부한다("routed identity belongs to a different route"). reference·session·stage handle 키 모두 이 검사를 지난다.
|
||
|
||
핸들 계열도 접두사로 분리된다 — `osh1`(stage), `osu1`(direct upload), `osm1`(multipart), `osv1`(version), `osr1`(reference). `ObjectNamespaceCodecTest.referenceAndHandleFamiliesRemainSeparated`가 그 분리를 고정하고, `dataKeyApiHasNoRawNameStringParameter`는 **API 서명 자체에 raw 이름 문자열이 없음**을 단언한다.
|
||
|
||
`CrockfordBase32`는 소문자 정규 알파벳(`0123456789abcdefghjkmnpqrstvwxyz` — I·L·O·U 제외)을 쓰고, 인코딩 후 남은 값이 있으면 거부한다("base32 output length is too small") — 잘림을 조용히 넘기지 않는다.
|
||
|
||
test에 property-based 검사가 있다 — `routeParserRejectsArbitraryNonCanonicalText(@ForAll String candidate)`(jqwik), `namespaceRejectsAliasesAndTraversalInputs`, 그리고 fingerprint codec에는 **golden vector**가 고정돼 있다(`canonicalIntentHasAFrozenGoldenVector`, `sameIntentIsStableAndEverySemanticChangeChangesTheFingerprint`).
|
||
|
||
`policySnapshotCodecIsCanonicalAndContainsNoCredentialSurface`는 정책 스냅샷에 자격증명 표면이 없음을 test로 고정한다.
|
||
|
||
## 22. P3/기록 — 보류 효과 전이가 `updatedAt`을 전진시키지 않는다
|
||
|
||
`ObjectOperationKernel`의 두 메서드가 새 시각 대신 기존 값을 쓴다(`152-...` §8.2d).
|
||
|
||
```java
|
||
markEffectSent → current.withPendingEffect(effect, current.updatedAt())
|
||
markResponseLost → current.withEffectCertainty(INDETERMINATE, current.updatedAt())
|
||
```
|
||
|
||
`confirmEffect`와 `advancePublication`·`terminate`는 `now`를 받는다. 즉 **의도를 적은 시각과 응답 유실을 기록한 시각이 durable 레코드에 남지 않는다** — revision은 올라가지만 `updatedAt`은 이전 단계의 값 그대로다.
|
||
|
||
기능상 문제는 없다. revision이 순서를 주고, lease 만료 판정은 `ObjectPublicationHandoffRecord`가 자기 `leaseExpiresAt`로 따로 한다. 다만 "언제 이 mutation을 보냈는가"는 응답 유실 조사에서 가장 먼저 묻는 값이고, 지금은 레코드에서 답할 수 없다. **P3/기록.**
|
||
|
||
## 23. Negative-space probes — sub-scope 03
|
||
|
||
- **8.1 reachability**: `kernel`·`codec` 패키지의 leaf 밖 참조 0(exit=1).
|
||
- **8.2 계약 ↔ 구현**: 다섯 전이표의 terminal 처리 전수 확인(§19). 보류 효과의 기록-호출-확인 순서를 호출 지점으로 추적(§20).
|
||
- **8.2b 조건부 형제**: `confirmEffect`·`advancePublication`은 `now`를 받고 `markEffectSent`·`markResponseLost`는 받지 않음(§22).
|
||
- **8.3 중복 mechanism**: 키 리터럴이 두 인코더에만 존재하고 route 격리가 파싱으로 강제됨(§21).
|
||
- **8.4 test 커버리지**: 상태 기계·kernel·codec 각각에 이름이 규칙을 말하는 test가 있고, property-based 검사와 golden vector가 포함됨.
|
||
|
||
## 24. Sub-scope 03 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P3/기록** | `markEffectSent`·`markResponseLost`가 `updatedAt`을 전진시키지 않아, 의도를 적은 시각과 응답 유실 시각이 durable 레코드에 남지 않는다 | 응답 유실 조사 |
|
||
|
||
## 25. Sub-scope 03 완료 조건
|
||
|
||
- denominator 39 / 39 FULL_READ (`152-...` OWNED FILES)
|
||
- §8.1~§8.4 네 종 probe 수행
|
||
- 실행 probe 불필요 — 판정 지점이 정적으로 결정 가능
|
||
- 소스 미변경
|
||
|
||
---
|
||
|
||
## 26. Sub-scope 04 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **40 / 40 FULL_READ**
|
||
> 범위: `s3/**` main 26 (3,581 LOC) + 전용 test 14 (약 2,300 LOC)
|
||
> 역할: AWS SDK v2를 이 패키지 안에 가두고, 클라이언트 정책·바인딩·조건부 제어 저장소·비동기 브리지를 **컴파일 시점에 전부 검증된 값**으로 만든다
|
||
|
||
manifest와 probe: `evidence/raw/153-objectstorage-s3-probes.txt`.
|
||
|
||
## 27. Confirmed — SDK 타입이 production에서 leaf를 벗어나지 않는다
|
||
|
||
`software.amazon.awssdk`를 참조하는 main 파일은 leaf 안에 19개이고, 그중 16개가 `s3/**`이다. 밖의 셋은 `ObjectStorageConfig`·`S3ObjectStorageAdapter`(둘 다 루트의 legacy 어댑터)와 `config/ObjectStorageCapabilityConfig`(legacy runtime이 S3 client를 만드는 지점)뿐 — 전부 README가 "deprecated compatibility only"로 선언한 경로다.
|
||
|
||
leaf 밖에서 SDK를 참조하는 파일은 **아키텍처 test 카탈로그와 빌드 파일뿐**이다(`153-...` §8.1) — `GraphQlReturnTypePolicy`, `WebForbiddenTypeCatalog`, `NotificationArchitectureTest`, 그리고 `application-core`의 `ObjectStorageArchitectureContractTest`. 즉 production 코드에서의 유출은 없고, 유출 금지가 다른 leaf의 금지 타입 목록으로도 지켜지고 있다.
|
||
|
||
## 28. Confirmed — 클라이언트 정책이 시간 예산의 정합성을 검사한다
|
||
|
||
`S3ClientPolicy.validate()`가 개별 값의 양수 여부를 넘어 **값들 사이의 관계**를 본다.
|
||
|
||
- per-attempt timeout < parent call timeout
|
||
- 다섯 전송 timeout(connection·TLS·acquire·read·write)이 모두 per-attempt timeout 이하
|
||
- `retryBaseDelay ≤ retryMaximumBackoff`
|
||
- **재시도 최악 예산이 부모 호출 예산 안에 드는지**:
|
||
`attemptTimeout × maxAttempts + maxBackoff × (maxAttempts − 1) ≤ apiCallTimeout`
|
||
그리고 그 곱셈은 `ArithmeticException`을 잡아 "retry budget overflows"로 거부한다.
|
||
|
||
즉 "재시도를 다 해도 부모 예산을 못 넘는다"가 설정 검증으로 강제된다 — 설정만 보고는 알 수 없는 종류의 모순이다.
|
||
|
||
엔드포인트도 좁다 — scheme은 http/https만, userinfo·query·fragment 금지, 그리고 **평문 AWS 엔드포인트 금지**(`http` + `*.amazonaws.com` → 거부). 정적 자격증명은 access/secret가 **둘 다 있거나 둘 다 없어야** 한다. `toString()`은 자격증명을 `[REDACTED]`로 대체한다.
|
||
|
||
`S3AsyncClientFactoryTest`가 이 규칙들을 이름으로 고정한다 — `rejectsMissingNonPositiveAndContradictoryTimeoutPoolAndRetryPolicy`, `rejectsUnsafeEndpointAndPartialStaticCredentialsWithoutExposingSecrets`.
|
||
|
||
## 29. Confirmed — provider 타입마다 신원 규칙이 다르고, 둘 다 좁다
|
||
|
||
`S3ProviderBinding.compile`이 `AWS_S3_GENERAL_PURPOSE`와 `MINIO_COMMUNITY_2024_01_16`을 다르게 검증한다.
|
||
|
||
| | AWS | MinIO |
|
||
|---|---|---|
|
||
| expectedOwner | **정확히 12자리 숫자 필수** | 설정하면 거부("cannot claim an AWS expected owner") |
|
||
| endpoint override | **금지**("not part of the qualified profile") | **필수**, canonical HTTPS만 |
|
||
| addressing | `virtual-hosted` 강제 | `virtual-hosted` 또는 `path-style` |
|
||
| credentials | `default-chain` 허용 | **환경변수 참조 필수**("MinIO requires explicit environment credential references") |
|
||
|
||
그리고 provider 타입과 무관하게 `autoCreateBucket || publicAcl`이면 거부한다 — "runtime provisioning and public ACLs are forbidden". 런타임이 버킷을 만들거나 공개 ACL을 붙이는 경로가 설정 단계에서 닫힌다.
|
||
|
||
## 30. Confirmed — mutation의 불확실성이 보존된다
|
||
|
||
`S3ProviderErrorMapper.map(failure, mutation)`의 첫 분기가 규칙이다. `AwsServiceException`이 **아니면**(즉 서버가 답하지 않았으면) 그리고 mutation이면 → `Failure.INDETERMINATE, authoritative=false`. 서버가 답한 경우에만 error code/status로 분류하고 `authoritative = normalized != INDETERMINATE`로 표시한다.
|
||
|
||
상태 코드 매핑의 기본값도 같은 방향이다 — 알 수 없는 status는 `mutation ? INDETERMINATE : UNKNOWN`. 즉 "실패했으니 재시도"라는 순진한 독법이 구조적으로 불가능하다.
|
||
|
||
`S3ConditionalObjectControlStore`도 같은 규칙을 제어 평면에 적용한다 — `INDETERMINATE`면 즉시 실패하지 않고 **정확한 GET으로 실제 상태를 읽어** 기대값과 비교한 뒤 결정한다. test `droppedCreateResponseResolvesByExactGetAndDigestComparison`과 `staleWriterConflictAndCorruptControlNeverBecomeAbsence`, `exact404IsTheOnlyAbsentRead`가 그 세 갈래를 고정한다. 마지막 이름이 특히 중요하다 — 부재로 해석되는 유일한 신호가 정확한 404다.
|
||
|
||
## 31. Confirmed — 논리 다이제스트와 provider 체크섬을 분리해 둘 다 대조한다
|
||
|
||
`S3ChecksumPolicy`가 사용자 메타데이터 키 `ca-logical-sha256`에 논리 SHA-256을 넣고, provider의 네이티브 `checksumSHA256`도 함께 요청한다. `requireMatchingEvidence`가 **둘 다** 기대값과 같은지 확인하고 하나라도 어긋나면 `CONTENT_MISMATCH`다.
|
||
|
||
`S3ObjectEvidenceMapper.fromHead`는 그 위에 세 가지를 더 요구한다 — content length가 바인딩 상한 안, `serverSideEncryption == AES256`, ETag 존재. 그리고 provider version id와 ETag는 `HeadEvidence`에 담겨 **adapter-private**로 남는다(`S3ConditionalRequestMapper`가 `ifMatch`/`versionId` 조건으로만 쓴다).
|
||
|
||
## 32. Confirmed — 비동기 브리지가 단일 구독·유계 버퍼·역압을 지킨다
|
||
|
||
`S3AsyncRequestBodyBridge`는 `subscribed.compareAndSet(false, true)`로 단일 구독을 강제하고, **downstream 수요를 기다린 뒤에야** 한 청크를 보유한다. 누적 길이가 선언 길이를 넘으면 즉시 `ObjectChunkWriteException`, 완료 시 관측 identity가 기대와 다르면 provider 실패다. 취소와 예산 만료를 매 청크 경계에서 확인한다.
|
||
|
||
`S3AsyncResponseBodyBridge`는 SDK 콜백을 논블로킹으로 두고 동기 소비자를 adapter 소유 worker에서 호출한다. `S3ConditionalObjectControlStore`의 `BoundedControlTransformer`도 누적 바이트가 64 KiB를 넘으면 중단한다.
|
||
|
||
test가 이 성질을 직접 잡는다 — `streamsOnceOffTheSubscriberThreadWithOneChunkOfProducerLead`, `cancellationAndDigestMismatchFailWithoutProducerReplay`, `truncatedAndOversizedSdkChunksFailClosed`.
|
||
|
||
## 33. Negative-space probes — sub-scope 04
|
||
|
||
- **8.1 reachability**: SDK 참조를 leaf 안팎으로 전수 조사(§27). 밖은 아키텍처 test 카탈로그뿐.
|
||
- **8.2 계약 ↔ 구현**: 정책의 시간 예산 상호 검증(§28), provider 타입별 신원 규칙(§29).
|
||
- **8.2b 조건부 형제**: AWS와 MinIO가 같은 필드에 대해 **반대 방향** 규칙을 갖고 둘 다 강제됨(§29).
|
||
- **8.3 중복 mechanism**: 논리 다이제스트와 provider 체크섬을 **의도적으로 이중화**하고 둘 다 대조(§31) — 중복이 아니라 교차 검증.
|
||
- **8.4 불확실성 보존**: 비권위적 실패의 분류와 정확한 GET을 통한 해소(§30). 단일 구독·유계 버퍼(§32).
|
||
|
||
## 34. Sub-scope 04 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| — | **없음.** SDK가 패키지에 갇혀 있고, 정책이 값이 아니라 값들 사이의 관계를 검증하며, mutation 불확실성이 분류에서 보존되고 정확한 GET으로만 해소된다 | — |
|
||
|
||
## 35. Sub-scope 04 완료 조건
|
||
|
||
- denominator 40 / 40 FULL_READ (`153-...` OWNED FILES) — main 3,581 LOC 전수 판독
|
||
- §8.1~§8.4 네 종 probe 수행
|
||
- 실행 probe 불필요 — 판정 지점이 정적으로 결정 가능하고 test가 각 성질을 이름으로 고정
|
||
- 소스 미변경
|
||
|
||
---
|
||
|
||
## 36. Sub-scope 05 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **25 / 25 FULL_READ**
|
||
> 범위: `direct/**` 13 + `multipart/**` 5 (main 18, 1,988 LOC) + 전용 test 7
|
||
> 역할: 브라우저가 provider와 직접 주고받는 bearer grant의 상태 기계와, part 원장·완료 검증
|
||
|
||
manifest와 probe: `evidence/raw/154-objectstorage-direct-multipart-probes.txt`.
|
||
|
||
## 37. 이 sub-scope의 설계 — 비밀은 durable하지 않고, 승인은 명시적으로 닫힌다
|
||
|
||
durable record가 **비밀을 담지 않는다**는 것이 출발점이다. `DirectTransferSessionRecord`의 한 줄 javadoc이 그것이다 — "Durable non-secret direct-transfer session state; bearer material is deliberately absent." 저장되는 것은 generation·제약 다이제스트·서명 시각·만료·credential revision·reference revision뿐이고, presigned URI와 서명 헤더는 **process-local 캐시**에만 남는다. 프로세스가 재시작하면 이미 발급된 grant는 재현되지 않고 `"issued direct grant bearer material is unavailable after process restart"`로 명시적으로 실패한다 — 조용히 새로 서명해서 두 번째 bearer를 만드는 대신이다.
|
||
|
||
상태 전이도 CAS로 순서가 고정된다. `SESSION_RESERVED → GRANT_PREPARED → GRANT_ISSUED → DATA_UPLOADED`이고, test 이름이 그 순서를 그대로 못박는다 — `preparedCasPrecedesSigningAndIssuedCasPrecedesReturningTheBearerGrant`. 서명 **전에** prepared가 durable해야 하고, bearer를 **반환하기 전에** issued가 durable해야 한다.
|
||
|
||
multipart 쪽에서 가장 흥미로운 것은 완료 시점의 **admission drain**이다. `DirectMultipartCompletionVerifier.requireAdmissionDrained`는 provider가 "controlled ingress가 비었다"고 권위 있게 말해 주지 않으면, `마지막 grant 만료 + 검증된 시계 오차 + 최대 in-flight 지평` 이 지나기 전에는 완료를 거부한다. 이미 발급된 part PUT이 아직 날아가고 있을 수 있기 때문이다. test 이름이 `completionHorizonRejectsWhileIssuedPartRequestsMayStillArrive`와 `lateGrantAdmissionRejectsAfterCompletionFence` — 양방향 모두 고정돼 있다.
|
||
|
||
part 원장은 provider의 LIST 순서에 의존하지 않는다. `MultipartPartLedger.ordered(n)`은 1..n을 **정확한 키로** 하나씩 읽고 하나라도 없으면 "ledger has a gap"이다. `record(...)`는 같은 증거면 `REPLAYED`, 다른 증거면 conflict — 재시도가 원장을 다시 쓰지 못한다. 그리고 `DirectMultipartCompletionVerifier.requireExactLedger`가 클라이언트가 제출한 receipt token 목록과 원장을 **위치까지 일치**시키고 논리 길이 합계를 기대값과 대조한다.
|
||
|
||
로그 유출 차단도 한 곳에 모여 있다 — `PresignedGrantRedactor.redact(URI)`는 인자를 아예 쓰지 않고 `[REDACTED_PRESIGNED_URI]`를 돌려주며, 헤더는 **키 이름만** 정렬해 노출한다. test가 `bearerUriAndSignedValuesAreNeverRendered`로 잡는다.
|
||
|
||
## 38. P2 — 직접 multipart의 마지막 part는 grant를 받을 수 없다
|
||
|
||
`S3ClientPolicy.requirePartSize(long partBytes, boolean finalPart)`는 **마지막이 아닌** part에만 5 MiB 하한을 적용한다(`MINIMUM_NON_FINAL_PART_BYTES = 5 * 1024 * 1024`). S3의 실제 규칙과 같다.
|
||
|
||
leaf 안에 이 정책의 호출 지점이 셋인데, 마지막 하나만 다르다.
|
||
|
||
| 호출 지점 | `finalPart` 인자 |
|
||
|---|---|
|
||
| `MultipartUploadPlan.partBytes:61` | `partNumber == partCount` — 계산된 값 |
|
||
| `S3ManagedMultipartProvider:80` | `finalPart` — 호출자가 넘긴 값 |
|
||
| `DirectMultipartCoordinator.createPartGrant:163` | **`false` 하드코딩** |
|
||
|
||
그리고 `PartUploadGrantRequest`에는 "이것이 마지막 part"라는 필드가 **없다**(operationKey · sessionId · partNumber · exactPartLength · expectedPartDigest · requestedTtl · budget · cancellation). 그러므로 coordinator가 그 사실을 알아낼 방법도 없다 — `maximumParts()`는 상한일 뿐 실제 part 수가 아니고, 실제 수는 `completeMultipart`에서 `request.partTokens().size()`로 비로소 정해진다.
|
||
|
||
**결과.** `DirectMultipartUploadPort.createPartGrant`는 5 MiB 미만의 part에 대해 항상 `"S3 multipart part size is outside the supported range"`로 거부한다. 즉
|
||
|
||
- 총 크기가 5 MiB 미만인 객체는 직접 multipart로 **전혀** 올릴 수 없다(part 1개 = 마지막 part).
|
||
- 고정 part 크기 + 나머지라는 통상적인 클라이언트 분할(예: 12 MiB를 5+5+2로)은 마지막 grant 요청에서 실패한다. 성공하려면 클라이언트가 **모든** part를 5 MiB 이상으로 재분할해야(5+7) 하는데, 그 요구는 port 계약 어디에도 적혀 있지 않다.
|
||
|
||
같은 파일이 `MultipartUploadPlan`에서 마지막 part 구분을 정확히 계산하고 있으므로 규칙을 모르는 상태가 아니다. 직접 경로에서만 정보가 요청 타입에 실려 오지 않아 보수적인 상수로 대체된 것이다. **판정: P2.** 수정은 `PartUploadGrantRequest`에 마지막 part 표시를 추가하고 그 값을 넘기는 것, 또는 start 요청에 정확한 part 수를 고정해 `partNumber == exactPartCount`로 유도하는 것이다.
|
||
|
||
test는 이 경계를 건드리지 않는다 — `DirectMultipartCoordinatorTest`의 세 케이스는 모두 5 MiB 이상 part만 쓴다. 반대로 `S3SdkApiCharacterizationTest:93-94`는 `requirePartSize(MIN, false)`와 `requirePartSize(MIN - 1, true)` 둘 다 통과함을 확인한다 — 정책 자체는 옳고, 직접 경로의 호출만 어긋나 있다는 것을 이 test가 오히려 증명한다.
|
||
|
||
## 39. P2 — 서명된 grant의 endpoint 검증이 upload 경로에만 있다
|
||
|
||
`DirectTransferPolicy`의 host allowlist는 이 sub-scope의 유일한 endpoint 방어다. 그런데 실제 provider가 서명해 돌려준 URI를 그 allowlist에 대조하는 호출은 **한 곳뿐**이다.
|
||
|
||
```java
|
||
// createUploadGrant
|
||
DirectGrantProvider.DirectGrantMaterial material = provider.signUpload(current.session());
|
||
policy.validateSignedGrant(material.requestUri(), current.session().expiresAt()); // ← 검증
|
||
|
||
// createDownloadGrant
|
||
DirectGrantProvider.DirectGrantMaterial material = provider.signDownload(prepared.session(), published);
|
||
// ← 대응하는 validateSignedGrant 없음
|
||
|
||
// createPartGrant (DirectMultipartCoordinator)
|
||
DirectGrantProvider.DirectGrantMaterial material = provider.signPart(session, grant.record());
|
||
// ← 대응하는 validateSignedGrant 없음. 이 coordinator는 DirectTransferPolicy를 아예 갖고 있지 않다
|
||
```
|
||
|
||
`planGrant(...)`도 `validateEndpoint`를 부르지만, 두 호출자 모두 `policy.planningEndpoint()`를 넘긴다. 그 메서드는 `"https://" + allowedHosts` 중 사전순 첫 host를 조립한 값이므로 **정의상 항상 통과**한다. 결국 allowlist가 실제 URI에 대해 힘을 갖는 지점은 upload 경로 한 곳뿐이고, 브라우저에 그대로 건네지는 download bearer와 multipart part bearer는 host·scheme·userinfo 검사를 통과하지 않는다.
|
||
|
||
덧붙여 `validateSignedGrant(URI, Instant expectedExpiry)`는 `expectedExpiry`를 **null 검사만** 하고 쓰지 않는다. 서명 재료(`DirectGrantMaterial.expiresAt()`)와 세션이 선언한 만료가 어긋나도 걸리지 않으며, 호출자는 세션 쪽 만료를 응답에 실어 보낸다. 즉 "서명된 grant를 검증한다"는 이름이 실제로는 host 검사 하나다.
|
||
|
||
**판정: P2.** 현재 노출은 없다(§40: 미배선). 그러나 이 세 경로는 모두 같은 종류의 값 — 브라우저에 넘길 bearer URI — 를 다루는 형제이고, 방어가 하나에만 있다. 수정은 `validateSignedGrant`를 세 경로 모두에서 호출하고(그러려면 `DirectMultipartCoordinator`도 policy를 받아야 한다), `expectedExpiry`를 실제로 `material.expiresAt()`과 대조하는 것이다.
|
||
|
||
## 40. Confirmed — 직접 전송 subsystem은 미배선이고, README가 그 사실을 정확히 적는다
|
||
|
||
`DirectTransferCoordinator`·`DirectMultipartCoordinator`·`DirectTransferPolicy`를 이름으로 부르는 곳은 `direct/**` 패키지 **밖에 하나도 없다**(`154-...` §8.1b, 매치 0). `DirectObjectUploadPort`·`DirectObjectDownloadGrantPort`·`DirectMultipartUploadPort` 세 application port는 production 구현이 등록되지 않는다.
|
||
|
||
README가 이것을 두 문장으로 선언한다 — "Direct transfer, multipart, quarantine, retention, and production reconciliation cards remain R0."와 "no direct-grant port is registered." 이 leaf에서 반복해 확인한 정직함이고, 미배선 자체는 결함이 아니다.
|
||
|
||
## 41. P2 — 그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다
|
||
|
||
미배선이 선언돼 있는데도, **설정은 그 capability를 계속 받아들이고 런타임은 자격증명을 쥔 presigner를 실제로 만든다.**
|
||
|
||
1. `ObjectCapabilityRequirement.DIRECT_UPLOAD` / `DIRECT_MULTIPART`는 destination 요구사항으로 선언 가능하고, `ObjectStorageBindingCompiler:206-207`이 그것을 provider capability로 번역한다.
|
||
2. `S3ProviderBinding.compileProfiles`는 **MinIO에 대해서만** 이 두 capability 주장을 거부한다("the exact MinIO release cannot claim native conditional managed mutation support"). **AWS profile에는 대응하는 거부가 없다.**
|
||
3. 그러면 `S3ObjectStorageProviderContribution:112-150`이 `directUploadEnabled || directMultipartEnabled`일 때 `presignerFactory.apply(clientPolicy)`로 `S3Presigner`를 할당하고 `S3DirectTransferProvider` / `S3DirectMultipartProvider`를 만들어 `SelectedObjectStorageProviderFactory`에 넣는다.
|
||
4. 그리고 그 둘을 factory에서 **꺼내 가는 코드가 없다**(`154-...` §8.1c, 매치 0).
|
||
|
||
즉 AWS binding에서 `direct-upload`를 요구하는 배포는 — startup을 통과하고, presigner를 할당하고, 두 provider를 조립하고, **직접 전송 port는 여전히 하나도 얻지 못한다.** README가 말한 R0는 사실이지만, 그 사실을 강제하는 것은 문서뿐이다.
|
||
|
||
이 leaf 안에 정반대의 사례가 있어서 대비가 분명하다 — `filesystem-local-dev`는 `prod`/`production` 프로파일에서 **compile 시점에 거부**되고, MinIO의 capability 과대 주장도 compile 시점에 거부된다. 같은 파일이 같은 종류의 "이 조합은 자격이 없다"를 AWS + DIRECT_* 에 대해서만 하지 않는다.
|
||
|
||
**판정: P2.** 데이터 위험은 없다 — 없는 port는 호출될 수 없다. 위험은 (a) 운영자가 켰다고 믿는 기능이 없다는 것과 (b) 아무도 쓰지 않는 서명 자격증명 핸들이 프로세스 수명 동안 살아 있다는 것이다. 수정은 셋 중 하나다: coordinator를 조건부로 조립하거나, R0인 동안 `DIRECT_UPLOAD`/`DIRECT_MULTIPART` 요구를 compile 단계에서 provider 종류와 무관하게 거부하거나, capability가 켜져도 presigner를 만들지 않도록 조립을 뒤로 미루거나.
|
||
|
||
## 42. P3/기록 — 선언만 되고 강제되지 않는 정책 항목
|
||
|
||
- **`DirectTransferPolicy.maximumOutstandingGenerations`.** 생성자가 1..16 범위를 검사하지만 읽는 곳이 없다(`154-...` §8.4, 매치 2 = 선언과 검증뿐). `DirectTransferSessionRecord.grantGeneration`도 항상 1이다 — `planGrant`가 `new DirectGrantGeneration(1, ...)`로 고정하고 generation을 올리는 경로가 없다. "outstanding generation을 몇 개까지 허용한다"는 정책이 표현돼 있으나 generation 자체가 재발급되지 않으므로 지금은 의미를 갖지 않는다.
|
||
- **`DirectTransferCorsPolicy`.** "infrastructure must apply before direct admission"이라고 적혀 있으나 production 소비자가 없고, 이 값을 어디에 어떻게 반영해야 하는지 알려 주는 배선도 없다. 유일한 참조는 `DirectTransferCorsContractTest`다. 브라우저 계약을 코드로 고정해 둔 것 자체는 유용하나, 적용 주체가 코드 밖(인프라)이라는 사실은 README에 없다.
|
||
|
||
## 43. Negative-space probes — sub-scope 05
|
||
|
||
- **8.1 reachability**: coordinator·policy 모두 패키지 밖 참조 0(§40). README가 그 상태를 선언(§40). 그러나 capability는 설정에서 살아 있고 presigner는 실제로 할당된다(§41).
|
||
- **8.2 조건부 형제 ①**: `requirePartSize`의 세 호출 지점 중 하나만 `finalPart`를 하드코딩(§38).
|
||
- **8.2b 조건부 형제 ②**: `validateSignedGrant`가 upload 경로에만 있고 download·part 경로에는 없다(§39).
|
||
- **8.2c 조건부 형제 ③**: `S3ProviderBinding`이 DIRECT_* 주장을 MinIO에서만 거부(§41).
|
||
- **8.3 중복 mechanism**: 두 coordinator가 각자 독립된 process-local `ConcurrentHashMap` bearer 캐시를 갖는다. 둘 다 경계가 없고(무한 증가 가능) 만료된 항목을 청소하지 않으며, 제거는 성공적인 완료/확인 경로에서만 일어난다. 발급 후 완료되지 않은 세션의 재료는 프로세스 수명 동안 남는다 — 미배선이므로 지금 노출은 없고, 두 곳이 같은 방식으로 같은 성질을 갖는다는 점에서 우연이 아니라 공통 설계다. **P3/기록.**
|
||
- **8.4 문서/선언 drift**: 강제되지 않는 정책 항목 2종(§42).
|
||
|
||
## 44. Sub-scope 05 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | `DirectMultipartCoordinator:163`이 `requirePartSize(..., false)`를 하드코딩하고 `PartUploadGrantRequest`에 마지막 part 표시가 없어, 5 MiB 미만 part에 grant를 발급할 수 없다 — 총 5 MiB 미만 객체는 직접 multipart 불가 | 현재 미배선; 배선 시 모든 통상적 클라이언트 분할 |
|
||
| **P2** | 서명된 bearer URI의 host allowlist 검증이 upload 경로에만 있고 download·multipart part 경로에는 없다. `validateSignedGrant`의 `expectedExpiry`는 검사되지 않는다 | 현재 미배선; 배선 시 브라우저로 나가는 두 bearer |
|
||
| **P2** | AWS binding에서 `DIRECT_UPLOAD`/`DIRECT_MULTIPART` 요구가 compile을 통과해 presigner를 할당하지만, 그것을 쓰는 port가 없다. MinIO에는 같은 주장을 막는 검사가 있다 | AWS provider + direct capability 요구 배포 |
|
||
| **P3/기록** | `maximumOutstandingGenerations`가 검증만 되고 읽히지 않으며 grant generation은 항상 1이다 | 정책/조립 |
|
||
| **P3/기록** | `DirectTransferCorsPolicy`는 production 소비자 0이고, 적용 주체가 인프라라는 사실이 README에 없다 | 문서 |
|
||
| **P3/기록** | 두 coordinator의 process-local bearer 캐시가 무경계·무만료이며, 미완료 세션의 재료는 프로세스 수명 동안 남는다 | 배선 시 |
|
||
|
||
## 45. Sub-scope 05 완료 조건
|
||
|
||
- denominator 25 / 25 FULL_READ (`154-...` OWNED FILES)
|
||
- §8.1~§8.4 네 종 probe 수행, 조건부 형제 비교는 3건 독립 수행
|
||
- 세 finding 모두 정적으로 결정 가능(호출 인자 상수 · 호출 부재 · 조립 경로)하여 실행 probe 불필요
|
||
- 소스 미변경
|
||
|
||
---
|
||
|
||
## 46. Sub-scope 06 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **43 / 43 FULL_READ**
|
||
> 범위: `filesystem/**` 6 + `maintenance/**` 8 + `readiness/**` 8 + `provider/**` 4 + 루트 4 (main 30, 2,420 LOC) + test 12 + resource 1
|
||
> 역할: local-dev provider, legacy 채택(adoption) 경로, readiness 증거 타입, provider-neutral 계약, 그리고 deprecated 루트 어댑터
|
||
|
||
manifest와 probe: `evidence/raw/155-objectstorage-platform-readiness-probes.txt`.
|
||
|
||
## 47. §6의 forward reference 해소 — readiness 레지스트리는 실재하고 test가 강제한다
|
||
|
||
sub-scope 01에서 `build.gradle:46`이 `docs/registries/object-storage-readiness.yaml`을 `test` 태스크의 입력으로 선언하는데 leaf 소스에 그 파일명을 부르는 곳이 없다는 점을 미해결로 남겼다. 해소한다.
|
||
|
||
파일은 **저장소 루트에 실재한다**(`docs/registries/object-storage-readiness.yaml`). build.gradle은 파일명을 시스템 프로퍼티 `objectstorage.readiness.registry`로 전달하고, leaf test `ObjectStorageReadinessRegistryTest:131`이 그 프로퍼티를 읽어 YAML을 파싱한다. 그래서 소스 grep에 파일명이 잡히지 않았다.
|
||
|
||
그 test가 하는 일이 이 sub-scope에서 가장 강한 governance 장치다.
|
||
|
||
- 레지스트리 키가 `schema_version`과 `claims` **둘뿐**임을 확인하고,
|
||
- claim의 card id 집합이 `ObjectStorageCapabilityCard.cardIds()`(고정된 9종)와 **정확히 일치**함을 확인하고,
|
||
- 각 claim에 `validate(knownProviders, availableTasks)`를 돌려 provider·필수 태스크·한계 문구·R2/R3 만료를 검사하고,
|
||
- **R1인 카드가 정확히 두 개**(`managed-upload-single`, `managed-download`)임을 확인하고,
|
||
- direct 3종 · quarantine · retention · reconciliation **여섯 카드가 모두 R0**임을 확인한다.
|
||
|
||
README가 산문으로 적은 "Direct transfer, multipart, quarantine, retention, and production reconciliation cards remain R0"가 여기서 기계 검사가 된다. YAML 파일 자신도 헤더 주석에 소유 test 두 개(`ContractRegistrySchemaGovernanceTest` = 저장소 소유, `ObjectStorageReadinessRegistryTest` = 의미 소유)를 적어 둔다.
|
||
|
||
`ObjectStorageCapabilityEvidence.validate`의 개별 규칙도 좁다 — `filesystem-local-dev`는 R2/R3를 주장할 수 없고, `requiredTasks`는 비어 있을 수 없으며 전부 실재하는 태스크여야 하고, `limitations`도 비어 있을 수 없다. "한계를 적지 않은 readiness 주장"이 표현 불가능하다.
|
||
|
||
## 48. §41 보강 — 레지스트리는 문서 주장을 얼어붙히지만 런타임 설정 경로는 덮지 않는다
|
||
|
||
§41에서 "R0 경계가 문서에만 있다"고 적었다. §47을 반영해 정확히 다시 말한다.
|
||
|
||
R0 경계는 **문서 주장에 대해서는** 기계 검사된다(§47). 그러나 그 검사의 대상은 `docs/registries/object-storage-readiness.yaml`이고, `KNOWN_PROVIDERS`는 `filesystem-local-dev` 하나다. 운영자가 `app.object-storage` 설정에 AWS provider용 qualification profile을 쓰면서 `DIRECT_UPLOAD` capability를 주장하는 경로는 이 레지스트리를 **거치지 않는다**. `S3ProviderBinding.compileProfiles`가 그 주장을 MinIO에 대해서만 거부하므로, AWS + DIRECT_* 조합은 여전히 compile을 통과하고 presigner를 할당한다(§41).
|
||
|
||
따라서 §41의 판정은 유지되고 오히려 선명해진다 — 이 저장소에는 "이 카드는 R0"를 강제하는 장치가 이미 있는데, 런타임 설정 경로가 그 장치의 사정권 밖에 있다.
|
||
|
||
## 49. P2 — APPLY를 켜는 설정은 있고, 승인을 검증하는 bean은 없다
|
||
|
||
legacy 채택은 이 leaf에서 가장 권한이 센 동작이다. 원시 locator로 legacy 네임스페이스를 읽어 관리 네임스페이스에 **발행**한다. 그래서 설계가 detached 2인 승인을 요구한다 — `Ed25519LegacyAdoptionApprovalVerifier`가 서로 다른 두 승인자의 Ed25519 서명을 검증하고, 문서의 destination/epoch/operationId/manifest·네임스페이스 다이제스트가 요청과 일치하는지, 유효창이 최대 7일 안인지까지 본다. 구현은 촘촘하다 — `LegacyAdoptionApprovalCodec.decode`는 길이 프레이밍 이진 코덱이고 **decode 후 재인코딩해 원본 바이트와 같아야만** 통과한다(비정규 인코딩 거부).
|
||
|
||
그런데 조립이 비대칭이다.
|
||
|
||
| | 켜는 방법 | bean |
|
||
|---|---|---|
|
||
| APPLY 실행 경로 | `app.object-storage.legacy-adoption.enabled=true`, `mode=APPLY` | `ObjectStorageLegacyMigrationConfig:30`이 `LegacyObjectAdoptionPort`를 만든다 |
|
||
| 승인 검증 | **없음** | **없음** — `Ed25519LegacyAdoptionApprovalVerifier`를 `main`에서 생성하는 코드가 저장소 전체에 0(`155-...` §8.1) |
|
||
|
||
`LegacyObjectAdoptionSettings`는 `enabled`·`mode`·`reportPath`·`reviewedManifestPath`·`reviewedManifestSha256`·`batchSize`·`operationTimeout`을 노출하고, APPLY일 때 reviewed manifest 경로와 sha256을 요구한다. 그러나 **신뢰 승인자 키, 키 id, 승인 유효기간에 해당하는 설정 항목이 하나도 없다.** 검증기를 켤 방법이 설정에 없다.
|
||
|
||
adapter 쪽 `LegacyObjectAdoptionService.requireApproval`은 `request.approval()` 객체의 **필드 동등성만** 검사한다 — operationKey · manifest · 네임스페이스 다이제스트 · destination · 유효창. 서명은 보지 않는다. 그리고 `LegacyObjectAdoptionApproval`은 application-core의 공개 생성자를 가진 값 타입이다(검증기 자신이 `new`로 만든다).
|
||
|
||
의도된 조립은 sample-portfolio가 보여 준다 — `AdoptLegacyPosterImageUseCase.authorizeApply`가 **호출자가 준 approval을 버리고** `approvals.verify(document, request)`의 결과로 요청을 다시 만든다. 즉 "application에서 검증하고 adapter에서 적용한다"가 설계다. 그 use case 역시 어디에도 배선돼 있지 않다.
|
||
|
||
**판정: P2.** 지금 우회가 열려 있는 것은 아니다 — 검증기 bean이 없으면 `AdoptLegacyPosterImageUseCase`를 쓰는 fork의 컨텍스트는 시작에 실패하고, 그것은 조용하지 않은 실패다. 위험은 다른 쪽이다: **설정 한 줄로 켜지는 절반과 손으로 배선해야 하는 절반**이 있고, 켜지는 쪽이 권한이 센 쪽이다. `enabled=true, mode=APPLY`를 켠 fork가 use case를 쓰지 않고 port를 직접 부르면 adapter의 검사는 필드 동등성뿐이다. 수정은 `ObjectStorageLegacyMigrationConfig`가 승인자 키를 설정에서 읽어 `Ed25519LegacyAdoptionApprovalVerifier` bean도 함께 만들되, APPLY 모드에서 그 bean이 없으면 startup을 거부하는 것이다.
|
||
|
||
`LegacyObjectAdoptionServiceTest`에 test가 **하나뿐**이고 그것이 `reportOnlyInspectsExactEvidenceAndPerformsNoMutation`이라는 점도 같은 방향을 가리킨다 — APPLY 경로에는 서비스 수준 test가 없다. 검증기 자체는 `LegacyAdoptionApprovalVerifierTest`가 두 케이스(`verifiesCanonicalExactBindingWithTwoDistinctTrustedApprovers`, `rejectsDuplicateApproverAndAnyBindingTamper`)로 덮는다.
|
||
|
||
## 50. P3 — nonce replay 경계가 결과를 읽고 버린다
|
||
|
||
```java
|
||
LegacyAdoptionApprovalReplayStore.ClaimResult claim = replayStore.claim(replay);
|
||
var published = publications.publish(request.publicationRequest(), inspected.producer());
|
||
if (claim != ClaimResult.TERMINAL_REPLAY) { replayStore.markTerminal(...); }
|
||
```
|
||
|
||
`ClaimResult`는 `CLAIMED` / `EXACT_REPLAY` / `TERMINAL_REPLAY` 셋인데, 어느 값이든 **발행은 그대로 진행된다.** claim 결과가 바꾸는 것은 terminal 기록을 쓸지 여부뿐이다. 인터페이스 javadoc은 자신을 "Durable compare-and-set nonce replay boundary"라고 부르지만, 경계로서 무엇도 막지 않는다.
|
||
|
||
실제 피해는 제한적이다 — 발행이 operation-key 기반 멱등이므로 이미 소진된 nonce로 다시 들어와도 결과는 `REPLAYED`이고 두 번째 객체가 생기지 않는다. 그래서 P3다. 그러나 (a) 이름이 약속하는 것과 다르고, (b) `markTerminal`에 넘기는 `expectedRevision`이 항상 `replay.revision()` = 0이라 CAS 인자로서도 고정값이며, (c) 승인 문서의 nonce가 "한 번만 쓰인다"는 성질은 이 코드로는 보장되지 않는다. 수정은 `TERMINAL_REPLAY`에서 발행 전에 거부하는 것이다.
|
||
|
||
## 51. Confirmed — local-dev provider의 경로 방어와 publication
|
||
|
||
`LocalObjectPathGuard`는 이 저장소에서 반복해 본 강한 형태다 — root 정규화 + `startsWith` 봉쇄 + root 자신 거부에 더해, 부모 경로를 **root부터 한 세그먼트씩 내려가며** 심링크와 비디렉터리를 거부하고(`createParentsWithoutLinks` / `rejectExistingLinks`), 대상 자신도 심링크면 거부한다. control key는 `control/v1/` 접두사 + `[a-z0-9._/-]+` + `//`·`/./`·`/../` 금지 + 세그먼트별 재검사다. 그리고 control 레코드는 물리 파일명에 `.record`를 붙인다 — 객체 저장소가 허용하는 `reference`와 `reference/lifecycle` 쌍이 파일시스템에서 파일/디렉터리 충돌을 일으키지 않도록. 논리 키는 그대로 유지된다.
|
||
|
||
발행은 **배타적 하드링크**다. `LocalDevObjectDataStore.create`가 임시 파일에 쓰고 `channel.force(true)` 후 `Files.createLink(target, temporary)`를 하며, `FileAlreadyExistsException`을 `CONFLICT`로, `UnsupportedOperationException`을 "local filesystem cannot prove immutable create"로 번역한다 — 하드링크를 지원하지 않는 파일시스템에서 조용히 약한 방식으로 내려가지 않는다. 앞선 `Files.exists(NOFOLLOW)` 검사는 빠른 경로일 뿐이고 배타성은 `createLink`가 준다. POSIX면 소유자 읽기 전용 권한을 씌운다.
|
||
|
||
test가 이것들을 이름으로 잡는다 — `traversalAbsoluteUnicodePercentAndSymlinkEscapesAreRejected`, `exclusiveCreateRaceHasOneWinner`, `injectedDiskFailureLeavesNoFinalOrTemporaryData`, `restartInspectsCommittedDataWithoutReplayingProducer`, `corruptControlRecordRemainsPresentAndNeverAppearsAbsent`, `createsRestrictivePermissionsWherePosixIsSupported`. 마지막에서 두 번째가 특히 이 leaf의 규칙이다 — 손상된 control 레코드는 **부재로 보이지 않는다**.
|
||
|
||
`LocalDevObjectControlStore`도 자신의 한계를 클래스 javadoc에 적는다 — "Single-process local create/CAS store; it deliberately does not claim multi-node linearizability." 그리고 descriptor의 capability 표가 `MULTI_NODE_LINEARIZABLE_CAS`와 `POWER_LOSS_DURABILITY`를 `UNSUPPORTED`로 명시한다. `ObjectStorageProviderContract:184`의 `unsupportedOptionalCapabilitiesAreDeclaredRatherThanSkipped`가 그 선언을 강제한다 — 지원하지 않는 것을 test에서 건너뛰는 대신 선언하게 만든다.
|
||
|
||
## 52. P3/기록 — 같은 capability 표가 두 벌 있다
|
||
|
||
`filesystem-local-dev`의 capability 표가 두 곳에 **독립적으로 하드코딩**돼 있다.
|
||
|
||
- `FilesystemLocalDevProviderContribution.capabilitySupport()` — `describe(settings)`가 쓰는 것. sub-scope 01에서 확인했듯 이것이 **compile 시점 권위**다.
|
||
- `LocalDevObjectStorageProvider.capabilitySupport()` — 런타임 `descriptor()`가 쓰는 것.
|
||
|
||
현재 둘은 같다(각각 `Support.SUPPORTED` 6개, 같은 여섯 capability). 그러나 공유하는 상수도, 한쪽이 다른 쪽을 부르는 구조도 없다. 한쪽만 고치면 compile이 허용한 capability와 런타임이 주장하는 capability가 갈라지고, 그 불일치를 잡는 test는 없다. **P3/기록.** 수정은 표를 한 곳에 두고 양쪽이 그것을 부르는 것이다.
|
||
|
||
## 53. P3/기록 — deprecated 루트 어댑터에는 형제에게 있는 방어가 없다
|
||
|
||
루트의 네 파일(`ObjectStorageSettings` · `ObjectStorageConfig` · `FilesystemObjectStorageAdapter` · `S3ObjectStorageAdapter`)은 README가 "deprecated compatibility only... never back the new semantic ports"로 선언한 경로이고, `LegacyObjectStorageActivationGuard`가 명시적 opt-in을 요구하며 canonical 네임스페이스와의 혼용을 예외로 막는다. 그 선까지는 규율이 있다.
|
||
|
||
그 아래에서는 canonical 경로에 있는 방어가 하나씩 없다.
|
||
|
||
| | canonical | legacy 루트 |
|
||
|---|---|---|
|
||
| `autoCreateBucket` | `S3ProviderBinding:268`이 `autoCreateBucket \|\| publicAcl`을 **거부** | `ObjectStorageSettings:46` 기본값 **`true`**, startup에서 실제로 버킷 생성 |
|
||
| 평문 엔드포인트 | AWS 도메인 + `http` **거부** | 기본값 `http://localhost:9000` |
|
||
| 심링크 | `LocalObjectPathGuard`가 root·부모·대상 전부 거부 | `FilesystemObjectStorageAdapter.resolve`는 `normalize()` + `startsWith`만 — 심링크 검사 **0** |
|
||
| production 프로파일 | `filesystem-local-dev`를 `prod`/`production`에서 거부 | `LegacyObjectStorageActivationGuard`에 프로파일 검사 **없음** |
|
||
|
||
`LegacyObjectStorageConfigTest.autoCreateBucketTrueProvisionsTheBucketWhileTheS3BeanIsCreated`가 그 기본값을 의도된 동작으로 고정하고 있으므로 우발적 잔재는 아니다. 그래서 P3/기록이다 — deprecated 경로가 자신의 과거 의미를 보존하는 것은 정당하고, 위험한 것은 그 경로가 production 프로파일에서 아무 제지 없이 켜진다는 점 하나다. `FilesystemObjectStorageAdapter`의 심링크 미검사는 baseDir 안에 심링크를 심을 수 있는 로컬 접근을 전제하므로 노출이 좁다.
|
||
|
||
## 54. Negative-space probes — sub-scope 06
|
||
|
||
- **8.1 reachability**: `maintenance/**`는 설정 flag로 배선된다(§49). `readiness/**`의 세 타입은 leaf test가 소비한다(§47) — 앞선 grep이 test 패키지까지 제외해 0으로 보였던 것을 바로잡았다.
|
||
- **8.1b forward reference 해소**: build.gradle → 시스템 프로퍼티 → leaf test → 루트 YAML(§47).
|
||
- **8.2 조건부 형제 ①**: 켜지는 절반과 손배선 절반의 비대칭(§49).
|
||
- **8.2b 조건부 형제 ②**: legacy 루트 vs canonical의 네 가지 방어 차이(§53).
|
||
- **8.3 중복 mechanism**: capability 표 두 벌(§52).
|
||
- **8.4 선언 대비 강제**: nonce replay 경계가 결과를 쓰지 않음(§50). readiness 주장의 한계 문구 강제(§47).
|
||
|
||
## 55. Sub-scope 06 findings backlog
|
||
|
||
| 우선순위 | finding | reachability |
|
||
|---|---|---|
|
||
| **P2** | `legacy-adoption.enabled=true, mode=APPLY`는 설정으로 켜지는데 `Ed25519LegacyAdoptionApprovalVerifier` bean은 저장소 어디에도 없고 승인자 키 설정 항목도 없다. adapter의 승인 검사는 필드 동등성뿐 | APPLY를 켜고 port를 직접 부르는 fork |
|
||
| **P3** | `LegacyAdoptionApprovalReplayStore.claim` 결과가 발행을 막지 않는다 — `TERMINAL_REPLAY`도 그대로 발행 | APPLY 경로 |
|
||
| **P3/기록** | `filesystem-local-dev` capability 표가 contribution과 provider에 두 벌로 하드코딩 | 표가 갈라질 때 |
|
||
| **P3/기록** | deprecated 루트 경로에 production 프로파일 검사가 없고 `autoCreateBucket` 기본값이 `true`, 평문 엔드포인트가 기본값, 심링크 검사 없음 | legacy opt-in 배포 |
|
||
|
||
## 56. Sub-scope 06 완료 조건
|
||
|
||
- denominator 43 / 43 FULL_READ (`155-...` OWNED FILES: main 30 / test+resource 13)
|
||
- §8.1~§8.4 probe 수행, 조건부 형제 비교 2건, 중복 mechanism 1건
|
||
- §6의 readiness 레지스트리 forward reference 해소(§47) 및 그에 따른 §41 보강(§48)
|
||
- 소스 미변경
|
||
|
||
---
|
||
|
||
## 57. Sub-scope 07 범위와 denominator
|
||
|
||
> 내부 상태: COMPLETE — **6 / 6 FULL_READ**
|
||
> 범위: 전용 qualification source set 3종 6파일 (546 LOC)
|
||
> 역할: 실제 provider를 상대로 "무엇이 되고 무엇이 안 되는지"를 증거로 고정하는 lane
|
||
|
||
manifest와 probe: `evidence/raw/156-objectstorage-qualification-lanes-probes.txt`.
|
||
|
||
| source set | 파일 | 성격 |
|
||
|---|---|---|
|
||
| `objectStorageMinioContractTest` | `MinioManagedObjectContractTest` 291 · `MinioDirectTransferContractTest` 25 | 전자는 실제 MinIO 컨테이너 기동 |
|
||
| `objectStorageMinioFaultTest` | `MinioManagedObjectFaultTest` 147 · `MinioDirectTransferFaultTest` 32 | 전자는 MinIO + Toxiproxy |
|
||
| `objectStorageAwsQualificationTest` | `AwsS3ManagedCommonSubsetQualificationTest` 27 · `AwsS3DirectTransferQualificationTest` 24 | 환경변수 권한 검사만 |
|
||
|
||
세 lane 모두 `registerStrictQualificationTest`로 등록되고 `requiredClasses`로 클래스 이름이 고정된다 — 클래스를 지우거나 이름을 바꾸면 lane 등록이 실패한다. 설명 문구도 "non-skipping"을 명시한다. 즉 "Docker 없으면 조용히 skip"이 이 lane들에는 없다.
|
||
|
||
## 58. Confirmed — MinIO의 조건부 create가 **작동하지 않는다**는 것을 실측으로 증명한다
|
||
|
||
`MinioManagedObjectContractTest`가 digest로 고정된 MinIO 이미지를 띄우고 다음을 순서대로 확인한다.
|
||
|
||
1. `PutObject` + `If-None-Match: *` + SHA-256 체크섬 + AES256 → 성공.
|
||
2. **같은 키에 다시** `PutObject` + `If-None-Match: *` → **또 성공하고 내용이 덮인다.** 변수 이름이 결론이다: `overwrittenDespiteCreateOnlyCondition`.
|
||
3. `HeadObject`로 길이·`ca-logical-sha256` 메타데이터·SSE가 보존됨을 확인, `GetObject` + `If-Match` + `Range`로 부분 읽기가 정확함을 확인.
|
||
4. control 키에 대해서도 같은 일이 일어남을 확인(`overwrittenControlCreate`).
|
||
5. 그러나 **stale `If-Match`는 HTTP 412**로 정확히 거부됨을 확인.
|
||
6. 두 번째 test에서 low-level multipart가 동작하되 `CompleteMultipartUpload` + `If-None-Match: *`가 **기존 객체를 덮는다**는 것을 확인.
|
||
|
||
이 여섯 줄이 sub-scope 04에서 본 `S3ProviderBinding`의 MinIO 거부 규칙 — "the exact MinIO release cannot claim native conditional managed mutation support" — 의 근거다. 즉 코드의 거부가 의견이 아니라 이 lane의 실측 결과다.
|
||
|
||
결과는 `src/test/resources/object-storage/minio-provider-evidence.json`에 고정돼 있고, 그 한계 문구가 그대로 사람이 읽을 문장이다 — "PutObject If-None-Match was accepted and overwrote an existing object", "CompleteMultipartUpload accepted If-None-Match despite a pre-existing target and overwrote it", 그리고 범위를 좁히는 세 줄("local single-node container evidence only", "not AWS evidence", "not production TLS or deployment-topology evidence").
|
||
|
||
`MinioManagedObjectFaultTest`는 Toxiproxy로 양방향 대역폭을 0으로 만들고, 실패가 **5초 안에 유계로** 발생하는지와 복구 후 같은 객체가 그대로인지를 본다 — `connectionCutProducesABoundedFailureAndRecoveryWithoutMutationReplay`. 컨테이너 두 개가 모두 try-with-resources 안에 있고 `@SuppressWarnings("resource")`에 그 이유가 주석으로 적혀 있다.
|
||
|
||
`MinioDirectTransfer*` 두 파일이 provider를 부르지 않는 것도 근거가 있다 — javadoc이 "No bearer or multipart mutation is attempted because qualification proved that create-only PUT and create-only multipart completion are ignored by this provider identity." 즉 이미 증명된 결과를 근거로 mutation 행렬에 들어가지 않겠다는 선언이고, 두 파일은 그 결정을 evidence 파일에 대한 assertion으로 얼려 둔다.
|
||
|
||
## 59. P3/기록 — AWS lane은 환경변수만 검사하고 통과한다
|
||
|
||
`AwsS3ManagedCommonSubsetQualificationTest`와 `AwsS3DirectTransferQualificationTest`의 본문은 전부 다음 형태다.
|
||
|
||
```java
|
||
Map<String, String> environment = System.getenv();
|
||
assertThat(environment.get("OBJECT_STORAGE_AWS_QUALIFICATION_ENABLED")).isEqualTo("true");
|
||
assertThat(environment.get("OBJECT_STORAGE_AWS_BUCKET")).isNotBlank();
|
||
assertThat(environment.get("OBJECT_STORAGE_AWS_REGION")).isNotBlank();
|
||
assertThat(environment.get("OBJECT_STORAGE_AWS_EXPECTED_OWNER")).matches("[0-9]{12}");
|
||
```
|
||
|
||
AWS를 호출하는 코드는 한 줄도 없다. 권한이 없으면 실패한다는 점에서 fail-closed이지만, **권한이 있어도 아무것도 증명하지 않는다.** lane 이름은 `objectStorageAwsQualificationTest`이고 설명은 "Runs only with explicit protected AWS sandbox authority and exact inputs"이므로, CI에서 이 lane이 초록으로 통과한 것을 AWS가 자격 검증되었다는 증거로 읽을 여지가 있다.
|
||
|
||
지금 실제 위험은 낮다 — readiness 레지스트리의 `KNOWN_PROVIDERS`는 `filesystem-local-dev` 하나뿐이라 AWS 주장 row 자체가 없고(§47), README도 "It does not prove … production credentials/TLS/IAM/encryption, S3 response-loss behavior, or R2 readiness"라고 적는다. 그래서 **P3/기록**이다. 다만 `CapabilityEvidenceSource.CI_QUALIFICATION`이라는 값이 존재하고 §41에서 본 대로 AWS profile은 capability를 주장할 수 있으므로, fork가 이 초록 lane을 근거로 삼는 경로가 구조적으로 열려 있다. 수정은 lane 본문을 실제 sandbox 호출로 채우거나, 채우기 전까지 클래스/lane 이름에 "authority-gate"임이 드러나게 하는 것이다.
|
||
|
||
## 60. P3/기록 — provider 신원 문자열이 세 곳에 독립적으로 적혀 있다
|
||
|
||
정확한 MinIO 신원이 세 곳에 문자열로 존재한다.
|
||
|
||
| 위치 | 값 |
|
||
|---|---|
|
||
| `S3ProviderType.MINIO_COMMUNITY_2024_01_16:8` | `s3-compatible-minio-community-release-2024-01-16t16-07-38z` |
|
||
| `S3ProviderVersion:7` | `release-2024-01-16t16-07-38z-sdk-2.30.0` |
|
||
| `minio-provider-evidence.json:3-4` | 위 두 값과 **정확히 동일** |
|
||
|
||
컨테이너 이미지 digest도 세 곳(contract test, fault test, evidence JSON)에 같은 값으로 적혀 있고 현재 모두 일치한다. 그러나 이 일치를 검사하는 test는 없다 — evidence JSON에 대한 assertion은 `status`와 profile 이름에 대한 부분 문자열 확인뿐이다(§58). production 바인딩이 인정하는 신원과 자격 검증이 실제로 돌아간 신원이 갈라져도 아무도 알려주지 않는다. **P3/기록.** 수정은 evidence JSON을 파싱해 `S3ProviderType`/`S3ProviderVersion`의 canonical 값 및 lane의 이미지 digest와 대조하는 assertion 하나를 추가하는 것이다.
|
||
|
||
## 61. Negative-space probes — sub-scope 07
|
||
|
||
- **8.1 lane 등록**: 세 lane 모두 strict·non-skipping·`requiredClasses` 고정(§57).
|
||
- **8.2 조건부 형제**: 여섯 파일 중 provider를 실제로 부르는 것은 **둘**(`MinioManagedObject*`). 나머지 넷 중 둘은 근거 있는 freeze(§58), 둘은 근거 없는 통과(§59).
|
||
- **8.3 중복 신원**: 신원 문자열·이미지 digest 3중 기재, 교차 검사 없음(§60).
|
||
- **8.4 증거의 자기 한정**: evidence JSON의 `limitations` 7줄이 범위를 스스로 좁힌다(§58).
|
||
|
||
## 62. Sub-scope 07 완료 조건
|
||
|
||
- denominator 6 / 6 FULL_READ (`156-...` OWNED FILES)
|
||
- §8.1~§8.4 probe 수행
|
||
- lane 실행은 Docker와 보호된 AWS sandbox 권한이 필요하므로 이 분석에서 실행하지 않음 — 대신 lane이 무엇을 주장하고 그 주장이 어디에 소비되는지를 정적으로 추적
|
||
- 소스 미변경
|
||
|
||
---
|
||
|
||
## 63. 모듈 ledger 정합
|
||
|
||
| # | 범위 | main | test | 기타 | 합 | FULL_READ | probe |
|
||
|---|---|---|---|---|---|---|---|
|
||
| 1 | governance + `config/**` | 19 | 5 | 4 | 28 | 28 | `149`, `150` |
|
||
| 2 | `control/**` | 24 | 1 | – | 25 | 25 | `151` |
|
||
| 3 | `kernel/**` + `codec/**` | 30 | 9 | – | 39 | 39 | `152` |
|
||
| 4 | `s3/**` | 26 | 14 | – | 40 | 40 | `153` |
|
||
| 5 | `direct/**` + `multipart/**` | 18 | 7 | – | 25 | 25 | `154` |
|
||
| 6 | `filesystem/**`+`maintenance/**`+`readiness/**`+`provider/**`+루트 | 30 | 12 | 1 | 43 | 43 | `155` |
|
||
| 7 | qualification source set 3종 | – | – | 6 | 6 | 6 | `156` |
|
||
| | **TOTAL** | **147** | **48** | **11** | **206** | **206** | **7 / 7** |
|
||
|
||
coverage ledger: `FULL_READ` **206** / `STRUCTURAL_ONLY` **0** / `EXCLUDED` **0** / 미분류 **0**.
|
||
|
||
## 64. 모듈 findings
|
||
|
||
| # | 우선순위 | finding | 위치 | reachability |
|
||
|---|---|---|---|---|
|
||
| 1 | **P2** | 직접 multipart의 마지막 part에 grant를 발급할 수 없다 — `requirePartSize(..., false)` 하드코딩 + 요청 타입에 마지막 part 표시 없음 | §38 | 미배선; 배선 시 통상적 클라이언트 분할 전부 |
|
||
| 2 | **P2** | 서명된 bearer URI의 allowlist 검증이 upload 경로에만 있고 download·multipart part 경로에는 없다. `expectedExpiry`는 검사되지 않는다 | §39 | 미배선; 배선 시 브라우저로 나가는 두 bearer |
|
||
| 3 | **P2** | AWS binding에서 `DIRECT_UPLOAD`/`DIRECT_MULTIPART` 요구가 compile을 통과해 presigner를 할당하지만 그것을 쓰는 port가 없다. MinIO에는 같은 주장을 막는 검사가 있다 | §41, §48 | AWS provider + direct capability 요구 배포 |
|
||
| 4 | **P2** | `legacy-adoption.enabled=true, mode=APPLY`는 설정으로 켜지는데 Ed25519 승인 검증기 bean과 승인자 키 설정이 없다 — adapter의 승인 검사는 필드 동등성뿐 | §49 | APPLY를 켜고 port를 직접 부르는 fork |
|
||
| 5 | **P3** | `filesystem-local-dev` production 거부가 `prod`/`production` 두 리터럴에만 걸려 있다 | §5 | 프로파일 이름을 바꾼 fork |
|
||
| 6 | **P3** | nonce replay 경계가 `ClaimResult`를 읽고 버린다 — `TERMINAL_REPLAY`도 그대로 발행 | §50 | APPLY 경로 |
|
||
| 7 | P3/기록 | `markEffectSent`·`markResponseLost`가 `updatedAt`을 전진시키지 않는다 | §22 | 응답 유실 조사 |
|
||
| 8 | P3/기록 | `maximumOutstandingGenerations`가 검증만 되고 읽히지 않으며 grant generation은 항상 1 | §42 | 정책/조립 |
|
||
| 9 | P3/기록 | `DirectTransferCorsPolicy`의 production 소비자 0, 적용 주체가 인프라라는 사실이 README에 없음 | §42 | 문서 |
|
||
| 10 | P3/기록 | 두 coordinator의 process-local bearer 캐시가 무경계·무만료 | §43 | 배선 시 |
|
||
| 11 | P3/기록 | `filesystem-local-dev` capability 표가 두 벌로 하드코딩 | §52 | 표가 갈라질 때 |
|
||
| 12 | P3/기록 | deprecated 루트 경로에 production 프로파일 검사 없음, `autoCreateBucket` 기본 `true`, 평문 엔드포인트 기본, 심링크 검사 없음 | §53 | legacy opt-in 배포 |
|
||
| 13 | P3/기록 | AWS qualification lane이 환경변수만 검사하고 통과한다 | §59 | 초록 lane을 증거로 삼는 fork |
|
||
| 14 | P3/기록 | provider 신원 문자열·이미지 digest가 세 곳에 독립 기재, 교차 검사 없음 | §60 | 신원이 갈라질 때 |
|
||
|
||
**결함 아님으로 판정한 후보 3건** — `RoutingObjectReadAdapter`의 무방비 `split`(§7: `ObjectReference` 생성자 검증이 막는다), control 레코드의 관용 UTF-8 디코딩(§16: printable ASCII 검사가 닫는다), sub-scope 02·04의 zero-finding 결과 자체.
|
||
|
||
**이월 해소 1건** — sub-scope 01의 readiness registry forward reference를 §47에서 확정(파일은 저장소 루트에 실재하고 leaf test가 시스템 프로퍼티로 읽어 9장 카드와 R0/R1 수준을 강제한다).
|
||
|
||
## 65. 이 모듈에서 반복해서 나타난 패턴
|
||
|
||
- **불확실성을 보존한다.** mutation의 비권위적 실패는 `INDETERMINATE`로 분류되고 정확한 GET으로만 해소된다(§30). 손상된 control 레코드는 부재로 보이지 않는다(§51). 부재로 읽히는 유일한 신호는 정확한 404다.
|
||
- **값이 아니라 값들 사이의 관계를 검증한다.** 재시도 최악 예산이 부모 호출 예산 안에 드는지(§28), provider 타입마다 반대 방향의 신원 규칙(§29), 레코드 상태와 증거 완전성의 결합(§37).
|
||
- **표현 불가능성으로 막는다.** canonical key 문법, 논리 다이제스트와 provider 체크섬의 이중 대조(§31), 길이 프레이밍 이진 코덱의 재인코딩 검사(§49).
|
||
- **자기 한정을 문서에 적는다.** README의 R0 선언과 evidence JSON의 `limitations` 7줄(§58), "does not claim multi-node linearizability"(§51).
|
||
- **그리고 이 모듈의 P2 넷은 전부 같은 모양이다** — 설정 표면이나 계약이 절반만 조립돼 있다. 마지막 part 표시가 요청 타입에 없고(§38), 검증이 세 경로 중 하나에만 있고(§39), capability가 compile을 통과하는데 소비자가 없고(§41), APPLY는 설정으로 켜지는데 검증기는 손배선이다(§49). 개별 구현의 품질과 **조립의 대칭성** 사이에 일관된 격차가 있다.
|
||
|
||
## 66. 모듈 완료 조건
|
||
|
||
- denominator 206 / 206 FULL_READ, `STRUCTURAL_ONLY` 0, `EXCLUDED` 0, 미분류 0 (§63)
|
||
- 7개 하위 범위 전부 §8.1~§8.4 네 종 negative-space probe 수행, evidence `149`~`156` 8건 생성
|
||
- 후보 finding 3건을 코드로 추적해 결함 아님으로 판정, 이월 1건 해소
|
||
- 소스 미변경 — 이 분석은 어떤 애플리케이션 코드도 수정하지 않았다
|
||
|
||
## 67. 검증
|
||
|
||
`evidence/raw/157-objectstorage-suite-verification.txt`.
|
||
|
||
```
|
||
$ cd src && LANG=C.UTF-8 LC_ALL=C.UTF-8 ./gradlew :adapter:outbound:objectstorage:test --console=plain -q
|
||
GRADLE_EXIT=0
|
||
classes=47 tests=140 failures=0 errors=0 skipped=0
|
||
|
||
$ git status --short
|
||
changed=0
|
||
```
|
||
|
||
skip 0이라는 점을 기록해 둔다 — 이 leaf의 `:test`에는 조건부로 비활성화되는 test가 없다. Docker나 보호된 자격이 필요한 것들은 애초에 별도 source set으로 분리돼 있고(§57), `:test`에 섞여 들어와 조용히 건너뛰지 않는다. 그 세 lane(`objectStorageMinioContractTest`·`objectStorageMinioFaultTest`·`objectStorageAwsQualificationTest`)은 이 분석에서 실행하지 않았다.
|
||
|
||
작업 트리는 변경 0이다 — 이 분석 과정에서 애플리케이션 소스를 수정하거나 임시 파일을 남기지 않았다.
|
||
|
||
## Source anchors
|
||
|
||
이 문서가 backtick으로 인용한 타입·경로를 저장소 트리에 대고 해석한 결과다. 해석된 것만 싣는다 — 총 **82개** (main 64 · test 10 · 기타 8).
|
||
|
||
```
|
||
src/adapter/outbound/objectstorage/build.gradle
|
||
src/config/architecture/modules.json (adapter-outbound-objectstorage 항목)
|
||
|
||
main:
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/FilesystemObjectStorageAdapter.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/ObjectStorageConfig.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/ObjectStorageSettings.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/S3ObjectStorageAdapter.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/CrockfordBase32.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectControlKeyCodec.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectDataKeyCodec.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/LegacyObjectAdoptionSettings.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/LegacyObjectStorageActivationGuard.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageBindingCompiler.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageCapabilityAssembler.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageLegacyMigrationConfig.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageMaintenanceCapabilityConfig.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageProviderContribution.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/ObjectStorageScanMaintenanceConfig.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectDirectGrantAdapter.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/RoutingObjectReadAdapter.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/config/SelectedObjectStorageProviderFactory.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/CanonicalJsonReader.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ControlRecordSupport.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectControlRecord.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectControlStore.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectOperationRecord.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectPublicationHandoffRecord.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/ObjectStagedObjectRecord.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/control/UnsupportedObjectControlSchemaException.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartCompletionVerifier.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartCoordinator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectTransferCoordinator.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectTransferCorsPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectTransferPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectTransferSessionRecord.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/direct/PresignedGrantRedactor.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/FilesystemLocalDevProviderContribution.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalDevObjectControlStore.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalDevObjectDataStore.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalDevObjectStorageProvider.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/filesystem/LocalObjectPathGuard.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectOperationKernel.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectOperationStateMachine.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/PendingObjectEffect.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/StagedObjectPublicationKernel.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/Ed25519LegacyAdoptionApprovalVerifier.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyAdoptionApprovalCodec.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyAdoptionApprovalReplayStore.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectAdoptionService.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/multipart/MultipartPartLedger.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/multipart/MultipartUploadPlan.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/readiness/CapabilityEvidenceSource.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/readiness/ObjectStorageCapabilityCard.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/readiness/ObjectStorageCapabilityEvidence.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3AsyncRequestBodyBridge.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3AsyncResponseBodyBridge.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ChecksumPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ClientPolicy.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ConditionalObjectControlStore.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ConditionalRequestMapper.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3DirectMultipartProvider.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3DirectTransferProvider.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ObjectEvidenceMapper.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ProviderBinding.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ProviderErrorMapper.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ProviderType.java
|
||
src/main/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3ProviderVersion.java
|
||
|
||
test:
|
||
src/test/java/dev/caskeleton/adapter/outbound/objectstorage/LegacyObjectStorageConfigTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/objectstorage/codec/ObjectNamespaceCodecTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectMultipartCoordinatorTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/objectstorage/direct/DirectTransferCorsContractTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/objectstorage/kernel/ObjectOperationStateMachineTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyAdoptionApprovalVerifierTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/objectstorage/maintenance/LegacyObjectAdoptionServiceTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/objectstorage/readiness/ObjectStorageReadinessRegistryTest.java
|
||
src/test/java/dev/caskeleton/adapter/outbound/objectstorage/s3/S3AsyncClientFactoryTest.java
|
||
src/test/resources/object-storage/minio-provider-evidence.json
|
||
|
||
기타:
|
||
docs/registries/object-storage-readiness.yaml
|
||
src/build.gradle
|
||
src/objectStorageAwsQualificationTest/java/dev/caskeleton/adapter/outbound/objectstorage/qualification/AwsS3DirectTransferQualificationTest.java
|
||
src/objectStorageAwsQualificationTest/java/dev/caskeleton/adapter/outbound/objectstorage/qualification/AwsS3ManagedCommonSubsetQualificationTest.java
|
||
src/objectStorageMinioContractTest/java/dev/caskeleton/adapter/outbound/objectstorage/qualification/MinioDirectTransferContractTest.java
|
||
src/objectStorageMinioContractTest/java/dev/caskeleton/adapter/outbound/objectstorage/qualification/MinioManagedObjectContractTest.java
|
||
src/objectStorageMinioFaultTest/java/dev/caskeleton/adapter/outbound/objectstorage/qualification/MinioDirectTransferFaultTest.java
|
||
src/objectStorageMinioFaultTest/java/dev/caskeleton/adapter/outbound/objectstorage/qualification/MinioManagedObjectFaultTest.java
|
||
|
||
해석되지 않은 인용 (11종) — 외부 타입·문서상 약칭 등:
|
||
application.yml
|
||
evidence/raw/149-objectstorage-module-inventory.txt
|
||
evidence/raw/150-objectstorage-config-activation-probes.txt
|
||
application-prod.yml
|
||
evidence/raw/151-objectstorage-control-probes.txt
|
||
evidence/raw/152-objectstorage-kernel-codec-probes.txt
|
||
evidence/raw/153-objectstorage-s3-probes.txt
|
||
evidence/raw/154-objectstorage-direct-multipart-probes.txt
|
||
evidence/raw/155-objectstorage-platform-readiness-probes.txt
|
||
evidence/raw/156-objectstorage-qualification-lanes-probes.txt
|
||
evidence/raw/157-objectstorage-suite-verification.txt
|
||
|
||
```
|