Commit 44471d1
feat(app): add shared object storage foundation (#752)
## 배경
`app` / `common` 레이어에서 공통 object storage foundation이 필요해서, storage 계약과
provider adapter를 먼저 올리는 PR입니다.
이번 PR은 foundation만 다룹니다.
- 공통 storage contract
- provider별 adapter wiring
- blob backing store
- compression / smoke / 회귀 테스트
- backend reference 문서
상위 asset metadata 모델과 서비스별 attachment 통합은 의도적으로 제외했습니다.
## 이번 PR에서 포함한 범위
- `common`
- `ObjectStoragePort`
- `StorageUri`
- `StoreObjectRequest`, `OpenDownloadRequest`, `DownloadHandle`
- `app`
- `S3CompatibleObjectStorageAdapter`
- `MinioObjectStorageAdapter`
- `TencentCosObjectStorageAdapter`
- `BlobObjectStorageAdapter`
- `ObjectStorageRegistry`
- blob backing store migration / entity / repository
- 문서
- `docs/reference/backend/object-storage.md`
- 관련 backend index 갱신
- MinIO smoke / compression 작업 메모 추가
## 주요 변경
- `storage_uri`는 public URL이 아니라 internal locator로만 사용합니다.
- backend alias는 논리 이름 `minio` / `tencent-cos` / `blob`으로 정리했습니다.
- S3-compatible adapter는 metadata round-trip을 보장합니다.
- MinIO는 path-style access를 강제합니다.
- blob backend는 bucket-qualified target을 reject합니다.
- blob download도 `filename` / `disposition` 계약을 맞췄습니다.
- `Content-Disposition`은 filename이 없어도 `attachment` / `inline`을 유지하고,
filename이 있으면 ASCII fallback + `filename*` UTF-8 encoding을 사용합니다.
- oversized blob 업로드는 configurable limit로 방어합니다.
- `blob`는 텍스트 계열 payload에 대해 선택적 `GZIP` compression을 지원합니다.
- `S3CompatibleObjectStorageAdapter.head()`는 provider 편차를 고려해 header /
metadata / range / list fallback을 사용합니다.
- env-gated `MinIO` smoke test를 추가했습니다.
## MinIO smoke 메모
- 이 개발기에서는 `39000` 포트를 `ZscalerTunnel`이 이미 점유하고 있었습니다.
- 그래서 `http://127.0.0.1:39000`은 MinIO가 아니라 `Proxy-Agent: Ztunnel/1.0`
응답이 나왔습니다.
- 실제 smoke는 `39100:9000`으로 MinIO를 띄워서 검증했습니다.
- smoke test는 endpoint env를 그대로 사용하므로 특정 포트 고정에 의존하지 않습니다.
## 설정/기본값
- local profile 기본 provider: `minio`
- dev / ci 기본 provider: `blob`
- prod profile 기본 provider: `blob`
- `Tencent COS` adapter는 wiring과 테스트까지 포함하지만, 운영 기본값 전환은 별도 판단으로 남겨뒀습니다.
## 리뷰 반영
이번 PR에서 리뷰로 반영한 핵심은 아래입니다.
- blob adapter의 `readBytes()` 제거 및 max size guard 추가
- `BlobStoredObjectEntity.id` non-nullable 정리
- S3 metadata round-trip 반영
- MinIO path-style wiring 강제 및 회귀 테스트 추가
- blob bucket-qualified target reject
- `Content-Disposition` filename 없는 케이스까지 보강
- backend alias를 `minio` / `tencent-cos` / `blob`으로 정리
- 실제 MinIO smoke에서 드러난 port 하드코딩 assertion 제거
## 검증
아래 focused suite로 확인했습니다.
```bash
RUN_MINIO_SMOKE_TEST=true \
MINIO_SMOKE_TEST_ENDPOINT=http://127.0.0.1:39100 \
MINIO_SMOKE_TEST_REGION=us-east-1 \
MINIO_SMOKE_TEST_ACCESS_KEY=minio \
MINIO_SMOKE_TEST_SECRET_KEY=minio123 \
MINIO_SMOKE_TEST_BUCKET=deck-smoke \
./gradlew --no-build-cache :common:test --tests "io.deck.common.api.storage.StorageUriTest" \
:app:test --tests "io.deck.app.storage.blob.BlobObjectStorageAdapterTest" \
--tests "io.deck.app.storage.s3.S3CompatibleObjectStorageAdapterTest" \
--tests "io.deck.app.storage.minio.MinioObjectStorageSmokeTest"
```
또한 fresh DB에서 `app` migration만 지정해 blob backing store migration을 검증했습니다.
## 주의 사항
- `:app` Gradle Flyway task는 기본적으로
`db/migration/ci-seed/V9999__ci-seed.sql`까지 함께 스캔합니다.
- 이번 fresh migration 검증은
`-Dflyway.locations=filesystem:src/main/resources/db/migration/app`로
`app` migration만 지정해서 확인했습니다.
## 이번 PR에서 제외한 범위
- `StoredAsset` 공통 자산 메타데이터 모델
- `PUBLIC` / `PRIVATE` asset serving 정책
- `meetpie og_image`의 object storage 이전
- `meetpie` namecard 내부 이미지의 persisted `imageUrl/data:image` 제거
- `deskpie` note/file attachment 통합
## 후속 설계
상위 자산 모델과 서비스 통합 방향은 별도 이슈로 분리했습니다.
- #756
- #756
---------
Co-authored-by: JK <jk@chequer.io>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>1 parent 9b21b02 commit 44471d1
36 files changed
Lines changed: 2724 additions & 1 deletion
File tree
- backend
- app
- src
- main
- kotlin/io/deck/app
- config
- storage
- blob
- cos
- minio
- s3
- resources
- db/migration/app
- test
- kotlin/io/deck/app
- config
- migration
- storage
- blob
- cos
- minio
- s3
- resources
- common/src
- main/kotlin/io/deck/common/api
- config
- storage
- test/kotlin/io/deck/common/api/storage
- gradle
- docs
- plans
- reference
- backend
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
61 | 65 | | |
62 | 66 | | |
63 | 67 | | |
| |||
Lines changed: 206 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
Lines changed: 12 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
0 commit comments