Skip to content

Commit 8d05a69

Browse files
authored
Merge pull request #279 from DocGrid/feature/278
[Feat] 파일 저장소 구현체 선택 설정 및 Local Filesystem 지원
2 parents 8aeedc9 + 7f7b592 commit 8d05a69

50 files changed

Lines changed: 982 additions & 71 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ SPRING_PROFILES_ACTIVE=local
1313
# 로컬 Hibernate SQL 진단이 필요할 때만 주석 해제합니다.
1414
# JPA_SHOW_SQL=true
1515

16-
# MinIO (docker-compose 기본값)
16+
# 파일 저장소 - 기본 애플리케이션 값은 local이지만 현재 Docker 개발환경은 MinIO를 명시적으로 사용합니다.
17+
STORAGE_TYPE=minio
18+
STORAGE_BUCKET=docgrid
19+
# STORAGE_TYPE=local일 때만 사용하며 실제 파일은 Git에 포함되지 않습니다.
20+
STORAGE_LOCAL_ROOT=./data/docgrid
21+
22+
# MinIO Adapter (docker-compose 기본값)
1723
MINIO_ENDPOINT=http://localhost:9000
1824
MINIO_ACCESS_KEY=minioadmin
1925
MINIO_SECRET_KEY=minioadmin1234
20-
MINIO_BUCKET=docgrid
2126

2227
# 문서 Chunk 정책 - 기본값을 쓰면 설정하지 않아도 됩니다.
2328
# 같은 배포군의 Worker는 결정성을 위해 반드시 같은 값을 사용해야 합니다.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,8 @@ __pycache__/
4545
.env
4646
.env.properties
4747

48+
# Local Filesystem Adapter가 저장하는 문서 원본
49+
/data/
50+
4851
# 공급사 설치 파일과 라이선스는 Repository 외부에 보관한다.
4952
/.local-vendor/

README.md

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,42 @@ cd docgrid
3535

3636
이후 명령은 별도 안내가 없는 한 저장소 루트에서 실행합니다.
3737

38+
## 파일 저장소 선택
39+
40+
DocGrid의 API와 인덱싱 Worker는 특정 Cloud SDK가 아니라 공통 파일 저장소 Port를 사용합니다. 현재
41+
Local Filesystem과 MinIO Adapter를 지원하며 `STORAGE_TYPE`으로 하나만 선택합니다.
42+
43+
| `STORAGE_TYPE` | 용도 | 추가 설정 |
44+
|---|---|---|
45+
| `local` | 별도 Object Storage 없이 실행하는 기본값 | `STORAGE_LOCAL_ROOT`, `STORAGE_BUCKET` |
46+
| `minio` | Docker 또는 외부 MinIO 사용 | `STORAGE_BUCKET`, `MINIO_ENDPOINT`, Credential |
47+
48+
Local Filesystem은 `STORAGE_TYPE`을 설정하지 않았을 때의 애플리케이션 기본값입니다. 실제 절대 경로는
49+
DB에 저장하지 않고, DB에는 `LOCAL` Provider와 논리 Bucket·Object Key만 저장합니다.
50+
51+
```dotenv
52+
STORAGE_TYPE=local
53+
STORAGE_BUCKET=docgrid
54+
STORAGE_LOCAL_ROOT=./data/docgrid
55+
```
56+
57+
현재 `.env.example`은 팀의 Docker MinIO 개발 방식을 바로 실행할 수 있도록 `minio`를 명시합니다.
58+
59+
```dotenv
60+
STORAGE_TYPE=minio
61+
STORAGE_BUCKET=docgrid
62+
MINIO_ENDPOINT=http://localhost:9000
63+
MINIO_ACCESS_KEY=minioadmin
64+
MINIO_SECRET_KEY=minioadmin1234
65+
```
66+
67+
API와 Worker는 반드시 동일한 `STORAGE_TYPE`과 저장소 Endpoint·Bucket을 사용해야 합니다. 같은 DB
68+
Schema를 사용하면서 서로 다른 Local Directory나 개발자별 MinIO를 바라보면 DB의 Object Key는
69+
존재하지만 실제 파일을 찾지 못합니다.
70+
71+
`STORAGE_TYPE` 변경은 기존 파일을 자동으로 옮기지 않습니다. 파일이 들어 있는 DB Schema의 Provider를
72+
바꾸려면 Object와 DB Metadata를 함께 이전하는 별도 Migration이 필요합니다.
73+
3874
## EC2 OpenSQL을 사용하는 로컬 실행
3975

4076
프론트엔드와 Spring Boot는 로컬에서 실행하고, DB만 SSH Tunnel을 통해 EC2 OpenSQL에 연결합니다.
@@ -55,10 +91,17 @@ DB_PORT=55433
5591
DB_NAME=<development-database>
5692
DB_USER=<development-user>
5793
DB_PASSWORD=<development-password>
58-
DB_SCHEMA=public
94+
DB_SCHEMA=<development-schema>
5995
DB_SSLMODE=disable
6096
SPRING_PROFILES_ACTIVE=local
6197
98+
# EC2 OpenSQL의 개발자별 Schema와 한 환경으로 묶을 Local Docker MinIO입니다.
99+
STORAGE_TYPE=minio
100+
STORAGE_BUCKET=docgrid-<developer>
101+
MINIO_ENDPOINT=http://localhost:9000
102+
MINIO_ACCESS_KEY=minioadmin
103+
MINIO_SECRET_KEY=minioadmin1234
104+
62105
# 자동 인덱싱 Worker를 로컬에서 실행합니다.
63106
INDEXING_WORKER_ENABLED=true
64107
INDEXING_WORKER_MAX_CONCURRENCY=1
@@ -70,8 +113,9 @@ OLLAMA_MODEL=qwen2.5:7b
70113
`DB_SSLMODE=disable`은 DB 자체 TLS가 비활성화되어 있고 아래 SSH Tunnel로 전송 구간을 암호화하는
71114
환경의 설정입니다. OpenSQL 서버가 TLS를 제공하면 서버 정책에 맞는 SSL Mode를 사용합니다.
72115

73-
MinIO 설정은 `.env.example`의 로컬 기본값을 그대로 사용할 수 있습니다. `.env`는 Git에 추가하지
74-
않습니다.
116+
EC2 OpenSQL과 개발자별 Local MinIO를 함께 사용할 때는 개발자마다 DB Schema와 Bucket을 분리해야
117+
합니다. 팀 공용 Schema를 사용하려면 모든 API와 Worker가 접근할 수 있는 공용 저장소가 필요합니다.
118+
`.env`는 Git에 추가하지 않습니다.
75119

76120
### 2. OpenSQL SSH Tunnel 열기
77121

@@ -104,6 +148,12 @@ Docker Desktop을 실행한 뒤 로컬 인프라를 기동합니다. 이 구성
104148
docker compose up -d --build minio embedding-server
105149
```
106150

151+
`STORAGE_TYPE=local`을 선택했다면 MinIO는 실행하지 않고 BGE-M3만 기동합니다.
152+
153+
```bash
154+
docker compose up -d --build embedding-server
155+
```
156+
107157
BGE-M3는 첫 실행 시 약 3GB 모델을 내려받으므로 준비까지 10~15분 정도 걸릴 수 있습니다. 모델이
108158
준비되기 전에 Spring Boot의 인덱싱 Worker를 실행하지 마세요.
109159

@@ -116,6 +166,7 @@ docker compose logs -f embedding-server
116166

117167
```bash
118168
curl -f http://localhost:8000/health
169+
# STORAGE_TYPE=minio인 경우에만 확인합니다.
119170
curl -f http://localhost:9000/minio/health/live
120171
```
121172

@@ -188,7 +239,7 @@ npm --prefix frontend run dev
188239
4. 문서 내용으로 검색
189240
5. RAG 질문에 Ollama 답변과 인용 근거가 표시되는지 확인
190241

191-
인덱싱은 BGE-M3와 MinIO가 필요하고, 최종 RAG 답변 생성은 Ollama가 필요합니다.
242+
인덱싱은 BGE-M3와 선택한 파일 저장소가 필요하고, 최종 RAG 답변 생성은 Ollama가 필요합니다.
192243

193244
## 사용 포트
194245

@@ -199,8 +250,8 @@ npm --prefix frontend run dev
199250
| `55433` | 로컬 | EC2 OpenSQL로 연결되는 SSH Tunnel |
200251
| `5432` | EC2 | OpenSQL 실제 포트 |
201252
| `8000` | 로컬 Docker | BGE-M3 임베딩 서버 |
202-
| `9000` | 로컬 Docker | MinIO API |
203-
| `9001` | 로컬 Docker | MinIO Console |
253+
| `9000` | 로컬 Docker | MinIO API(`STORAGE_TYPE=minio`) |
254+
| `9001` | 로컬 Docker | MinIO Console(`STORAGE_TYPE=minio`) |
204255
| `11434` | 로컬 (네이티브) | Ollama RAG LLM 서버 |
205256

206257
## 종료
@@ -212,6 +263,13 @@ Docker Service는 다음 명령으로 중지합니다.
212263
docker compose stop minio embedding-server
213264
```
214265

266+
Local Filesystem을 사용했다면 `embedding-server`만 중지합니다. `STORAGE_LOCAL_ROOT`의 원본 파일은
267+
애플리케이션 종료 후에도 유지되며 Git에 포함되지 않습니다.
268+
269+
```bash
270+
docker compose stop embedding-server
271+
```
272+
215273
로컬 PostgreSQL 구성까지 실행했다면 `postgres`도 함께 중지합니다.
216274

217275
```bash
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.opensource.docgrid.domain.document.config;
2+
3+
import java.nio.file.Path;
4+
5+
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
import org.springframework.stereotype.Component;
7+
8+
import lombok.Getter;
9+
import lombok.Setter;
10+
11+
/**
12+
* 파일 저장소 Adapter 선택과 모든 구현이 공유하는 논리적 저장 위치 설정을 제공한다.
13+
* 실제 Provider Client 생성과 I/O는 각 Adapter가 담당하며 이 설정은 도메인 분기를 만들지 않는다.
14+
*/
15+
@Getter
16+
@Setter
17+
@Component
18+
@ConfigurationProperties(prefix = "storage")
19+
public class FileStorageProperties {
20+
21+
private FileStorageType type = FileStorageType.LOCAL;
22+
private String bucket = "docgrid";
23+
private Local local = new Local();
24+
25+
/**
26+
* Local Filesystem Adapter가 Object Key를 해석할 기준 Root를 제공한다.
27+
*/
28+
@Getter
29+
@Setter
30+
public static class Local {
31+
32+
private Path root = Path.of("./data/docgrid");
33+
}
34+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.opensource.docgrid.domain.document.config;
2+
3+
/**
4+
* 현재 실행 환경에서 활성화할 수 있는 파일 저장소 Adapter 종류를 제한한다.
5+
* DB에 기록되는 StorageProvider와 달리, 실제로 등록된 Adapter만 설정 값으로 노출한다.
6+
*/
7+
public enum FileStorageType {
8+
LOCAL,
9+
MINIO
10+
}

backend/src/main/java/com/opensource/docgrid/domain/document/entity/FileObject.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
* 파일 오브젝트(실제 바이너리 위치) 테이블.
2929
*
3030
* <p>역할: 업로드된 파일 바이너리의 저장 위치와 해시를 기록한다.
31-
* 이유: 실제 파일 바이너리는 DB에 저장하지 않고 MinIO/S3/local오브젝트 스토리지에 저장하므로,
32-
* 그 위치(bucket/objectKey)와 무결성 검증용 해시만 이 테이블에 보관한다.
31+
* 이유: 실제 파일 바이너리는 DB에 저장하지 않고 Local/MinIO/S3 등 외부 파일 저장소에 저장하므로,
32+
* 그 논리 위치(bucket/objectKey)와 무결성 검증용 해시만 이 테이블에 보관한다.
3333
* 관계: document_versions.file_object_id가 이 테이블을 참조한다(하나의 파일이 여러 버전에서 재사용될 수 있음).
3434
* unique 제약: 동일 storage_provider/bucket/objectKey 조합과 file_hash/file_size 조합은 각각 유일해야 한다.
3535
* index: uploaded_by에 대한 조회 인덱스를 둔다.

backend/src/main/java/com/opensource/docgrid/domain/document/repository/FileObjectRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ INSERT INTO file_objects (
2121
created_at, updated_at
2222
) VALUES (
2323
:bucketName, :objectKey, :originalFilename, :contentType,
24-
:fileSize, :fileHash, 'MINIO', :uploadedBy, CURRENT_TIMESTAMP,
24+
:fileSize, :fileHash, :storageProvider, :uploadedBy, CURRENT_TIMESTAMP,
2525
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
2626
)
2727
ON CONFLICT (file_hash, file_size) DO NOTHING
@@ -33,6 +33,7 @@ int insertIfAbsent(
3333
@Param("contentType") String contentType,
3434
@Param("fileSize") Long fileSize,
3535
@Param("fileHash") String fileHash,
36+
@Param("storageProvider") String storageProvider,
3637
@Param("uploadedBy") Long uploadedBy
3738
);
3839
}

backend/src/main/java/com/opensource/docgrid/domain/document/service/DocumentParsingService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
/**
1818
* 두 개의 짧은 DB Transaction 사이에서 원본 읽기, 형식별 파싱과 Chunk 계산을 조정한다.
1919
*
20-
* <p>이 Service 자체에는 Transaction을 적용하지 않아 MinIO I/O와 CPU 계산 중 DB 행 잠금이
20+
* <p>이 Service 자체에는 Transaction을 적용하지 않아 파일 저장소 I/O와 CPU 계산 중 DB 행 잠금이
2121
* 유지되지 않게 한다. 외부 구간에는 불변 Snapshot과 Draft만 전달하고 JPA Entity는 전달하지 않는다.
2222
*/
2323
@Service

backend/src/main/java/com/opensource/docgrid/domain/document/service/DocumentUploadFacade.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private StoredFile storeCandidate(DocumentUploadRequest request, ValidatedFile v
7272
objectKey
7373
);
7474
} catch (IOException e) {
75-
log.error("MinIO 업로드용 파일 스트림을 열지 못했습니다.", e);
75+
log.error("파일 저장소 업로드용 원본 Stream을 열지 못했습니다.", e);
7676
throw new DocGridException(ErrorCode.FILE_STORAGE_FAILED, e);
7777
}
7878
}
@@ -101,8 +101,7 @@ private void cleanupCandidate(StoredFile candidate) {
101101
try {
102102
fileStorageService.delete(candidate);
103103
} catch (RuntimeException cleanupException) {
104-
log.error("사용되지 않은 MinIO Object 정리에 실패했습니다. bucket={}, objectKey={}",
105-
candidate.bucketName(), candidate.objectKey(), cleanupException);
104+
log.error("사용되지 않은 저장소 Object 정리에 실패했습니다.", cleanupException);
106105
}
107106
}
108107
}

backend/src/main/java/com/opensource/docgrid/domain/document/service/DocumentVersionUploadFacade.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private StoredFile storeCandidate(DocumentVersionUploadRequest request, Validate
7272
inputStream, validatedFile.fileSize(), validatedFile.contentType(), objectKey
7373
);
7474
} catch (IOException exception) {
75-
log.error("MinIO 업로드용 파일 스트림을 열지 못했습니다.", exception);
75+
log.error("파일 저장소 업로드용 원본 Stream을 열지 못했습니다.", exception);
7676
throw new DocGridException(ErrorCode.FILE_STORAGE_FAILED, exception);
7777
}
7878
}
@@ -94,8 +94,7 @@ private void cleanupCandidate(StoredFile candidate) {
9494
try {
9595
fileStorageService.delete(candidate);
9696
} catch (RuntimeException cleanupException) {
97-
log.error("사용되지 않은 MinIO Object 정리에 실패했습니다. bucket={}, objectKey={}",
98-
candidate.bucketName(), candidate.objectKey(), cleanupException);
97+
log.error("사용되지 않은 저장소 Object 정리에 실패했습니다.", cleanupException);
9998
}
10099
}
101100
}

0 commit comments

Comments
 (0)