Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ DB_PASSWORD=local_password
DB_SCHEMA=public
DB_SSLMODE=disable
SPRING_PROFILES_ACTIVE=local
# JWT 서명 키 - 기본값이 없는 필수 값입니다. 32바이트 이상 임의 값을 생성해 .env에만 둡니다.
# echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env
# 로컬 Hibernate SQL 진단이 필요할 때만 주석 해제합니다.
# JPA_SHOW_SQL=true

# 파일 저장소 - 기본 애플리케이션 값은 local이지만 현재 Docker 개발환경은 MinIO를 명시적으로 사용합니다.
STORAGE_TYPE=minio
# 파일 저장소 - 빠른 실행은 별도 Object Storage 없이 Local Filesystem을 사용합니다.
STORAGE_TYPE=local
STORAGE_BUCKET=docgrid
# STORAGE_TYPE=local일 때만 사용하며 실제 파일은 Git에 포함되지 않습니다.
STORAGE_LOCAL_ROOT=./data/docgrid

# MinIO Adapter (docker-compose 기본값)
# MinIO Adapter - STORAGE_TYPE=minio와 선택형 minio Profile(docker compose --profile minio)을 쓸 때만 사용합니다.
MINIO_ENDPOINT=http://localhost:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin1234
Expand Down
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

DocGrid는 조직의 PDF·DOCX 문서를 저장·인덱싱하고, 사용자가 열람할 수 있는 문서만 벡터 검색과
RAG 답변에 활용합니다. 문서·컬렉션 관리, 인용 근거, MCP 연동, 인덱싱 상태와 복구 작업을 웹에서
제공합니다. 로컬 개발은 PostgreSQL·MinIO·Valkey로 재현할 수 있습니다. 최종 배포 구성은
제공합니다. 로컬 개발은 PostgreSQL·Valkey와 Local Filesystem 저장소로 재현할 수 있습니다. 최종 배포 구성은
Vercel 프런트엔드, Cloud Run 공개 게이트웨이, GCP의 백엔드 A/B·AI·캐시 VM,
OpenProxy를 경유하는 OpenSQL 3노드와 GCS 원본 저장소를 연결합니다.

Expand Down Expand Up @@ -105,15 +105,15 @@ Pub/Sub 신호는 재조회 계기이며, 내구성 있는 메시지 큐로 사
| Backend | Java 17, Spring Boot 3.5.16, Spring Security, Spring Data JPA, Spring AI MCP, STOMP WebSocket, Flyway, Spring Boot Actuator |
| Database · HA | OpenSQL 17.8 / PostgreSQL 17, pgvector 0.8.1, OpenProxy A/B, Patroni, etcd 3노드 |
| AI | FastAPI, BAAI/bge-m3 Embedding, Ollama, qwen2.5:7b |
| Storage | GCS (GCP 원본), MinIO (로컬), Local Filesystem, 선택형 AWS S3 Adapter |
| Storage | GCS (GCP 원본), Local Filesystem (로컬 기본값), 선택형 MinIO·AWS S3 Adapter |
| Cache · 메시지 | 공용 Redis VM (GCP 역할 캐시·대시보드 Pub/Sub), Valkey 9.1 (로컬 Compose), Spring Data Redis |
| 배포 · 네트워크 | Vercel, Cloud Run Nginx 공개 게이트웨이, Google Cloud Load Balancing, Compute Engine VM, IAP (위 구성도 기준) |
| 관측 · 부하 | RAGOps Dashboard, Micrometer/Prometheus, Grafana, Blackbox Exporter, k6 Remote Write (위 구성도 기준); Alertmanager (로컬 monitoring profile) |
| Test | JUnit 5, Gradle, Node.js Test Runner, k6 |

## 빠른 실행

아래는 GCP 접근 권한 없이 로컬 PostgreSQL·MinIO·Valkey로 핵심 기능을 확인하는 경로입니다.
아래는 GCP 접근 권한 없이 로컬 PostgreSQL·Valkey와 Local Filesystem 저장소로 핵심 기능을 확인하는 경로입니다.
명령은 저장소 루트에서 실행합니다.

### 1. 사전 준비
Expand All @@ -130,19 +130,23 @@ Pub/Sub 신호는 재조회 계기이며, 내구성 있는 메시지 큐로 사
git clone https://github.com/DocGrid/docgrid.git
cd docgrid
cp .env.example .env
echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env
cp frontend/.env.example frontend/.env.local
npm --prefix frontend ci
```

`.env.example`은 로컬 PostgreSQL·MinIO 접속값을 제공하지만 인덱싱 Worker는 기본값으로 **꺼져
로그인 토큰 서명 키 `JWT_SECRET`은 기본값이 없는 필수 값이므로 저장소에 두지 않고 두 번째 명령으로 로컬에서
생성합니다.

`.env.example`은 로컬 PostgreSQL 접속값과 Local Filesystem 저장소(`STORAGE_TYPE=local`)를 제공하지만 인덱싱 Worker는 기본값으로 **꺼져
있습니다**. 업로드 후 자동 인덱싱까지 확인하려면 로컬 `.env`에
`INDEXING_WORKER_ENABLED=true`를 추가합니다. 실제 운영 비밀번호와 외부 서버 인증정보는 README,
Issue 또는 Commit에 기록하지 않습니다.

### 3. PostgreSQL, MinIO, Valkey, BGE-M3 실행
### 3. PostgreSQL, Valkey, BGE-M3 실행

```bash
docker compose up -d --build postgres minio valkey embedding-server
docker compose up -d --build postgres valkey embedding-server
docker compose logs -f embedding-server
```

Expand All @@ -151,7 +155,6 @@ BGE-M3는 첫 실행 시 모델 다운로드와 로딩이 필요하므로 준비

```bash
curl -f http://localhost:8000/health/ready
curl -f http://localhost:9000/minio/health/live
```

### 4. Ollama 실행
Expand Down Expand Up @@ -221,7 +224,7 @@ Embedding Provider가 준비되지 않아도 먼저 기동해 수집 실패와
### 종료

```bash
docker compose stop postgres minio valkey embedding-server
docker compose stop postgres valkey embedding-server
# macOS에서 Homebrew로 Ollama를 실행한 경우
brew services stop ollama
```
Expand All @@ -236,8 +239,8 @@ brew services stop ollama

| `STORAGE_TYPE` | 용도 | 주요 설정 |
|---|---|---|
| `local` | 별도 Object Storage 없는 단일 실행 환경 | `STORAGE_LOCAL_ROOT`, `STORAGE_BUCKET` |
| `minio` | 기본 Docker 개발 환경 | `MINIO_ENDPOINT`, Credential, `STORAGE_BUCKET` |
| `local` | 빠른 실행 기본값. 별도 Object Storage 없는 단일 실행 환경 | `STORAGE_LOCAL_ROOT`, `STORAGE_BUCKET` |
| `minio` | 선택형 `minio` Compose Profile(`docker compose --profile minio up -d minio`). 공식 이미지를 새로 받을 수 없어 이미지가 이미 있는 환경에서만 사용 | `MINIO_ENDPOINT`, Credential, `STORAGE_BUCKET` |
| `s3` | 코드에 남아 있는 선택형 Adapter; 현재 GCP 저장소에는 미사용 | `AWS_REGION`, AWS Credential, `STORAGE_BUCKET` |
| `gcs` | Google Cloud Storage 환경 | ADC 인증, `STORAGE_BUCKET` |

Expand Down Expand Up @@ -357,7 +360,7 @@ DocGrid의 자체 소스코드와 문서는 [Apache License 2.0](LICENSE)에 따
| pgvector/pgvector 0.8.1 (Container Image) | 로컬 Compose PostgreSQL 17 | pgvector는 PostgreSQL License. 이미지는 PostgreSQL과 OS 패키지를 포함 |
| Prometheus v3.5.5, Alertmanager v0.33.1 (Container Image) | 선택형 `monitoring` profile | Apache-2.0 |
| Grafana 13.2.3 (Container Image) | 선택형 `monitoring` profile. 수정하지 않고 별도 컨테이너로 실행 | AGPL-3.0 (OSI 승인) |
| MinIO (Container Image) | 로컬 Compose의 `minio` 서비스. 팀은 현재 사용하지 않음(파일 저장소는 GCS). 공식 `minio/minio` 이미지는 2026-10-09 기준 새로 받을 수 없음(`docker pull` 거부 확인) | AGPL-3.0 |
| MinIO (Container Image) | 선택형 `minio` Compose Profile. 빠른 실행과 기본 설정(`STORAGE_TYPE=local`)에서는 기동하지 않으며 팀 배포 저장소는 GCS. 공식 `minio/minio` 이미지는 2026-10-09 `docker pull` 거부, 2026-10-15 Docker Hub 저장소 API 404로 새로 받을 수 없음 | AGPL-3.0 |

외부 모델, 라이브러리, Container Image와 OpenSQL 배포본은 DocGrid의 Apache License 2.0 적용 범위에
포함되지 않습니다. 위 표의 라이선스는 2026-10-09 기준 각 패키지의 메타데이터(POM·PyPI·npm
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ services:
minio:
image: minio/minio
container_name: docgrid-minio
# 공식 이미지를 새로 받을 수 없어 기본 실행에서 제외하고 STORAGE_TYPE=minio를 쓸 때만 명시적으로 기동한다.
profiles: ["minio"]
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin1234
Expand Down
152 changes: 152 additions & 0 deletions docs/design/gimin-#502-readme-quickstart-local-storage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# README 빠른 실행을 새 PC에서 끝까지 재현되게 수정 (#502)

closes #502

#454에서 "기본 경로 분리(compose profile, `.env.example`, README 빠른 실행 변경)는 팀 협의 후 별도 이슈"로
남긴 항목이다. 팀 협의를 거쳐 진행한다. 새로 클론한 PC에서 README를 그대로 따라 하는 재현 검증 중
`JWT_SECRET` 누락을 추가로 발견해 같은 이슈에서 고친다.

## 1. 배경

2차 평가(10/12~10/28)는 평가 시점의 저장소를 새 PC에 클론해 README를 따라 실행한다. 수정 전 README
「빠른 실행」은 새 PC에서 두 곳에서 멈춘다.

### 1.1 3단계: MinIO 이미지를 받을 수 없다

- `docker-compose.yml`의 `minio` 서비스는 태그 없는 `image: minio/minio`를 사용한다.
- 공식 이미지는 새로 받을 수 없다. 2026-10-09 `docker pull` 거부(#454)에 이어 2026-10-15에도 Docker Hub
저장소 API(`hub.docker.com/v2/repositories/minio/minio/`)가 404를 반환했다.
- README 3단계 `docker compose up -d --build postgres minio valkey embedding-server`가 minio를 포함하고,
`.env.example`은 `STORAGE_TYPE=minio`를 지정한다.
- 팀 개발 PC에는 이미지가 남아 있어 드러나지 않았다.

반면 애플리케이션은 이미 MinIO 없이 실행할 수 있다.

| 확인 위치 | 내용 |
|---|---|
| `application.yml` | `storage.type: ${STORAGE_TYPE:local}` |
| `FileStorageProperties` | 기본 `FileStorageType.LOCAL`, Local이면 Bucket 미설정 시 `docgrid` 자동 사용 |
| `LocalFileStorageService` | `@ConditionalOnProperty(havingValue = "local", matchIfMissing = true)` |
| `MinioStorageService`, `MinioConfig` | `@ConditionalOnProperty(havingValue = "minio")`. 다른 저장소에서는 MinIO Endpoint·Credential을 요구하지 않음 |

### 1.2 5단계: `JWT_SECRET`이 없어 Backend가 기동하지 않는다

- `application.yml`의 `jwt.secret: ${JWT_SECRET}`은 기본값이 없고 `JwtProvider`가 생성자에서 주입받는다.
- `.env.example`, README, `backend/README.md` 어디에도 `JWT_SECRET`이 없다. 팀원은 각자 `.env`에 값을 두고
있어 드러나지 않았다.
- 새 PC에서는 `bootRun`이 `Could not resolve placeholder 'JWT_SECRET'`로 Context 초기화에 실패한다.
- local 프로필에서 기본값 없이 필수인 Placeholder는 이 값과 `MINIO_*`(`.env.example`에 있음)뿐이다.

## 2. 결정

**애플리케이션 코드와 Spring 설정은 바꾸지 않고, 실행 설정과 README만 바꾼다.**

| 파일 | 변경 |
|---|---|
| `.env.example` | `STORAGE_TYPE=minio` → `STORAGE_TYPE=local`. MinIO 변수는 남기고 선택형 Profile에서만 쓴다고 주석 수정. `JWT_SECRET` 생성 방법을 주석으로 안내 |
| `docker-compose.yml` | `minio` 서비스에 `profiles: ["minio"]` 추가 |
| `README.md` 빠른 실행 | 2단계에 `echo "JWT_SECRET=$(openssl rand -base64 32)" >> .env` 추가. 3단계 명령·제목, MinIO Health Check, 종료 명령에서 minio 제거. 경로 설명을 Local Filesystem으로 수정 |
| `README.md` 개요·기술 스택·환경 선택 | 기본 경로가 MinIO라고 설명하던 문장을 Local Filesystem 기본값, 선택형 MinIO로 정합화 |
| `README.md` 라이선스 표 | MinIO 행을 "선택형 Profile, 기본 설정에서는 기동하지 않음"과 2026-10-15 확인 결과로 수정 |

### 대안과 선택 이유

| 대안 | 판단 |
|---|---|
| MinIO 이미지를 릴리스 태그로 고정 | 공식 저장소 자체를 받을 수 없어 태그 고정으로 해결되지 않는다(#454). |
| 다른 S3 호환 이미지로 교체 | 새 이미지의 라이선스·호환성 검증이 필요하고 평가 직전 범위가 커진다. |
| compose에서 minio 서비스 삭제 | MinIO를 쓰는 팀원 로컬 환경과 MinIO 대상 E2E를 끊는다. Profile로 남기면 기존 사용자는 그대로 쓸 수 있다. |
| `.env.example`에 로컬 전용 `JWT_SECRET` 고정값 | `cp`만으로 실행되지만 저장소에 고정 서명 키가 공개된다. 명령 1줄을 늘리고 값을 로컬에서 생성한다. |
| `application-local.yml`에 `JWT_SECRET` 기본값 | 시크릿을 설정 파일에 하드코딩하지 않는다는 팀 규칙에 어긋난다. |

`JwtProvider`는 `JWT_SECRET`을 UTF-8 바이트 그대로 HMAC 키로 쓰므로 32바이트 이상이어야 한다.
`openssl rand -base64 32`는 44자를 만든다.

### MinIO를 계속 쓰는 경우

이미지를 이미 가진 환경은 `.env`에 `STORAGE_TYPE=minio`를 두고 다음처럼 명시적으로 기동한다.

```bash
docker compose --profile minio up -d minio
```

Compose는 명령줄에서 서비스 이름을 직접 지정하면 해당 Profile을 자동 활성화한다. 검증 VM에서
`docker compose --dry-run up -d --wait postgres minio valkey`가 minio Pull을 시도하는 것으로 확인했다.
README 「테스트」의 MinIO E2E 명령은 수정 없이 기존처럼 동작한다.

## 3. 비목표

- 저장소 Adapter 코드와 Spring 설정 변경
- MinIO 대상 개발용 E2E(`storageWorkerE2eTest`, `minioIntegrationTest` 등)의 이미지 수급 문제.
새 PC에서는 이 명령도 minio Pull에서 실패한다.
- 기존 MinIO 저장 파일의 Local Filesystem 이전. 이미 `STORAGE_TYPE=minio`로 쌓은 로컬 데이터는 저장소
설정만 바꾸면 기존 파일을 읽지 못하므로 기존 `.env`를 유지한다. `.env.example` 변경은 기존 `.env`에
영향을 주지 않는다.

## 4. 검증

### 4.1 환경

- 호스트: Apple M5, 10 CPU, 24GB, macOS. Java 17.0.18, Node.js 26.5.0
- 새 PC 조건: 기존 Docker Desktop의 이미지·볼륨·Cache를 쓰지 않도록 Lima `docker-rootful` VM(6 CPU,
8GiB, Docker 29.9.1 linux/arm64)을 새로 만들어 빈 Docker 데몬으로 사용했다. npm·Gradle Cache도 빈
디렉터리로 지정했다. README 명령은 바꾸지 않고 `DOCKER_HOST`, `npm_config_cache`, `GRADLE_USER_HOME`
환경 변수만 지정했다.
- 저장소: 별도 디렉터리에 `git clone https://github.com/DocGrid/docgrid.git`(`develop` `f84c732`) 후
이 브랜치 변경을 적용했다.
- 회선: 측정 중 다운로드 속도가 약 2MB/s였다. 모델 다운로드 시간은 회선에 비례한다.

### 4.2 수정 전 재현

빈 데몬에서 `develop`의 README 3단계 명령을 그대로 실행했다.

```text
$ docker compose up -d --build postgres minio valkey embedding-server
Image minio/minio Error pull access denied for minio/minio, repository does not exist or may require 'docker login'
Image pgvector/pgvector:0.8.1-pg17 Interrupted
Image valkey/valkey:9.1-alpine Interrupted
```

3초 만에 exit 1로 끝났고 컨테이너는 하나도 만들어지지 않았다.

### 4.3 수정 후 README 순서대로 실행

| 단계 | 명령·확인 | 결과 | 소요 시간 |
|---|---|---|---|
| 2 | `git clone` | 성공 | 33초 |
| 2 | `cp .env.example .env` 등, `npm --prefix frontend ci` | 성공(빈 npm Cache) | 2분 5초 |
| 2 | `echo "JWT_SECRET=..." >> .env` | 44자 값 생성 | 즉시 |
| 3 | `docker compose up -d --build postgres valkey embedding-server` | exit 0. 이미지 Pull·Embedding Image Build | 7분 7초 |
| 3 | `curl -f http://localhost:8000/health/ready` | `{"status":"ready"}`. BGE-M3 4.59GB 다운로드·로딩 포함 | `up` 시작부터 42분 31초 |
| 4 | `brew install ollama` / `brew services start ollama` | Ollama 0.40.4 | 85초 / 8초 |
| 4 | `ollama pull qwen2.5:7b` | 1차 1,491초 후 DNS 조회 실패로 중단, 재실행 시 이어받아 성공 | 2,110초(재실행분) |
| 4 | `ollama run qwen2.5:7b "안녕"` | 정상 응답, `ollama ps` `100% GPU` | 6초 |
| 5 | `./backend/gradlew -p backend bootRun` | `Started DocgridApplication`, `local` 프로필, Flyway 52개 적용, 인덱싱 Worker 등록 | 빈 Gradle Cache 3분 19초(배포판·의존성·컴파일), 앱 기동 4.92초 |
| 5 | `npm --prefix frontend run dev` | `http://localhost:3000` 200 | 7초 |
| 6 | 회원가입·로그인(UI) | 성공. seed 부서 3개 표시 | - |
| 6 | PDF 업로드(UI, 1쪽 한국어 PDF 22KB) | `UPLOADED`. 원본이 `./data/docgrid/documents/...pdf`에 저장 | - |
| 6 | 인덱싱 | `INDEXED`, Chunk 2개. Embedding Job 생성→완료 10초 | 클릭부터 18초 |
| 6 | AI 검색(UI) | 업로드 문서가 근거 [1], 유사도 81.1% | 즉시 |
| 6 | RAG 답변과 인용 | `SUCCESS`, qwen2.5:7b, 입력 543·출력 291 Token, 지연 13,452ms. 답변 내용이 문서와 일치 | 제출부터 17초 |
| 종료 | `docker compose stop postgres valkey embedding-server`, `brew services stop ollama` | 모두 정상 종료 | - |

`bootRun` 1차 실행은 수정 전 `.env`(`JWT_SECRET` 없음)로 Context 초기화에 실패했다. 2단계 명령을 추가한 뒤
같은 명령으로 기동했다. 2차 기동은 Gradle Cache가 채워진 상태라 대표값으로 쓰지 않는다.

검증 중 다른 도구가 호스트에서 Docker Desktop을 다시 시작해 같은 포트(55432, 8000)를 IPv6로 열었다.
검증 Backend가 VM의 서비스를 사용했는지 다음으로 확인했다.

- VM DB에 검증 계정, 업로드 문서, Embedding Job 1건, Worker 1건, RAG 응답이 있다. Flyway 적용 시각은
검증 Backend 1차 기동 시각(22:39:21)이다.
- VM Embedding Server 로그에 업로드 인덱싱(`/embed/batch`, 22:44:45)과 검색 Query(`/embed`, 22:45:06) 요청이 있다.

### 4.4 남은 관찰

- RAG 답변 끝이 문장 중간에서 잘리고 "답변이 길어 일부 내용이 생략됐을 수 있습니다" 안내가 붙었다.
출력 291 Token으로 `ollama.num-predict` 400과 `generate-deadline` 60초 모두에 못 미쳤다. 원인은 이
이슈에서 조사하지 않았다.
- README 2단계는 "인덱싱 Worker는 기본값으로 꺼져 있습니다"라고 설명하지만 `application-local.yml`은
`${INDEXING_WORKER_ENABLED:true}`이므로 `.env.example`의 `SPRING_PROFILES_ACTIVE=local`에서는 기본으로
켜진다. 실행을 막지는 않아 이 이슈에서 수정하지 않았다.
- `embedding-server`는 모델을 받는 동안 로그가 `Waiting for application startup.`에서 진행 표시 없이 멈춰
있고, `start_period` 900초가 지나면 `unhealthy`로 표시된다. 느린 회선에서는 실패로 오해할 수 있다.
Loading