스키마 설정을 파일과 DB 양쪽에 뒀더니, 결국 어긋나기 시작했습니다
파일에서 시작한 설정 관리
프로젝트 초반에는 설정을 전부 파일로 관리했습니다. ontology.yaml에 온톨로지 스키마를 담고, prompts/ 아래에 문서 유형별 프롬프트를 뒀거든요. 파일 관리가 편했습니다. Git으로 버전 추적되고, 에디터로 바로 열어볼 수 있고, 서버에 올라가는 코드와 같이 배포되니까요.
그런데 운영하다 보니 UI에서 프롬프트를 수정할 일이 생겼습니다. 브라우저에서 직접 프롬프트를 고치고 저장하려면 DB에 넣어야 했거든요. 그래서 Spring Boot 쪽에 prompt_version 테이블을 만들고, UI에서 저장하면 거기 들어가도록 했습니다. 온톨로지 거버넌스 승격도 DB에 쌓이게 했고요(ontology_version 테이블).
여기서 문제가 시작됩니다. AI Server(FastAPI)는 여전히 파일을 읽고 있었거든요.
파일에서 읽는데 DB에서 저장했다
상황을 정리하면 이랬습니다.
Spring Boot UI → DB에 저장 (prompt_version, ontology_version)
AI Server → prompts/rag/system.md, ontology.yaml 파일을 직접 읽음
같은 프롬프트가 두 군데 있는 겁니다. DB에도 있고, 파일에도 있고.
"그럼 둘 다 업데이트하면 되잖아" 싶은데, 실제로는 계속 어긋나거든요. UI에서 저장하면 DB만 바뀝니다. AI Server는 파일을 읽으니까 옛 프롬프트로 추출을 계속해요. 사용자는 "고쳤는데 왜 안 바뀌지?" 하는 상황이 반복됩니다.
반대로 파일만 수정하면 DB에는 옛 버전이 남아서, UI에서 활성 버전을 조회하면 옛날 것이 보여요. 어느 쪽이 진짜인지 매번 헷갈립니다.
서버 환경에서는 문제가 하나 더 있었는데요. 파일 경로가 로컬과 서버에서 달라서, 같은 코드인데 어느 쪽에선 파일을 못 찾는 일도 생기더라고요.

한 정보는 한 곳에서만 관리한다
Single Source of Truth라는 원칙이 있습니다. 한 정보는 한 곳에서만 관리해야 한다는 건데요. 같은 정보를 두 군데에 두면 언젠가 어긋나게 돼있거든요. 저희가 겪은 문제가 정확히 그거였어요.
그럼 어디를 메인으로 할까. 파일이냐 DB냐.
기준 | 파일 | DB |
|---|---|---|
UI에서 수정 | 어려움 | 쉬움 |
Git 버전 관리 | 자연스러움 | 별도 설계 필요 |
배포 일체화 | 가능 | 가능 |
장애 복구 | 즉시 | 백업 필요 |
UI에서 사용자가 직접 수정하고, 버전 이력을 축적해야 하는 상황이었습니다. 그러면 DB를 메인으로 할 수밖에 없어요. 다만 파일을 아예 없애는 건 겁이 났습니다. DB에 장애가 나면 서버 자체가 기동이 안 될 수도 있거든요. 프롬프트는 매 요청마다 읽는 설정이라서요.
그래서 정한 원칙은 이렇습니다.
읽기: DB 조회 → 실패 시 → 파일에서 읽기 (폴백)
쓰기: DB 저장 → 파일에도 동기화
DB가 메인, 파일은 폴백 + 백업. 평상시엔 DB 하나로 운영되지만 장애 상황에서도 서비스가 죽지 않습니다. ontology.yaml이 Single Source of Truth라는 원칙을 글에서 쓴 적이 있는데, 그 "하나의 진실"이 파일에서 DB로 이동한 셈이에요.
테이블 하나와 로더 하나로 합쳤습니다
먼저 테이블을 합쳤습니다. prompt_version과 ontology_version 두 개를 config_version 하나로 통합했어요.
config_version
├── type: ontology / prompt
├── path: ontology.yaml, prompts/rag/system.md 등
├── content: 전체 텍스트
└── is_active: 현재 활성 버전같은 구조(path + content + 활성 플래그)를 공유하는데 굳이 테이블 두 개로 관리할 이유가 없었거든요. type 컬럼으로 구분하면 충분합니다.
그리고 로더를 통합했습니다. AI Server에서 파일 읽던 코드를 전부 한 함수로 모았어요.
# shared/config_loader.py
load_config("ontology.yaml") # DB → 파일 폴백
load_config("prompts/rag/system.md") # DB → 파일 폴백
save_config(path, content, note) # DB 저장 + 파일 동기화기존에 곳곳에 흩어져 있던 with open("prompts/...") as f: 같은 코드가 전부 load_config(...) 한 줄로 바뀝니다. 어떤 설정이든 이 함수 하나만 거치게 만들면, 나중에 폴백 로직을 고치거나 DB 인덱스를 걸 때 한 군데만 손대면 돼요.
이 구조는 직전에 정리했던 임포트 파이프라인의 Canonical Path 원칙과 같은 발상이에요. "모든 경로가 한 관문을 지나게 만든다"는 거요.

Spring Boot는 프록시로
여기서 한 단계 더 정리했습니다. 원래 Spring Boot에 PromptService.java, PromptVersion.java, PromptController.java, PromptVersionRepository.java 같은 프롬프트 도메인 코드가 꽤 있었거든요. 전부 삭제하고, AI Server의 /api/config/* 엔드포인트를 프록시하는 컨트롤러 하나로 대체했습니다.
이유는 단순해요. 프롬프트·온톨로지는 AI 도메인이니까 AI Server에서 관리하는 게 맞고, Spring Boot가 중간에서 비즈니스 로직을 가지면 또 이중 소스가 되거든요. "내가 가진 프롬프트 데이터"와 "AI Server가 가진 프롬프트 데이터"가 따로 돌아가기 시작합니다.
정리한 역할 분리는 이렇습니다.
Spring Boot: 웹 서비스 도메인(대화, 북마크, 알림, 통계) 직접 처리
Spring Boot → AI Server 프록시: AI 도메인(온톨로지, 프롬프트, RAG, 임포트)
한 쪽이 주인이면 그 쪽에만 로직을 둔다는 원칙. 이중 소스를 테이블 레벨에서 해결하고 나니까, 서버 간 역할 분리에서도 같은 원칙을 적용하게 되더라고요.
통합해도 운영 규칙은 나눠야 했습니다
테이블을 합치긴 했는데, 온톨로지와 프롬프트의 운영 방식은 달라야 했어요.
온톨로지 변경은 "새 타입 승격" 같은 명확한 이벤트가 있어서, 문서를 저장할 때 ontology_version_id를 자동으로 기록해둡니다. 나중에 재임포트가 필요한지 판단할 때 쓰려고요. 새 노드 타입이 승격되면 기존 문서를 다시 뽑아야 그 타입이 붙거든요.
프롬프트 변경은 자동 추적을 안 합니다. 프롬프트 변경은 품질 개선이지 스키마 변경이 아니거든요. 문서 유형별로 다른 프롬프트를 쓰는데(엑셀은 classify.md, PPT는 extract_entity.md 식), 어떤 프롬프트가 어떤 문서에 영향을 주는지 매핑하는 것도 복잡하고요.
프롬프트를 수정한 뒤 재임포트가 필요하다고 판단되면, 사용자가 파이프라인에서 수동으로 재임포트합니다. 자동으로 하면 프롬프트 한 번 고칠 때마다 전체 재임포트가 돌아서 비용이 크거든요.
구분 | 역할 | 변경 영향 | 추적 |
|---|---|---|---|
온톨로지 | 무엇을 뽑을지 정의 | 새 타입 → 재임포트 필수 | 자동 |
프롬프트 | 어떻게 뽑을지 지시 | 추출 품질 변화 → 재임포트 선택 | 수동 판단 |
같은 테이블(config_version)에 저장해도, type에 따라 운영 규칙은 분리합니다. 테이블 통합이 곧 운영 통합은 아니거든요.
마무리
파일에서 시작해서 DB로 옮겼고, 파일은 폴백으로 남겼습니다. 테이블은 prompt_version과 ontology_version을 config_version 하나로 합쳤고, 로더도 shared/config_loader.py로 통일했습니다. Spring Boot는 프롬프트·온톨로지에 대해 프록시 역할만 하게 정리했어요.
"설정을 파일에 둘까 DB에 둘까"는 프로젝트 초반에 한 번 정해놓고 끝내는 게 좋습니다. 양쪽 다 지원한다는 선택지가 가장 편해 보이는데, 운영하다 보면 두 소스가 어긋나는 시점이 반드시 오거든요. 어느 쪽을 메인으로 할지 먼저 정하고, 다른 쪽은 복구용 백업으로만 쓰는 구조를 추천합니다.