From 9ed45d3cc9d6b63047af2f4737f2485446d7be40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B8=B0=EB=AF=BC?= Date: Fri, 17 Jul 2026 18:15:00 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=EB=8B=B4=EB=8B=B9=20=EC=9D=B4=EC=8A=88?= =?UTF-8?q?=EB=B3=84=20=EC=9E=91=EC=97=85=20=EB=AC=B8=EC=84=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/gimin-#10-embedding-model-basic.md | 98 ++++++++++++ docs/gimin-#15-document-upload-api.md | 144 ++++++++++++++++++ docs/gimin-#25-document-version-upload.md | 36 ++--- ...in-#4-local-db.md => gimin-#3-local-db.md} | 2 +- docs/gimin-#5-mvp-domain-model.md | 114 ++++++++++++++ ...bedding-model-concurrency-verification.md} | 6 +- .../gimin-#25-document-version-upload.md | 6 +- ...-#27-minio-concurrency-integration-test.md | 20 +-- 8 files changed, 391 insertions(+), 35 deletions(-) create mode 100644 docs/gimin-#10-embedding-model-basic.md create mode 100644 docs/gimin-#15-document-upload-api.md rename docs/{gimin-#4-local-db.md => gimin-#3-local-db.md} (98%) create mode 100644 docs/gimin-#5-mvp-domain-model.md rename docs/test-results/{gimin-#10-embedding-model-concurrency-verification.md => gimin-#12-embedding-model-concurrency-verification.md} (97%) diff --git a/docs/gimin-#10-embedding-model-basic.md b/docs/gimin-#10-embedding-model-basic.md new file mode 100644 index 0000000..6b1bbe8 --- /dev/null +++ b/docs/gimin-#10-embedding-model-basic.md @@ -0,0 +1,98 @@ +# Issue #10 기본 임베딩 모델 등록 및 조회 + +## 1. 목적 + +문서 업로드 시 생성되는 `EmbeddingJob`이 사용할 기본 임베딩 모델 메타데이터를 등록하고 조회한다. + +이번 범위는 실제 모델 호출이 아니라 다음 계약을 제공하는 것이다. + +```text +local/test 환경의 기본 모델 Seed +→ 내부 서비스가 활성 모델 Entity 조회 +→ 업로드 시 EmbeddingJob.embedding_model_id 고정 +→ 외부 API는 공개 가능한 모델 정보만 DTO로 반환 +``` + +## 2. 기본 모델 정책 + +기본 모델은 다음 조건을 동시에 만족하는 모델이다. + +```text +is_active = true +is_searchable = true +``` + +두 필드의 역할은 다르다. + +- `is_active`: 새 EmbeddingJob에 사용할 수 있는가 +- `is_searchable`: 해당 모델로 생성한 기존 Vector를 현재 검색에 사용할 수 있는가 + +모델을 교체할 때 기존 모델을 `false + true`로 유지하면 신규 작업에는 사용하지 않으면서 기존 Vector 검색은 계속 허용할 수 있다. + +## 3. DB 제약 + +모델 차원은 양수여야 하며, 활성·검색 가능 모델은 최대 하나만 존재해야 한다. + +```sql +ALTER TABLE embedding_models + ADD CONSTRAINT ck_embedding_models_dimension_positive + CHECK (dimension > 0); + +CREATE UNIQUE INDEX uk_embedding_models_one_active_searchable + ON embedding_models ((1)) + WHERE is_active = TRUE + AND is_searchable = TRUE; +``` + +일반 `UNIQUE(is_active, is_searchable)`는 모든 Boolean 조합을 한 건으로 제한하므로 사용하지 않았다. Partial Unique Index는 `true + true`인 행만 같은 Key로 인덱싱한다. + +## 4. Mock Seed + +local/test Profile에서는 다음 모델을 멱등 Seed로 등록한다. + +| 항목 | 값 | +|---|---| +| Provider | `MOCK` | +| Model name | `mock-bge-m3` | +| Version | `v1` | +| Dimension | `1024` | +| Distance metric | `COSINE` | +| Active | `true` | +| Searchable | `true` | + +Seed는 `db/seed` Flyway Location을 사용하는 local/test에서만 적용되며 운영 Profile에는 적용하지 않는다. + +## 5. 조회 구조 + +내부 도메인 로직과 외부 API의 반환 타입을 분리했다. + +```text +EmbeddingModelQueryService.getActiveModel() +→ EmbeddingJob 생성에 필요한 Entity 반환 + +GET /api/embedding-models/active +→ 외부 공개용 EmbeddingModelResponse 반환 +``` + +업로드 시점에 `EmbeddingJob`이 모델 ID를 저장하므로, 이후 기본 모델이 바뀌어도 이미 접수된 작업이 다른 모델로 실행되지 않는다. + +외부 응답에는 Provider, 모델명, 버전, 차원, 거리 계산 방식 등 필요한 메타데이터만 포함하고 내부 설정과 감사 필드는 노출하지 않는다. + +## 6. 예외 처리 + +- 기본 모델이 없으면 설정 오류로 처리 +- 데이터가 비정상적으로 여러 건이면 중복 설정 오류로 처리 +- 내부 설정 문제와 정상적인 사용자 요청 오류를 구분 + +DB는 최대 한 개를 보장하고, 서비스는 0개인 상태도 명시적으로 처리한다. + +## 7. 검증 + +- Entity의 필수값과 차원 검증 +- Repository의 활성·검색 가능 모델 조회 +- Service의 정상·미설정·중복 결과 처리 +- Controller의 응답 DTO와 오류 응답 +- V27 Migration과 반복 가능한 Seed 적용 +- 애플리케이션 재기동 후 Mock 모델이 한 건으로 유지되는지 확인 + +Partial Unique Index의 실제 동시 트랜잭션 검증과 Benchmark 결과는 [Issue #12 테스트 결과](./test-results/gimin-#12-embedding-model-concurrency-verification.md)에 별도로 정리했다. diff --git a/docs/gimin-#15-document-upload-api.md b/docs/gimin-#15-document-upload-api.md new file mode 100644 index 0000000..7a5d47f --- /dev/null +++ b/docs/gimin-#15-document-upload-api.md @@ -0,0 +1,144 @@ +# Issue #15 문서 업로드 접수 API 설계 및 구현 + +## 1. 목적 + +사용자가 TXT 또는 Markdown 파일을 업로드하면 원본을 MinIO에 저장하고 인덱싱 작업을 접수한다. + +```http +POST /api/documents +Content-Type: multipart/form-data +Authorization: Bearer {token} +``` + +요청 한 번으로 다음 데이터가 생성된다. + +```text +FileObject +Document +DocumentVersion 1 +PENDING EmbeddingJob +``` + +파싱, 청킹, 임베딩 실행과 Vector 저장은 포함하지 않는다. + +## 2. 요청과 인증 + +| 필드 | 설명 | +|---|---| +| `file` | 업로드할 TXT 또는 Markdown 파일 | +| `title` | 논리 문서 제목 | +| `description` | 문서 설명 | +| `visibility` | `PUBLIC` 또는 `PRIVATE` | + +사용자 ID는 요청값으로 받지 않고 인증된 `@CurrentUser`에서 가져온다. + +## 3. 파일 검증 + +MinIO에 저장하기 전에 다음 항목을 검증한다. + +- 빈 파일 여부 +- 최대 파일 크기 +- 허용 확장자 `.txt`, `.md` +- 확장자와 Content-Type 조합 +- 빈 파일명과 최대 길이 +- Path Traversal이 포함된 파일명 + +검증을 통과한 파일은 전체 바이트로 SHA-256을 계산한다. 파일 크기만 같다고 동일 파일로 판단하지 않는다. + +## 4. 논리 문서와 실제 파일 분리 + +```text +Document +→ 사용자가 관리하는 논리 문서 + +DocumentVersion +→ 특정 시점의 문서 내용 + +FileObject +→ MinIO Object의 위치와 해시·크기를 보관하는 메타데이터 +``` + +동일한 파일을 여러 번 새 문서로 올릴 수 있으므로 Document는 요청마다 생성하지만, 실제 파일은 `file_hash + file_size`가 같으면 기존 `FileObject`를 재사용한다. + +## 5. 업로드 처리 흐름 + +```text +파일 검증과 SHA-256 계산 +→ 기존 FileObject 사전 조회 +→ 있으면 MinIO 저장 없이 재사용 +→ 없으면 UUID 기반 Object Key로 MinIO 후보 저장 +→ DB 트랜잭션에서 FileObject 결정 +→ Document와 Version 1 생성 +→ 기본 EmbeddingModel을 연결한 PENDING Job 생성 +→ 첫 Version을 current_version_id로 설정 +``` + +Object Key는 원본 파일명을 직접 사용하지 않고 UUID를 조합한다. + +```text +documents/{directoryUuid}/{objectUuid}.{extension} +``` + +## 6. 동일 파일 동시 업로드 + +사전 조회는 불필요한 저장을 줄이는 최적화일 뿐 동시성 제어가 아니다. 두 요청이 동시에 조회하면 둘 다 FileObject가 없다고 판단할 수 있다. + +DB에는 다음 제약을 둔다. + +```sql +UNIQUE (file_hash, file_size) +``` + +그리고 `INSERT ... ON CONFLICT DO NOTHING`의 영향 행 수로 현재 후보가 채택됐는지 판단한다. + +```text +inserted = 1 +→ 현재 후보 채택 + +inserted = 0 +→ 다른 요청이 만든 FileObject 재조회·재사용 +→ 현재 요청의 미사용 MinIO 후보 삭제 +``` + +따라서 서로 다른 논리 Document 요청은 모두 성공하면서 실제 파일은 하나만 공유할 수 있다. + +## 7. 트랜잭션과 보상 처리 + +MinIO는 PostgreSQL 트랜잭션에 참여하지 않는다. MinIO 저장 후 DB 작업이 실패하면 DB만 Rollback되고 Object는 남을 수 있다. + +Facade는 외부 저장소 경계를 관리하고, Command Service는 DB 원자성을 관리한다. + +```text +Facade +→ 파일 검증, 해시, MinIO 후보 저장, 실패·미채택 후보 삭제 + +DocumentUploadService +→ FileObject, Document, DocumentVersion, EmbeddingJob을 하나의 DB 트랜잭션으로 저장 +``` + +DB 실패 시에는 현재 요청이 새로 저장한 후보만 삭제한다. 이미 다른 Version이 공유하는 기존 Object는 삭제하지 않는다. + +## 8. 생성 상태 + +```text +Document.status = UPLOADED +DocumentVersion.status = UPLOADED +DocumentVersion.version_no = 1 +Document.current_version_id = Version 1 +EmbeddingJob.status = PENDING +``` + +EmbeddingJob에는 접수 시점의 활성 EmbeddingModel ID를 저장한다. + +## 9. 검증 범위 + +- 정상 TXT·Markdown 업로드 +- 잘못된 파일 요청 거절 +- 같은 파일 순차 업로드 시 FileObject 재사용 +- 같은 파일 동시 업로드 시 FileObject 한 건 유지 +- MinIO 실패 시 DB 작업 미실행 +- DB 실패 시 신규 후보 Object 보상 삭제 +- 활성 EmbeddingModel이 없을 때 DB 전체 Rollback +- Controller, 검증, 해시, Facade, Command Service 단위·통합 테스트 + +실제 MinIO에서 경합 패자의 Object가 삭제되는지는 [Issue #27 테스트 결과](./test-results/gimin-#27-minio-concurrency-integration-test.md)에서 추가로 검증한다. diff --git a/docs/gimin-#25-document-version-upload.md b/docs/gimin-#25-document-version-upload.md index 4a1cd36..f30dd08 100644 --- a/docs/gimin-#25-document-version-upload.md +++ b/docs/gimin-#25-document-version-upload.md @@ -1,4 +1,4 @@ -# PR 2.1 문서 새 버전 업로드 설계 +# Issue #25 문서 새 버전 업로드 설계 ## 1. 목적 @@ -15,7 +15,7 @@ current_version_id는 새 버전이 INDEXED될 때까지 기존 버전 유지 이 API에서는 파싱, 청킹, 임베딩을 수행하지 않는다. 새 버전의 원본 파일과 인덱싱 Job만 접수한다. ```text -브랜치명: feature/25 +GitHub Issue: #25 ``` --- @@ -160,14 +160,14 @@ public record DocumentVersionUploadRequest( } ``` -PR 2.1은 파일 내용의 버전만 관리한다. 제목과 설명은 Document 공통 메타데이터이며 별도 메타데이터 수정 API에서 변경한다. +이번 구현은 파일 내용의 버전만 관리한다. 제목과 설명은 Document 공통 메타데이터이며 별도 메타데이터 수정 API에서 변경한다. ```text 새 Version.title_snapshot → Version 접수 시점의 Document.title 복사 Version 처리 중 별도 API로 Document.title 변경 -→ PR 9 완료 처리에서 title_snapshot으로 덮어쓰지 않음 +→ 후속 인덱싱 완료 처리에서 title_snapshot으로 덮어쓰지 않음 ``` `title_snapshot`은 해당 Version이 접수될 당시의 제목을 기록하는 감사·출처용 스냅샷이다. 현재 문서 제목을 교체하기 위한 후보 값으로 사용하지 않는다. 추후 제목이나 설명 자체를 버전 자산으로 관리해야 한다면 메타데이터 revision과 `description_snapshot`을 포함한 별도 설계를 추가한다. @@ -225,7 +225,7 @@ document_versions - created_by ``` -PR 2.1 마이그레이션에서 다음 컬럼을 추가한다. +이슈 #25 마이그레이션에서 다음 컬럼을 추가한다. ```sql ALTER TABLE document_versions @@ -245,7 +245,7 @@ URL / API / MCP Version 기존 데이터는 `file_object_id`가 있는 행만 연결된 `file_objects` 값으로 백필한다. `file_objects.original_filename`과 `content_type`은 기존 호환성을 위해 이번 PR에서는 제거하지 않는다. -PR 2.1 이후에는 두 업로드 경로가 모두 Version 메타데이터를 채워야 한다. +이슈 #25 적용 이후에는 두 업로드 경로가 모두 Version 메타데이터를 채워야 한다. ```text DocumentUploadService @@ -282,7 +282,7 @@ DOCUMENT_VERSION_TYPE_MISMATCH ## 8. 권한 및 보안 정책 -PR 2.1 MVP에서는 문서 소유자만 새 버전을 생성한다. +현재 MVP에서는 문서 소유자만 새 버전을 생성한다. ```text document.owner_user_id = 현재 인증 사용자 @@ -364,7 +364,7 @@ documents.source_type = UPLOAD → 기존 FAILED current_version_id는 새 Version 완료 전까지 유지 ``` -최신 FAILED Version과 같은 파일을 다시 전송하면 새 Version을 만들지 않고 PR 17 수동 재처리를 안내한다. FAILED Version 뒤에 더 최신 Version이 생성되면 과거 FAILED Version은 수동 재처리할 수 없다. +최신 FAILED Version과 같은 파일을 다시 전송하면 새 Version을 만들지 않고 별도의 수동 재처리 기능을 사용한다. FAILED Version 뒤에 더 최신 Version이 생성되면 과거 FAILED Version은 수동 재처리할 수 없다. --- @@ -450,7 +450,7 @@ embedding_jobs.document_version_id = 21 embedding_jobs.status = PENDING ``` -Version 2가 INDEXED되면 PR 9에서: +Version 2가 INDEXED되면 후속 인덱싱 완료 처리에서: ```text documents.current_version_id = 21 @@ -497,7 +497,7 @@ CREATE UNIQUE INDEX uk_document_versions_one_in_progress ### current_version 단조 증가 -PR 9 완료 처리에서는 Document를 Lock하고 버전 번호를 비교한다. +후속 인덱싱 완료 처리에서는 Document를 Lock하고 버전 번호를 비교한다. ```text 완료 Version.version_no > currentVersion.version_no @@ -510,7 +510,7 @@ PR 9 완료 처리에서는 Document를 Lock하고 버전 번호를 비교한다 stale 완료 오류는 `INDEXING_STALE_VERSION_COMPLETION`을 사용한다. Claim Token 검증과 별개로 오래된 완료 요청의 상태 쓰기를 최종 차단한다. -PR 17 수동 재처리에서는 FAILED Version보다 최신 Version이 이미 존재하면 재처리를 거부한다. +후속 수동 재처리 기능에서는 FAILED Version보다 최신 Version이 이미 존재하면 재처리를 거부한다. --- @@ -603,9 +603,9 @@ FileObject 저장·재사용 로직은 신규 문서 업로드와 버전 업로 --- -## 16. 후속 PR 계약 +## 16. 후속 기능 계약 -### PR 3 문서 상태 조회 +### 문서 상태 조회 현재 검색 가능한 버전과 처리 중 버전을 함께 반환한다. @@ -623,7 +623,7 @@ FileObject 저장·재사용 로직은 신규 문서 업로드와 버전 업로 } ``` -### PR 9 인덱싱 완료 +### 인덱싱 완료 - 완료 Version이 현재 Version보다 최신인지 검증한다. - 최신 Version이면 `current_version_id`를 교체한다. @@ -631,16 +631,16 @@ FileObject 저장·재사용 로직은 신규 문서 업로드와 버전 업로 - 오래된 Version 완료가 현재 Version을 덮어쓰지 못하게 한다. - stale 완료 요청은 Version과 Job 상태를 포함해 아무것도 변경하지 않고 409로 거절한다. -### PR 10 실패 및 자동 재시도 +### 실패 및 자동 재시도 - 새 Version 실패 시 기존 current Version을 유지한다. - 재시도 중에도 기존 INDEXED Version을 검색 가능하게 유지한다. -### PR 17 수동 재처리 +### 수동 재처리 - FAILED Version보다 최신 Version이 존재하면 오래된 Version 재처리를 거부한다. -### PR 18 E2E +### E2E 검증 - Version 2 처리 중 Version 1 검색 가능 여부 - Version 2 성공 후 current Version 교체 @@ -702,4 +702,4 @@ FileObject 저장·재사용 로직은 신규 문서 업로드와 버전 업로 - 새 Version마다 별도의 PENDING Job이 생성된다. - DB 실패 또는 동시성 패배 시 이번 요청의 미사용 MinIO 후보만 삭제된다. - FileObject 직접 접근 없이 문서 권한을 거쳐 파일에 접근한다. -- PR 3, 9, 10, 17, 18과의 계약이 문서화된다. +- 문서 상태 조회, 인덱싱 완료, 실패·재시도, 수동 재처리, E2E 검증과의 계약이 문서화된다. diff --git a/docs/gimin-#4-local-db.md b/docs/gimin-#3-local-db.md similarity index 98% rename from docs/gimin-#4-local-db.md rename to docs/gimin-#3-local-db.md index d9ec5fc..4818b84 100644 --- a/docs/gimin-#4-local-db.md +++ b/docs/gimin-#3-local-db.md @@ -1,4 +1,4 @@ -# Local DB +# Issue #3 로컬 DB 개발 환경 구성 이 문서는 로컬 개발 환경에서 OpenSQL-PG 기반 DB 컨테이너를 띄우고 Spring Boot `local` profile로 Flyway migration 실행을 확인하는 최소 가이드입니다. diff --git a/docs/gimin-#5-mvp-domain-model.md b/docs/gimin-#5-mvp-domain-model.md new file mode 100644 index 0000000..cd29d0e --- /dev/null +++ b/docs/gimin-#5-mvp-domain-model.md @@ -0,0 +1,114 @@ +# Issue #5 1단계 MVP 도메인 모델과 Flyway 스키마 설계 + +## 1. 목적 + +문서 업로드부터 권한 기반 검색과 출처 반환까지 이어지는 1단계 MVP의 데이터 구조를 먼저 정의한다. + +```text +사용자·조직 +→ 문서·버전·원본 파일 +→ 컬렉션·권한 +→ 청크·임베딩 작업 +→ 검색·RAG 응답·출처 +→ Worker·장애 복구 +``` + +기능 API보다 Entity와 DB 계약을 먼저 고정해 이후 작업이 같은 관계와 상태 모델을 사용하도록 하는 것이 핵심이다. + +이슈를 처음 등록할 때는 Entity와 Enum만 범위로 잡았지만, 실제 완료된 변경에서는 이 모델을 실행 가능한 DB 스키마로 검증하기 위해 Flyway V2~V25까지 함께 반영했다. 이 문서는 최초 계획이 아니라 최종 병합된 작업 범위를 기준으로 정리한다. + +## 2. 구현 범위 + +- JPA Entity 24종 +- 도메인 Enum 24종 +- Flyway V2~V25 마이그레이션 +- 주요 Unique Constraint와 조회용 Index +- Entity와 마이그레이션의 테이블·제약 이름 일치 +- Flyway 작성 및 의존성 관리 가이드 + +Repository, Service, Controller와 실제 업로드·검색 기능은 포함하지 않았다. + +## 3. 도메인 구성 + +| 도메인 | 주요 Entity | +|---|---| +| 사용자 | `User`, `Department`, `Role`, `UserRole` | +| 문서 | `Document`, `DocumentVersion`, `FileObject`, `DocumentChunk` | +| 컬렉션 | `DocumentCollection`, `CollectionDocument` | +| 권한 | `CollectionPermission`, `DocumentPermission`, `UserDocumentAccessCache` | +| 임베딩 | `EmbeddingModel`, `EmbeddingJob`, `Embedding` | +| 검색·RAG | `SearchQuery`, `SearchResult`, `RagResponse`, `ResponseCitation` | +| Worker·복구 | `WorkerNode`, `EmbeddingJobAttempt`, `IndexingEvent`, `FailoverEvent` | + +`Collection`은 Java 표준 타입과 이름이 충돌하므로 `DocumentCollection`으로 명명했다. + +## 4. 연관관계 원칙 + +연관관계는 단방향 `LAZY ManyToOne`을 기본으로 사용했다. + +```text +@ManyToMany 사용하지 않음 +users ↔ roles → UserRole로 해소 +collections ↔ documents → CollectionDocument로 해소 +``` + +양방향 컬렉션을 두지 않아 Entity 그래프가 불필요하게 커지는 것을 막고, 필요한 조회는 Repository에서 명시적으로 작성하도록 했다. + +모든 Entity는 전체 `@Setter`를 노출하지 않고 `markIndexed()`, `lock()`, `updateHeartbeat()`처럼 의미가 드러나는 상태 변경 메서드를 사용한다. + +## 5. Document와 DocumentVersion 순환 FK + +문서는 현재 검색 가능한 버전을 가리키고, 버전은 소속 문서를 가리킨다. + +```text +documents.current_version_id → document_versions.id +document_versions.document_id → documents.id +``` + +두 테이블을 동시에 만들 수 없으므로 다음 순서로 해결했다. + +```text +V5: documents 생성, current_version_id는 FK 없이 nullable 컬럼으로 생성 +V6: document_versions 생성 +V6: ALTER TABLE로 documents.current_version_id FK 추가 +``` + +새 문서는 최초 저장 과정에서 `currentVersion = null`일 수 있으며, 첫 Version 생성 후 연결한다. + +## 6. Flyway 마이그레이션 순서 + +```text +V2~V4 사용자와 FileObject +V5~V8 Document, Version, Collection +V9~V13 Role과 권한 +V14~V16 EmbeddingModel, Chunk, Embedding +V17~V20 Worker와 인덱싱 이벤트 +V21~V25 검색, RAG, 인용, 장애 복구 +``` + +공유된 기존 마이그레이션은 수정하지 않고 새 변경은 다음 버전 파일로 추가하는 방식을 전제로 한다. + +## 7. DB 제약 설계 + +대표적인 불변식은 DB 제약으로 보호했다. + +- 사용자 이메일과 부서·역할 코드의 단일성 +- 문서별 `version_no` 단일성 +- 저장소의 `bucket_name + object_key` 단일성 +- 컬렉션과 문서 연결 중복 방지 +- 청크 순서와 임베딩 결과 중복 방지 +- 검색 결과와 인용 관계 중복 방지 + +권한 대상은 `USER`, `DEPARTMENT`, `ROLE` 중 하나만 선택돼야 한다. 이 규칙은 JPA만으로 정확히 표현하기 어려워 Flyway의 `CHECK` 제약으로 적용했다. + +## 8. 의도적으로 남긴 후속 과제 + +- Vector 필드는 우선 문자열·TEXT로 매핑하고 실제 OpenSQL Vector 타입 적용을 후속으로 분리 +- JSON 성격 필드는 우선 문자열로 두고 JSON 타입 매핑을 후속 검토 +- `user_document_access_cache`는 권한 원본이 아니라 검색 가속 캐시로 사용 +- ROLE·DEPARTMENT·PUBLIC 권한은 검색 시점의 Live Predicate로 판단 +- 기본 임베딩 모델 단일성은 별도 Partial Unique Index로 보강 + +## 9. 결과 + +이 작업으로 이후 기능이 공통으로 사용할 24개 테이블과 JPA 모델의 기준이 마련됐다. 특히 문서 버전, 파일 재사용, 임베딩 작업 큐, 권한 캐시와 검색 출처 관계를 기능 구현 전에 명시해 후속 API가 임의의 스키마를 만들지 않도록 했다. diff --git a/docs/test-results/gimin-#10-embedding-model-concurrency-verification.md b/docs/test-results/gimin-#12-embedding-model-concurrency-verification.md similarity index 97% rename from docs/test-results/gimin-#10-embedding-model-concurrency-verification.md rename to docs/test-results/gimin-#12-embedding-model-concurrency-verification.md index 88e987b..c8b9974 100644 --- a/docs/test-results/gimin-#10-embedding-model-concurrency-verification.md +++ b/docs/test-results/gimin-#12-embedding-model-concurrency-verification.md @@ -1,4 +1,4 @@ -# 기본 임베딩 모델 단일성 및 동시성 검증 +# Issue #12 기본 임베딩 모델 단일성 및 동시성 검증 ## 1. 문제 배경 @@ -6,7 +6,7 @@ `is_searchable = true`인 행이다. 여러 서버가 동시에 "현재 기본 모델이 없다"고 조회한 뒤 각자 저장하면 애플리케이션 사전 조회만으로는 두 행이 함께 Commit되는 경쟁 조건이 생긴다. -검증 기준 Git 커밋은 `1bf956c`이며, PR 1 머지 커밋 `eecf255`가 포함된 최신 +검증 기준 Git 커밋은 `1bf956c`이며, 기본 임베딩 모델 구현 커밋 `eecf255`가 포함된 `develop`에서 측정했다. ## 2. is_active와 is_searchable을 분리한 이유 @@ -226,7 +226,7 @@ Partial Index: 16,384 bytes ## 15. 해결 전후 비교 -| 항목 | PR 1 기준선 | PR 1-A | +| 항목 | 기능 구현 기준선 | 동시성 검증 추가 후 | |---|---:|---:| | 전체 기본 테스트 | 17 | 26 | | Repository 테스트 | 5 | 5 | diff --git a/docs/test-results/gimin-#25-document-version-upload.md b/docs/test-results/gimin-#25-document-version-upload.md index 14d7733..a1a4477 100644 --- a/docs/test-results/gimin-#25-document-version-upload.md +++ b/docs/test-results/gimin-#25-document-version-upload.md @@ -1,4 +1,4 @@ -# 문서 새 버전 업로드 테스트 결과 +# Issue #25 문서 새 버전 업로드 테스트 결과 ## 1. 테스트 목적 @@ -12,7 +12,7 @@ - 파일명이 달라도 파일 바이트가 같으면 동일 파일로 판정한다. - 파일 크기가 같아도 SHA-256이 다르면 다른 파일로 판정한다. -상세 설계는 [`docs/pr-2.1-document-version-upload.md`](../pr-2.1-document-version-upload.md)를 참고한다. +상세 설계는 [Issue #25 문서 새 버전 업로드 설계](../gimin-#25-document-version-upload.md)를 참고한다. ## 2. 테스트 환경과 제약 @@ -22,7 +22,7 @@ - Swagger UI를 통한 API 호출 - `./gradlew test` 전체 테스트 수행 -PR 2.1은 Version과 PENDING EmbeddingJob을 접수하는 범위까지만 담당한다. 실제 Worker와 인덱싱 완료 처리는 아직 구현되지 않았으므로, 연속 Version 테스트에서는 기존 Version과 Job을 DB에서 `INDEXED`로 변경해 완료 상태를 모의했다. +이슈 #25는 Version과 PENDING EmbeddingJob을 접수하는 범위까지만 담당한다. 실제 Worker와 인덱싱 완료 처리는 아직 구현되지 않았으므로, 연속 Version 테스트에서는 기존 Version과 Job을 DB에서 `INDEXED`로 변경해 완료 상태를 모의했다. 이 수동 변경은 테스트 전용이며 운영 흐름에서는 후속 인덱싱 완료 API가 담당한다. diff --git a/docs/test-results/gimin-#27-minio-concurrency-integration-test.md b/docs/test-results/gimin-#27-minio-concurrency-integration-test.md index b57b344..77818c1 100644 --- a/docs/test-results/gimin-#27-minio-concurrency-integration-test.md +++ b/docs/test-results/gimin-#27-minio-concurrency-integration-test.md @@ -1,10 +1,10 @@ -# PR 2.2 실제 MinIO 업로드 동시성 테스트 결과 +# Issue #27 실제 MinIO 업로드 동시성 테스트 결과 ## 1. 검증 목적 파일 중복 제거 및 후보 Object 보상 삭제가 실제 MinIO에서도 동작하는지 검증했다. -기존 통합 테스트는 실제 OpenSQL/PostgreSQL을 사용했지만 `FileStorageService`는 Mock이었다. 따라서 삭제 메서드가 호출됐다는 사실만 확인할 수 있었고, 경합 패자의 Object가 실제 MinIO에서 사라졌는지는 보장하지 못했다. +기존 통합 테스트는 실제 Tmax OpenSQL 14.6(PostgreSQL 호환)을 사용했지만 `FileStorageService`는 Mock이었다. 따라서 삭제 메서드가 호출됐다는 사실만 확인할 수 있었고, 경합 패자의 Object가 실제 MinIO에서 사라졌는지는 보장하지 못했다. 이번 테스트는 다음 조건을 실제 저장소에서 확인했다. @@ -19,11 +19,11 @@ | 구성요소 | 사용 환경 | |---|---| | 애플리케이션 | Spring Boot test context | -| DB | OpenSQL/PostgreSQL 14.6 | +| DB | Tmax OpenSQL 14.6(PostgreSQL 호환) | | Schema | `docgrid_test` | | Object Storage | 실제 MinIO Docker container | | 테스트 profile | `test`, `minio-integration` | -| 테스트 bucket | 실행별 `docgrid-pr22-{uuid}` | +| 테스트 bucket | 실행별 UUID를 포함한 전용 Bucket | | 동시 실행 | 2개 Executor thread | MinIO 인증값은 코드에 하드코딩하지 않고 `.env`의 환경변수에서 읽었다. 개발용 `docgrid` bucket과 데이터를 분리하기 위해 테스트 전용 bucket을 동적으로 주입했다. @@ -38,7 +38,7 @@ MinIO 인증값은 코드에 하드코딩하지 않고 `.env`의 환경변수에 → 파일 검증 및 SHA-256 계산 → 실제 MinIOStorageService를 감싼 Barrier Wrapper → FileObjectResolutionService -→ 실제 OpenSQL/PostgreSQL transaction +→ 실제 Tmax OpenSQL transaction → 후보 채택 또는 보상 삭제 ``` @@ -216,7 +216,7 @@ POST /api/documents Content-Type: multipart/form-data ``` -PR 2.2 시점에는 Worker와 인덱싱 완료 API가 없으므로 테스트에서 다음 쿼리로 초기 Version의 완료 상태를 모의했다. +테스트 시점에는 Worker와 인덱싱 완료 API가 없으므로 다음 쿼리로 초기 Version의 완료 상태를 모의했다. ```sql UPDATE document_versions @@ -373,7 +373,7 @@ current_version_id는 Version 1 유지 ## 7. Version Facade 보상 분기 단위 테스트 -PR 2.1에서 추가된 Version Facade의 보상 경계도 6개 단위 테스트로 고정했다. +새 버전 업로드에서 추가된 Version Facade의 보상 경계도 6개 단위 테스트로 고정했다. | 상황 | 검증 결과 | |---|---| @@ -456,7 +456,7 @@ MinIO는 다음 순서로 정리했다. ```sql SELECT COUNT(*) FROM docgrid_test.file_objects -WHERE bucket_name LIKE 'docgrid-pr22-%'; +WHERE bucket_name LIKE :testBucketPrefix || '%'; ``` 결과: @@ -480,7 +480,7 @@ WHERE title LIKE '동시 신규 문서 %' 0 ``` -MinIO bucket 목록에서도 `docgrid-pr22-*` bucket이 남아 있지 않았다. +MinIO Bucket 목록에서도 해당 테스트 전용 Prefix를 사용하는 Bucket이 남아 있지 않았다. ## 10. 최종 결론 @@ -496,7 +496,7 @@ MinIO bucket 목록에서도 `docgrid-pr22-*` bucket이 남아 있지 않았다. - 남은 Object의 크기, Content-Type, SHA-256도 DB 값과 일치한다. - 기본 테스트는 MinIO 의존 없이 실행되고 실제 MinIO 검증은 전용 task로 분리된다. -Mock에서 `delete()` 호출을 검증한 것과 실제 Object가 존재하지 않는 것을 검증한 것은 서로 다른 보장이다. PR 2.2에서는 후자의 보장까지 자동화했다. +Mock에서 `delete()` 호출을 검증한 것과 실제 Object가 존재하지 않는 것을 검증한 것은 서로 다른 보장이다. 이번 작업에서는 후자의 보장까지 자동화했다. ## 11. 남아 있는 한계