데이터 아키텍처 — 소유권 · DB 분리 · 흐름
갱신일 2026-07-26 · 기준: server@bf192cc · ai@60ee59a · web@87e0834
누가 원본을 갖고 데이터가 어느 방향으로 흐르는가만 소유한다. 스키마 필드는 ../contracts/, 요청 순서는 ../interfaces/server-ai.md의 §1 분석(202 + callback)·§2 채팅(SSE passthrough)이 갖는다.
확인한 사실
원본은 하나, 나머지는 파생
| 소유자 | 갖는 것 | 성격 |
|---|
| heymoa-server | 유저·워크스페이스·프로젝트·노트, 전사 세그먼트, 분석 잡 상태와 결과, 채팅 히스토리, 도구 승인 상태, OAuth refresh token, 알림 | 원본. 지우면 복구할 수 없다 |
| heymoa-ai | 분석 결과 캐시, 전사 임베딩, LangGraph checkpoint | 파생. 통째로 날아가도 원본으로 다시 만든다 |
| heymoa-web | 없음 | 영속 저장소를 갖지 않는다. 화면 상태를 무엇이 어떻게 들고 있는지는 ../web/README.md가 갖는다 |
같은 Postgres를 쓰더라도 database를 나누고, cross-database 쿼리를 금지한다. 데이터는 API payload로만 오간다. 그래서 어느 쪽도 상대 테이블을 직접 읽지 않는다.
전사는 흐름마다 반대 방향으로 간다
| 흐름 | 방식 | 왜 |
|---|
| 분석 | push — server가 세그먼트 전체를 요청 payload에 실어 보낸다 | 회의가 끝난 확정 전사라 한 번에 넘기면 된다 |
| 채팅 | pull — ai가 매 턴 내부 조회 API로 당겨간다 | 진행 중 회의라 매 턴 최신이 필요하고, payload 비대를 계약이 막았다 |
pull 경로는 두 개다 — GET /internal/v1/notes/{noteId}/context와 GET /internal/v1/workspaces/{workspaceId}/notes. 둘 다 X-Internal-Token을 요구하고, ai는 네트워크·상태코드·JSON·스키마 실패를 전부 CONTEXT_UNAVAILABLE 하나로 좁힌다(context_client.py).
파생 데이터 셋의 성격이 서로 다르다
| 테이블 | 키 | 지워도 되나 | 만드는 주체 |
|---|
analysis_results | analysis_id (server가 발급한 13자 TSID). 상태 컬럼 없음, 저장은 upsert | 예 — 다시 분석하면 된다 | Alembic 0002 |
transcript_embeddings | (note_id, chunk_index) 유니크. 재적재는 note 단위 DELETE 후 INSERT | 예 — 재분석하면 다시 쌓인다 | Alembic 0003 (HNSW 인덱스를 비었을 때 미리 만든다) |
checkpoint* | thread_id = chatId | 지우면 그 대화의 멀티턴 맥락이 사라진다. server의 표시용 히스토리는 남는다 | LangGraph — Alembic이 배제하고(migrations/env.py의 include_object), 지금은 기동 시 saver.setup()이 만든다 |
분석 그래프에는 checkpoint가 없다. 한 번 돌고 끝나는 실행이라 이어 붙일 상태가 없다.
대화 상태는 소유가 갈라져 있다
| 무엇 | 어디 | 용도 |
|---|
루프용 full state (messages + profile_key) | ai의 checkpoint | 다음 턴에 모델이 볼 것 |
| 표시·히스토리 사본 | server의 agent_chat_message | 화면과 감사. SSE 중계 중 tee한 결과다 |
ai에는 히스토리 조회 API가 없다. 그리고 notes.read 도구를 탄 턴은 노트 전사 전문이 ToolMessage로 checkpoint에 남는다 — 매 턴 조립하는 컨텍스트는 노드 로컬 변수라 저장되지 않지만, 도구 결과는 messages에 들어가므로 예외다.
비밀정보의 자리
| 무엇 | 어디에 있나 | 어디에 없나 |
|---|
| OAuth refresh token | server의 tool_connection(암호화) | ai는 갖지 않는다 |
단기 access token (toolCredentials) | 요청 스코프 메모리뿐 | DB·파일·로그 금지. ai는 LangGraph runtime context로만 넘긴다 — state는 물론 configurable도 안 되는데, 문자열이어도 checkpoint metadata로 복사되기 때문이다 |
| DB 비밀번호 | RDS 관리형 Secret (EC2 IAM Role로 읽음) | 저장소에 없다 |
설계 전제
heymoa_ai가 통째로 사라져도 heymoa 데이터로 재구성할 수 있어야 한다. 이 한 문장이 cross-database 금지와 "파생만 보관"을 강제한다. 재구성이 불가능한 것을 ai에 두기 시작하면 백업·복구 책임이 둘로 늘어난다.
- 잡의 진실은 server가 갖는다. ai의
analysis_results에는 상태 컬럼이 없고 "끝난 것"만 들어온다. 진행 상태는 server의 analysis_job이 본다.
- 멱등성 키는 server가 발급한다. ai는 id를 만들지 않고
analysisId를 그대로 캐시 키로 쓴다. 그래서 server의 재시도가 중복 분석이 되지 않는다.
- 잡 유실을 감수한다. ai의 큐가 인메모리라 프로세스가 죽으면 접수분이 사라지지만, 재시도·워치독이 server 소유이므로 ai에 워커를 두면 같은 일을 두 번 하게 된다. 유실이 실제로 관측되면 그때 별도 워커를 도입한다.
- 임베딩 실패는 분석을 죽이지 않는다. 요약이 본체고 임베딩은 부산물이라 실패를 노드에서 삼킨다. 대신 실패한 노트는 재분석 전까지 검색에 안 잡힌다 — 재적재 경로가 아직 없다.
미확인
| 무엇 | 지금 아는 것 | 어디를 봐야 하나 |
|---|
| 운영에서 두 database가 정말 한 인스턴스인지 | 설계 전제는 "같은 인스턴스, 다른 database"이고, Terraform이 만드는 RDS의 db_name은 heymoa 하나다 | aws-architecture.md의 미확인 표 — heymoa_ai database와 CREATE EXTENSION vector를 누가 만드는지 |
| 임베딩 재적재 경로 | 지금은 없다. 재분석이 유일한 복구 수단이다 | 빠진 노트가 실제로 생기면 이슈로 판다 |
| 전사 길이 상한 | 정하지 않았다. 실제 전사가 몇 토큰인지 측정한 적이 없다 | 컨텍스트 초과가 실제로 나거나 길이를 측정한 뒤 |
| 대화 프롬프트 누적 | 도구 결과를 자르지 않아 대화가 길어질수록 커진다 | Langfuse의 턴별 토큰 계측 |
| 파생 데이터 보존 기간 | 삭제 정책이 없다 | 노트 삭제 시 ai 쪽 임베딩·checkpoint를 지우는 경로가 필요한지 판단 |