ONTOLOGICAL DB 문서 원문 보기

아키텍처 개선 포인트

⚠️ 이 문서는 감사 커밋 7d60c82 의 스냅샷이다. 조용히 틀린 답을 내던 다섯 항목(ARCH-01, ARCH-02/CODE-01, CODE-33, CODE-34, PERF-20)은 이후 수정되었다. 현재 상태는 03_backend/12_fixed_correctness.md 를 볼 것.

이 문서가 답하는 질문

  • 지금 아키텍처의 구조적 부담은 무엇인가?
  • 각 부담의 코드 근거는 어디이고, 무엇을 고쳐야 하며, 고치면 무엇이 좋아지고 무엇이 위험해지는가?
  • 무엇부터 해야 하는가?

작성 원칙: 모든 항목은 실제 코드를 읽고 파일:라인 근거를 붙였다. 일반론은 넣지 않았다. 확인하지 못한 것은 "미확인"이라고 적었다. 조사 시점: 2026-08-22, 커밋 7d60c82.


요약표

ID 제목 심각도
ARCH-01 UNION 이 파싱되고 조용히 무시된다 High · fixed
ARCH-02 컴파일 캐시가 스키마 변경에 무효화되지 않는다 High · fixed
ARCH-03 읽기 경로가 DDL을 실행한다 (stable 함수 안에서) High
ARCH-04 compile.rs 1,591줄 단일 파일 Med
ARCH-05 Cypher와 TypeQL이 공통 IR을 공유하지 않는다 Med
ARCH-06 compat/ 와 코어의 양방향 결합 Med
ARCH-07 Bolt 게이트웨이 프로세스 경계의 4가지 비용 Med
ARCH-08 읽기/쓰기 이원화가 만드는 불변식 일원 보관 Med
ARCH-09 에이전트 max_rows 한도가 강제되지 않는다 Med
ARCH-10 확장 업그레이드 경로가 없다 High
ARCH-11 결과 스트리밍이 어디에도 없다 Med
ARCH-12 문서-코드 드리프트 (WITH) Low
ARCH-13 shard 비트가 예약만 되고 죽어 있다 Low
ARCH-14 동작하지 않는 설정 손잡이 3개 Low
ARCH-15 재라벨링이 전량 재계산 + 타입당 1 SQL Med
ARCH-16 og_catalog.rule 이 쓰기 전용, role 카디널리티 미강제 Med
ARCH-17 오류 처리 전략 불일치 (error! / expect / unwrap / .ok()) Med
ARCH-18 Studio에 인증 계층이 없고 임의 SQL 엔드포인트가 있다 High

상세

ID 제목 심각도 근거 (파일:라인) 현상 제안 예상 효과 리스크
ARCH-01 UNION 이 파싱되고 조용히 무시된다 High · fixed 파싱: engine/src/cypher/parser.rs:147,161-168 · AST 필드: engine/src/cypher/ast.rs:210-215 · 소비자 부재: grep -rn "\.union" engine/src/ → 0건 · 컴파일 진입: engine/src/cypher/compile.rs:351-370 는 q.clauses 만 순회 MATCH … RETURN … UNION MATCH … RETURN … 이 오류 없이 첫 분기의 행만 반환한다. README·bolt/README.md 는 "UNION은 실패한다"고 적지만 실제로는 실패하지 않고 부분 답을 준다. 헌법 원칙 VIII과 spec 003 plan.md의 "조용히 잘못 해석하는 것보다 명시적 오류가 낫다"에 정면으로 어긋난다 (1) 즉시: compile_read() 진입부에 if q.union.is_some() { return Err("UNION is not supported yet — write the branches as separate queries and combine them in SQL") } 추가. (2) 후속: build_select() 결과를 UNION [ALL] 로 잇는다. 컬럼 이름/개수 일치 검증과 ORDER BY/LIMIT 의 소속(마지막 분기 vs 전체) 규칙 필요 조용한 오답이 사라진다. og_cypher_check() 를 쓰는 Bolt·에이전트 경로가 사전에 거부할 수 있게 된다 (1)은 리스크 거의 없음 — 현재 "동작"하던 질의가 오류가 되지만 그 동작은 틀린 답이었다. (2)는 reachability_only 판정을 분기별로 다시 정의해야 하고, jsonb 컬럼 이름 정렬 문제가 재등장한다
ARCH-02 컴파일 캐시가 스키마 변경에 무효화되지 않는다 High · fixed 캐시 정의/키: engine/src/cypher/mod.rs:26-31 ((String, String) = graph, query) · 유일한 비우기: mod.rs:60-65 if m.len() > 512 { m.clear() } · 뷰 폐기: engine/src/catalog/labeling.rs:172-182 bump_schema_version → drop_all_views · 뷰 생성은 컴파일 시에만: engine/src/cypher/compile.rs:773 → views.rs:93-97 bump_schema_version() 이 og_data.v_* / ve_* 를 전부 DROP VIEW … CASCADE 하지만 PLAN_CACHE 는 건드리지 않는다. 캐시 히트 시 ensure_view() 를 건너뛰므로, 폐기된 뷰를 참조하는 SQL이 실행된다. 트리거 경로가 넓다: og_add_property(types.rs:599), og_add_role, og_add_rule, 타입 개명, TypeQL define, 그리고 Cypher 쓰기의 프로퍼티 자동 승격(storage/mod.rs:112-119 → og_add_property) 캐시 키에 스키마 버전을 넣는다: (graph, query, schema_version). schema_version 은 og_catalog.schema_version 의 max로 이미 존재하며 og_schema() 가 이미 읽는다(agent/mod.rs:24-30). 조회 비용이 부담되면 백엔드 로컬에 마지막 확인 버전을 두고 값이 바뀌었을 때만 캐시를 비운다 스키마 변경 후 첫 질의가 조용히 실패하거나 낡은 후손 집합으로 실행되는 경로가 닫힌다. 프로퍼티 승격이 잦은 Cypher 워크로드에서 특히 효과가 크다 캐시 히트마다 버전 조회 1회가 추가된다(경량 SPI). 캐시 히트율이 스키마 변경 직후 일시적으로 0이 된다
ARCH-03 읽기 경로가 DDL을 실행한다 High stable 선언: engine/src/cypher/mod.rs:74 #[pg_extern(stable)] fn og_cypher_sql · 그 안의 DDL: engine/src/cypher/views.rs:135 CREATE OR REPLACE VIEW · 쓰기 경로 DDL: engine/src/storage/mod.rs:138-143 ALTER TABLE … TYPE text (서브타입마다 반복) · 읽기 경로 INSERT: engine/src/cypher/mod.rs:122-135 INSERT INTO og_data.og_audit 세 가지가 겹친다. (a) stable 은 "DB를 수정하지 않는다"는 플래너 계약인데 뷰 DDL을 실행한다. (b) 읽기 질의가 감사 INSERT를 한다. (c) 프로퍼티 확장이 데이터 쓰기 도중 ALTER TABLE (ACCESS EXCLUSIVE 락)을 건다. 결과: 읽기 전용 대기 서버에서 처음 보는 라벨의 질의가 성립하지 못한다 — spec 007이 "읽기 복제는 코드 없이 동작한다"고 주장하는 것과 충돌한다 (a) og_cypher_sql 을 volatile 로 내리거나, ensure_view 를 하지 않는 읽기 전용 컴파일 모드를 만든다(뷰가 없으면 인라인 UNION ALL 서브쿼리를 직접 방출). (b) 감사를 옵트인(og_set_setting('audit.enabled','on'))으로 바꾸거나 읽기 전용 트랜잭션에서는 건너뛴다. (c) 확장 DDL의 락 영향을 문서화하고, 실패를 let _ = 로 삼키지 않는다 읽기 대기 서버에서 Cypher가 실제로 동작하게 된다 — 이것은 spec 007 P0의 유일한 실현 수단이다. 플래너 계약이 정직해진다 인라인 UNION ALL 방출은 SQL 길이를 키우고 플랜 캐시 효율을 떨어뜨릴 수 있다. 감사 옵트인은 보안 요구와 충돌할 수 있으므로 07_security/ 와 합의가 필요하다. 대기 서버에서의 현재 실제 동작은 미확인이며 테스트가 선행되어야 한다
ARCH-04 compile.rs 1,591줄 단일 파일 Med engine/src/cypher/compile.rs 전체. 한 impl Compiler 안에 최적화 판단(prefer_reachability 34-78, multiplicity_blind 318-349, blind_expr 82-100), 절 컴파일(compile_match 172, compile_with 382, compile_call 415), 프로젝션(build_core 477, build_select 602, build_tabular 621), 패턴(compile_pattern 647, bind_node 717, join_rel 819), 표현식(expr 1146, binary 1336, func 1415, 함수 60여 개 디스패치)가 모두 있다. 구조체 필드도 7개 누산기 + 2개 모드 플래그(compile.rs:110-128) 변경 반경이 넓다. 예를 들어 OPTIONAL MATCH 술어 배치(248-275)와 홉 조인 순서(676-678)가 같은 파일 안에서 self.from 인덱스를 공유하며 상호작용한다. 새 Cypher 기능(예: UNION)이 이 상호작용 전체를 이해해야 추가 가능하다 4축으로 분할: (1) compile/plan.rs — prefer_reachability, multiplicity_blind, blind_expr 등 순수 판단 함수(SPI 1회 외에는 부작용 없음, 단위 테스트 가능). (2) compile/pattern.rs — compile_pattern / bind_node / join_rel / OptionalScope. (3) compile/project.rs — build_core / build_select / build_tabular / compile_with. (4) compile/expr.rs — expr / binary / type_of / func. Compiler 구조체와 Bind 는 compile/mod.rs 에 남기고 각 파일은 impl Compiler 를 이어 쓴다 각 축이 독립적으로 테스트 가능해진다. 특히 (1)은 SPI 없이 순수 함수로 만들 수 있어 prefer_reachability 의 손익분기 규칙을 회귀 테스트로 고정할 수 있다 — 그 규칙은 이미 두 번 틀린 이력이 있다(compile.rs:20-33, 60-67 주석) impl 분할은 private 필드 접근 때문에 같은 모듈 트리 안이어야 한다. 순수 이동 커밋과 동작 변경 커밋을 반드시 분리해야 리뷰가 가능하다. 기존 git blame 이 흐려진다
ARCH-05 Cypher와 TypeQL이 공통 IR을 공유하지 않는다 Med grep -rn "crate::cypher" engine/src/typeql/ → 0건 · grep -rn "crate::typeql" engine/src/cypher/ → 0건 · 독립 Compiler: cypher/compile.rs:110-128 vs typeql/compile.rs:39-55 · 독립 Bind: cypher/compile.rs:102-108 vs typeql/compile.rs:23-37 · 양쪽 모두 from: Vec<String>, wheres: Vec<String>, n: usize, fresh() 를 독립 구현 두 프런트엔드가 문자열 SQL을 각자 조립한다. 공통 IR이 없으므로 한쪽의 개선이 다른 쪽에 오지 않는다. 실제 격차: og_reach 도달성 재작성, PLAN_CACHE, views::ensure_view 유니온 뷰, 파라미터 타입 힌트 전파가 전부 Cypher 쪽에만 있다. spec 003 README는 "003의 계획 표현은 언어 중립적·분산 확장 가능해야 한다"고 명시하는데(specs/README.md 의존 관계 절), 현재 그런 표현이 존재하지 않는다 최소 침습부터: (1) sql_builder 공용 모듈 — from/wheres/ctes 누산기, fresh() 별칭 생성, quote_ident/sql_str 을 한 곳으로. (2) 가변 길이 홉 컴파일을 공용화 — og_vlp/og_reach 선택 로직을 언어 중립 함수로 빼서 TypeQL 파이프라인도 쓸 수 있게. (3) 장기적으로 LogicalPlan 열거형(Scan/Expand/Filter/Project/Aggregate)을 도입하고 두 파서가 그것으로 내려가게 한다 — spec 007 분산 계획의 전제이기도 하다 최적화가 한 번만 구현된다. TypeQL 깊은 순회가 자동으로 도달성 재작성을 얻는다. spec 007 push-down 계획이 실현 가능해진다 (3)은 큰 리팩터링이고 두 언어의 의미론 차이(TypeQL의 reify, 값 중복 제거 속성, role 특수화)를 IR이 표현해야 한다. 잘못 추상화하면 두 컴파일러 모두 느려지고 읽기 어려워진다. (1)부터 점진적으로 하는 것을 권한다
ARCH-06 compat/ 와 코어의 양방향 결합 Med 코어 → compat: cypher/compile.rs:421 use crate::compat::procs, cypher/mod.rs:165 crate::compat::ddl::run · compat → 코어 내부: compat/ddl.rs:10-11 use crate::cypher::ast::*; use crate::cypher::eval;, compat/procs.rs:47,111,191-193 crate::cypher::compile::sql_str, compat/procs.rs:226 crate::cypher::views::ensure_view · 하드코딩: cypher/compile.rs:1545-1557 가 "genai.vector.encode" 를 코어 함수 디스패치에 직접 박아 넣음 헌법 원칙 VI("코어는 하나, 어댑터는 엣지에")가 코드 의존 방향에서 지켜지지 않는다. Neo4j 호환면을 제거하거나 다른 호환면(예: GQL, AGE 호환)을 추가하려면 코어 컴파일러를 고쳐야 한다. compile.rs:1558-1566 의 "지원 함수 목록" 오류 메시지에도 Neo4j 고유 이름이 섞여 있다 코어에 프로시저/함수 레지스트리 트레이트를 정의하고 compat/ 가 등록하게 뒤집는다. 예: trait ProcProvider { fn plan(&self, name, args, alias) -> Option<Plan>; } 를 코어에 두고, compile_call 은 등록된 provider들을 순회한다. sql_str/quote_ident 는 ARCH-05의 공용 sql_builder 로 옮겨 compat 가 코어 내부를 참조하지 않게 한다. genai.vector.encode 도 레지스트리 항목으로 내린다 호환면 추가·제거가 코어를 건드리지 않는다. 원칙 VI가 문서가 아니라 타입으로 강제된다. compile.rs 도 함께 얇아진다 (ARCH-04 시너지) 트레이트 객체 디스패치가 컴파일 경로에 들어간다(컴파일은 캐시되므로 실행 비용은 아님). compat/ddl.rs 는 Cypher AST를 진짜로 필요로 하므로(Neo4j DDL이 Cypher 문법이다) AST 의존은 남는다 — 그 부분은 정당하며 굳이 없앨 필요 없다
ARCH-07 Bolt 게이트웨이 프로세스 경계의 네 가지 비용 Med ① 왕복 2회: bolt/src/session.rs:283-299 og_cypher_columns 후 og_cypher · ② 전체 물질화: session.rs:291-322 pg.query() → Vec<records> 를 만든 뒤 PULL n 을 그 벡터에서 서빙 · ③ 평문: session.rs:182 cfg.connect(NoTls) + TLS 미종단(bolt/README.md) · ④ 풀 없음: bolt/src/main.rs:69-79 세션당 PG 연결 1개 · ⑤ bookmark 상수: session.rs:218 항상 "ontological:0" 게이트웨이가 의미론을 갖지 않는다는 설계 결정은 옳지만, 프로세스 경계 자체가 비용을 만든다. ②가 특히 크다 — Bolt PULL n 은 원래 페이징 프로토콜인데, 여기서는 이미 전부 메모리에 있다. 큰 결과가 게이트웨이 RSS를 그대로 밀어올린다. ⑤ 때문에 causal consistency 북마크가 의미 없는 값이며, 드라이버가 그것을 신뢰하면 잘못된 안전감을 준다 ① og_cypher_columns 결과를 게이트웨이에 (query → columns) LRU로 캐시(파싱만 하는 immutable 함수이므로 안전). 또는 og_cypher 가 컬럼 순서를 함께 반환하는 표면 추가. ② postgres 크레이트의 포털/커서(query_raw 또는 BEGIN; DECLARE CURSOR)로 PULL n 만큼만 당긴다. ③ 게이트웨이↔︎PG 구간에 TLS 옵션(OG_BOLT_PGSSLMODE)을 추가하고, Bolt 종단 TLS는 프록시 전제를 README보다 강하게 배포 가이드에 못박는다. ⑤ 북마크를 지원하지 않음을 지원 매트릭스에 명시하거나 실제 LSN 기반 값으로 채운다 ①은 RUN당 왕복이 절반이 된다. ②는 큰 결과의 메모리 상한이 생기고 첫 레코드 지연이 줄어든다. ③은 관리형/네트워크 배포가 가능해진다 ②는 명시적 트랜잭션 밖에서 커서를 쓰려면 게이트웨이가 트랜잭션을 열어야 하고, 그러면 "게이트웨이는 상태를 갖지 않는다"는 성질이 약해진다. 현재 자동커밋 질의 의미론과 충돌하지 않도록 설계가 필요하다. ①의 캐시는 og_cypher_columns 가 immutable 이므로 안전하지만, 게이트웨이가 "캐시를 갖지 않는다"는 spec 011 문구와의 정합성을 plan.md에 기록해야 한다
ARCH-08 읽기/쓰기 이원화가 만드는 불변식 일원 보관 Med 설계 선언: engine/src/storage/mod.rs:1-10 · 불변식 유지 코드: storage/mod.rs:429-446(생성), 501-528(삭제), adjacency.rs:19-72 · 읽기 측 가정: engine/sql/access.sql:14-37, compile.rs:900-906 · 사후 검사: storage/stats.rs:172-263 "레지스트리 + 타입 테이블 + 양방향 인접이 언제나 일치한다"는 불변식이 Rust 쓰기 경로에만 존재한다. DB 제약(FK, CHECK, 트리거)으로 강제되지 않으므로, og_data.og_adj 나 타입 테이블에 직접 SQL을 쓰는 경로(수동 복구, ETL, 마이그레이션, 버그)가 불변식을 깰 수 있고 읽기는 그것을 조용히 신뢰한다. og_check_integrity() 는 4종을 검사하지만 사후 진단이고 LIMIT 100 이며 자동 실행되지 않는다 (1) 값싼 제약부터 DB로 내린다: og_data.og_edge(src)/(dst) → og_data.og_node(id) FK, og_adj 에 CHECK (n = coalesce(array_length(nbr,1),0) AND array_length(nbr,1) = array_length(eid,1)). (2) og_check_integrity() 를 CI/벤치 하네스의 게이트로 고정(하네스가 이미 무결성 검사를 돌린다 — bench/harness.py). (3) 검사 항목에 "타입 테이블 행 ↔︎ 레지스트리 행" 양방향 대조를 추가(현재 4번 검사는 레지스트리→카탈로그만 본다) 불변식 위반이 쓰기 시점에 즉시 드러난다. 복제 검증(spec 007 P0)의 신뢰도가 올라간다 FK는 쓰기 경로에 인덱스 조회를 추가한다 — 대량 적재 성능에 영향이 있으므로 벌크 경로에서 DEFERRABLE 또는 일시 비활성 전략이 필요하다. CHECK 는 배열 갱신마다 평가되므로 og_adj UPDATE 비용을 측정해야 한다
ARCH-09 에이전트 max_rows 한도가 강제되지 않는다 Med engine/src/agent/mod.rs:437-439 Spi::run(&format!("SET og.max_rows = {rows}")).ok(); · 소비자 부재: grep -rn "max_rows" engine/src/ → 이 두 줄뿐 · GUC 등록 부재: grep -rn "GucRegistry" engine/src/ → 0건 · 대비: 같은 함수의 statement_timeout/work_mem/default_transaction_read_only 는 PostgreSQL이 실제로 강제한다 에이전트 역할에 max_rows 를 선언하면 og_apply_role() 이 성공을 보고하고 {"role":…, "applied": limits} 에 그대로 실어 돌려준다. 그러나 어떤 질의 경로도 그 값을 읽지 않는다. 헌법 원칙 VIII은 "한계가 나쁜 질의를 서버가 멈추기 전에 멈춰야" 한다고 요구한다. 에이전트(그리고 그 에이전트를 신뢰하는 사람)는 강제되는 한도로 읽는다 둘 중 하나. (A) 실제로 강제한다: pgrx::GucRegistry 로 og.max_rows 를 등록하고, og_cypher()/og_typeql() 의 결과 수집 루프에서 초과 시 error! 로 중단(또는 컴파일 시 LIMIT 주입). (B) 강제하지 않음을 명시한다: og_apply_role 의 반환에 "enforced": ["statement_timeout_ms","work_mem_kb","read_only"], "advisory": ["max_rows"] 를 넣고 문서화. (A)를 권한다 — spec 008의 취지가 그것이다 에이전트가 만든 카테시안 곱이 서버를 흔들기 전에 멈춘다. og_estimate() 의 dry-run과 짝을 이룬다 (A)에서 LIMIT 주입은 집계 질의의 의미를 바꿀 수 있으므로 수집 루프 중단이 안전하다. 중단 시 부분 결과를 줄지 오류를 낼지 결정이 필요하다 — 에이전트에게는 오류가 낫다(조용한 절단은 원칙 VIII 위반)
ARCH-10 확장 업그레이드 경로가 없다 High engine/ontological.control default_version = '0.1.0' · engine/Cargo.toml version = "0.1.0" · engine/sql/ 에 파일 2개(bootstrap.sql, access.sql)뿐 · find . -name "ontological--*.sql" → 0건 · 스키마 변경 수단은 engine/src/lib.rs:22-23 의 extension_sql_file!(… bootstrap) / (… finalize) 하나 ALTER EXTENSION ontological UPDATE 가 불가능하다. 지금은 버전이 하나뿐이라 문제가 드러나지 않지만, 첫 스키마 변경이 나가는 순간 기존 설치본은 (a) 확장 삭제 후 재생성(= 데이터 손실) 또는 (b) 손으로 SQL 적용밖에 길이 없다. 헌법 원칙 I이 "기존 테이블·트랜잭션·백업과 공존해야 한다"고 요구하는데, 마이그레이션 불가는 그 공존을 반쪽으로 만든다. ontological_version() 은 CARGO_PKG_VERSION 을 돌려주므로 바이너리 버전과 스키마 버전이 분리되어 있지 않다 (lib.rs:34-37) (1) 0.1.0 을 마지막 "재설치만" 버전으로 고정하고 0.2.0 부터 engine/sql/ontological--0.1.0--0.2.0.sql 규약을 도입한다(pgrx의 #[pg_extern] 스키마 생성과 손으로 쓴 마이그레이션의 결합 방식을 먼저 결정해야 한다 — pgrx schema 기능 조사 필요). (2) og_catalog.setting 에 schema_layout_version 을 두고 부트스트랩이 심게 한다. (3) 업그레이드 회귀 테스트: CREATE EXTENSION 이전버전 → 워크로드 → ALTER EXTENSION UPDATE → 재검증. 헌법 품질 게이트가 이미 CREATE EXTENSION → 워크로드 → pg_dump/restore → 재검증 사이클을 요구하므로 같은 하네스에 붙일 수 있다 운영 배포가 가능해진다. 지금은 "설치는 되지만 업그레이드는 안 되는" 상태이며 이것이 프로덕션 채택의 실질적 차단 요인이다 pgrx의 확장 SQL 생성 방식과 손으로 쓴 마이그레이션 파일의 결합은 까다롭다. pgrx 0.19.2가 이를 어떻게 지원하는지는 미확인이며 조사가 선행되어야 한다. 잘못하면 두 경로가 서로 다른 스키마를 만든다
ARCH-11 결과 스트리밍이 어디에도 없다 Med 엔진: engine/src/cypher/mod.rs:145-152 table.filter_map(…).collect() → Vec<Value>, mod.rs:108 SetOfIterator::new(rows.into_iter()…) · 도달성: engine/src/storage/traverse.rs:118-158 out: Vec<(i64,i32)> 를 다 모은 뒤 TableIterator::new(out) · Bolt: bolt/src/session.rs:291-322 · Studio: portal/server/index.js:182-217 네 계층 모두 전체 결과를 메모리에 물질화한다. SetOfIterator / TableIterator 는 이미 완성된 벡터를 순회할 뿐 진짜 SRF 스트리밍이 아니다. 깊은 순회 문서가 "pgGraph의 바닥은 순회가 아니라 행 출력"이라고 지적하는데(docs/deep-traversal.md), 같은 지적이 이 구현에도 적용된다 — 50,000 노드 도달성은 Vec<(i64,i32)> 800KB + jsonb 변환본이다 (1) og_cypher() 를 Spi::connect 밖으로 결과를 흘려보낼 수 없는 pgrx 제약이 있는지 먼저 확인한다(미확인). 가능하면 SetOfIterator 를 지연 이터레이터로 바꾼다. (2) 불가능하면 og.max_rows(ARCH-09)를 실제 상한으로 쓰고, 초과 시 명시적 오류를 낸다. (3) og_reach 는 레벨별로 산출할 수 있으므로 레벨 단위 청크 반환을 검토한다 큰 결과에서 백엔드/게이트웨이 RSS가 예측 가능해진다. OOM-kill로 postmaster가 전체를 재시작하는 최악 경로가 닫힌다 pgrx의 SRF 수명 규칙과 SPI 커서 수명이 얽힌다. 잘못 구현하면 커서가 트랜잭션보다 오래 살아 크래시한다. (2)를 먼저 하는 것이 안전하다
ARCH-12 문서-코드 드리프트 (WITH) Low 코드: engine/src/cypher/compile.rs:372-408 compile_with() 구현됨 · 낡은 문서 ①: specs/003-cypher-query-engine/plan.md Phasing "P6 WITH 체이닝, UNION — 미착수" 및 Complexity Tracking "WITH 미지원" · 낡은 문서 ②: docs/architecture.md "WITH and UNION are not implemented." · 최신: README.md 상태표는 WITH 를 working으로 표기 같은 저장소가 같은 기능에 대해 세 가지를 말한다. LLM 에이전트가 plan.md 나 architecture.md 를 읽으면 WITH 를 쓰지 않으려 하거나 "지원되지 않는다"고 답한다. 그리고 그 문장이 UNION 과 묶여 있어서 진짜 문제인 ARCH-01이 이미 알려진 것처럼 보인다 (1) docs/architecture.md 의 해당 항목을 "UNION is not implemented — and it currently parses without error, which is worse; see ARCH-01" 로 수정. (2) specs/003-*/plan.md Phasing 표의 P6을 WITH ✅ / UNION 미착수 로 분리하고 Complexity Tracking의 "WITH 미지원" 항목을 제거(또는 "해소됨" 표시). (3) 상태 표기의 단일 진실 원천을 ../00_overview/05_spec_status.md 로 선언하고 나머지는 링크만 LLM과 신규 개발자가 같은 답을 얻는다. UNION 문제의 실제 심각도가 드러난다 plan.md 는 시점 기록의 성격도 있으므로 "완전 삭제"보다 "해소 표시"가 낫다. 어떤 문서가 권위인지 명확히 하지 않으면 드리프트가 재발한다
ARCH-13 shard 비트가 예약만 되고 죽어 있다 Low 인코딩 정의: engine/src/id.rs:1-29 · 유일한 할당자: engine/src/storage/mod.rs:33 id::make_id(0, type_id, local) — 하드코딩된 0 · 호출자 없는 헬퍼: engine/src/id.rs:62-67 with_shard() · 노출된 SQL 함수: id.rs:78-81 og_id_shard() — 항상 0 반환 · 매핑 경로도 0: interop/mod.rs:74-76 og_make_id(0, {tid}, …) · og_catalog.placement 테이블은 bootstrap.sql 에 없음(spec 007 plan.md가 설계로만 언급) 63비트 중 9비트(약 14%)가 항상 0이다. 예약 자체는 옳은 결정이지만(식별자를 나중에 다시 쓰지 않겠다는 것, spec 007 FR-008), 현재 상태에서는 (a) og_id_shard() 가 공개 API인데 언제나 0을 돌려주어 오해를 부르고, (b) with_shard() 가 죽은 코드이며, (c) local_id 36비트 한계(타입당 687억)가 shard 비트를 회수하면 완화될 수 있다는 선택지가 문서화되어 있지 않다 (1) og_id_shard() 문서와 docs/api.md 에 "현재 항상 0을 반환한다 — spec 007 샤딩 미구현" 을 명시. (2) with_shard() 에 #[allow(dead_code)] 와 "spec 007 P1에서 사용 예정" 주석을 붙이거나, 샤딩 착수 전까지 제거. (3) og_check_integrity() 에 "shard != 0 인 식별자가 존재하는가" 검사를 추가 — 있다면 손상이거나 미래 데이터다. (4) ADR로 "shard 9비트를 지금 회수하지 않는 이유"를 99_decisions/ 에 남긴다 예약의 의도가 코드와 문서에 남는다. 나중에 샤딩을 붙일 때 "왜 0인가"를 다시 조사하지 않아도 된다 없음에 가깝다. 다만 (2)에서 코드를 지우면 spec 007 착수 시 다시 써야 한다 — 주석 유지를 권한다
ARCH-14 동작하지 않는 설정 손잡이 3개 Low 심는 곳: engine/sql/bootstrap.sql:256-260 chunk_size, supernode_threshold, inference_max_depth, schema_version · chunk_size 소비자 부재: 실제 값은 Rust 상수 storage/adjacency.rs:15 CHUNK = 256, 보고조차 상수를 읽는다(storage/stats.rs:68,77) · supernode_threshold / inference_max_depth: grep -rn 결과 engine/src/ 에 소비자 0건 카탈로그에 있고 og_set_setting() 으로 바꿀 수 있는 설정 세 개가 아무 효과도 없다. 운영자는 chunk_size 를 512로 바꾸면 세그먼트가 커진다고 기대할 것이다. inference_max_depth 는 ARCH-16(추론 미구현)과 짝을 이루는 흔적이다 (1) 즉시: 세 키에 COMMENT 또는 og_catalog.setting 에 effective bool 컬럼을 추가해 "현재 효과 없음"을 드러낸다. (2) chunk_size 는 실제로 읽게 만들거나(세그먼트 크기를 그래프별로 조정 가능하게) 카탈로그에서 제거한다 — 읽게 만드는 쪽은 기존 세그먼트와의 혼재를 다뤄야 하므로 제거가 더 싸다. (3) supernode_threshold 는 og_degree_distribution() / og_reorganize() 의 판단 기준으로 실제 연결한다 설정 표면이 정직해진다. 운영 문서(08_operations/)가 거짓 손잡이를 소개하지 않게 된다 chunk_size 를 카탈로그에서 제거하면 이미 그 값을 읽는 외부 도구가 있을 수 있다(현재는 확인된 바 없음). og_graph_stats() 출력의 chunk_size 필드는 유지하는 편이 낫다
ARCH-15 재라벨링이 전량 재계산 + 타입당 1 SQL Med 전량 재계산: engine/src/catalog/labeling.rs:117-170 — DELETE FROM og_catalog.type_label WHERE graph_id = $1 후 DFS 재배분 · 행별 INSERT: labeling.rs:160-167 루프 안에서 Spi::run_with_args("INSERT INTO og_catalog.type_label …") · 존재하지 않는 최적화 경로: labeling.rs:112-116 주석이 [insert_between] 을 "흔한 경우를 처리한다"고 말하지만 grep -rn "insert_between" engine/src/ → 주석 1건 외 0건 · DAG 로드도 2회 SPI 스캔(labeling.rs:32-71) (a) GAP = 1024 간격 설계의 이점(중간 삽입이 재번호를 피함)을 실현하는 코드가 없다 — 어떤 계층 변경이든 그래프 전체 재계산이다. (b) 타입 N개면 SQL N+1문장이다. 1,000타입 온톨로지의 타입 하나 추가가 1,001문장 + 전체 뷰 폐기(bump_schema_version)를 유발한다. (c) 주석이 존재하지 않는 함수를 가리켜 독자를 오도한다 (1) 즉시: 주석에서 insert_between 참조를 제거하거나 "계획됨"으로 표시. (2) 배치 INSERT: unnest() 로 한 문장에 전부 넣는다 — INSERT INTO og_catalog.type_label SELECT * FROM unnest($1::int4[], $2::int4[], …). 이것만으로 N+1 → 2문장이 된다. (3) insert_between 을 실제로 구현: 새 타입의 부모 구간 안에 빈 공간이 있으면 그 자리만 채우고 전량 재계산을 건너뛴다. 빈 공간이 없을 때만 fallback. (4) bump_schema_version 이 뷰를 전량 폐기하는 것도 함께 재고 — 영향받는 타입의 조상 뷰만 폐기하면 충분한지 검토 대형 온톨로지의 스키마 변경 지연시간이 크게 줄어든다. (2)만으로도 즉각적 효과가 있고 리스크가 거의 없다 (3)은 구간 배분 불변식을 깨기 쉽다 — 다중 상속(경로당 라벨 행)과 사이클 검출을 함께 만족시켜야 한다. 회귀 테스트 없이 손대면 안 된다. (4)는 조상 관계를 정확히 계산해야 하고, 틀리면 낡은 뷰가 남아 ARCH-02보다 나쁜 상황이 된다
ARCH-16 og_catalog.rule 이 쓰기 전용, role 카디널리티 미강제 Med rule INSERT: catalog/types.rs:675-676 (og_add_rule), adapters/rdf.rs:434-435 (OWL TransitiveProperty 등) · rule SELECT: grep -rn "og_catalog.rule" engine/src/ → INSERT 2건뿐, SELECT 0건 · 카디널리티 저장: catalog/types.rs:643-650, typeql/schema.rs:377-390 · 카디널리티 소비: agent/mod.rs:158 — og_schema() 표시용 하나뿐 · 짝이 되는 죽은 설정: inference_max_depth(ARCH-14) 두 개의 "선언되지만 아무 일도 하지 않는" 스키마 요소. (a) 관계 특성 추론(spec 002 FR-027..FR-030, 헌법 원칙 IV의 제약 강제)이 카탈로그에 기록만 되고 질의에 반영되지 않는다. og_add_rule('kb','KNOWS','symmetric') 은 성공하지만 MATCH (a)-[:KNOWS]->(b) 가 역방향을 찾지 않는다. (b) card_min/card_max 는 og_schema() 를 통해 에이전트에게 제약으로 보고된다 — 에이전트는 그것을 강제되는 규칙으로 읽고 질의를 만든다. 헌법 원칙 VIII("스키마는 정확해야 한다")에 어긋난다 셋 중 하나를 고르고 문서화한다. (A) 강제/적용한다: symmetric/inverse 는 컴파일 타임에 dir 을 'b' 로 바꾸거나 관계 타입 배열을 확장하면 되므로 비교적 싸다. transitive 는 가변 길이 홉으로 확장 — inference_max_depth 가 여기 쓰인다. 카디널리티는 엣지 생성/삭제 시 검사. (B) 낮춘다: og_schema() 출력에서 강제되지 않는 필드를 "advisory": true 로 표시하고, og_add_rule() 이 "저장은 되지만 아직 적용되지 않는다"는 NOTICE 를 낸다. (C) 제거한다: 쓰지 않을 것이면 카탈로그에서 뺀다. 최소한 (B)를 즉시 하고 (A)를 spec 002 후속으로 계획할 것을 권한다 에이전트가 받는 스키마가 정직해진다 — 이것이 원칙 VIII의 핵심이다. (A)를 하면 TypeDB 대비 차별점(제약이 선언되고 강제된다)이 실제가 된다 (A)의 transitive 는 질의 의미를 크게 바꾼다 — 기존 질의 결과가 달라지므로 그래프별 옵트인이어야 한다. 카디널리티 강제는 기존 데이터가 이미 위반하고 있을 수 있어 마이그레이션 경로가 필요하다(ARCH-10과 얽힌다)
ARCH-17 오류 처리 전략 불일치 Med 실측 분포: error!( 115, .expect( 111, .unwrap() 95 (grep -rho … engine/src --include=*.rs | wc -l) · 파일별 편중: catalog/types.rs = .expect( 31 / error!( 18, storage/stats.rs = .expect( 10 / .unwrap() 10 / error!( 0 · 이중 unwrap: catalog/labeling.rs:207,227 r.get::<i32>(1).unwrap().unwrap() · 조용히 삼키는 곳: storage/mod.rs:138-151 확장 DDL let _ =, storage/mod.rs:379 인접 삭제 .ok(), agent/mod.rs:427-438 한도 SET .ok(), cypher/mod.rs:134 감사 .ok() · 헌법 요구: 기술 제약 "elog/ereport 규약을 따른다", 원칙 VIII "오류는 결정적이고 교정 가능해야" 세 가지 실패 방식이 뒤섞여 있고 규칙이 문서화되어 있지 않다. 사용자가 보는 결과가 (a) 의도된 교정 가능 메시지, (b) .expect 문자열, (c) 정보 없는 unwrap panic 셋 중 무엇일지 코드를 읽어야 안다. 더 심각한 것은 정합성에 직결되는 실패를 조용히 삼키는 네 지점이다 — 특히 확장(widening) DDL 실패는 og_catalog.property.data_type 만 text 로 바뀌고 실제 컬럼은 안 바뀌는 상태를 만들 수 있다 (1) 규칙을 정하고 CLAUDE.md 또는 03_backend/ 에 명문화한다: 사용자 입력·환경에 기인한 실패 → error! (교정 제안 포함) / "여기 도달하면 우리 버그" 인 불변식 → .expect("불변식 서술") / .unwrap() 금지(정당한 곳은 .expect 로 사유를 쓴다). (2) 조용히 삼키는 4곳을 재검토: 확장 DDL과 인접 삭제는 실패 시 error! 로 올려 트랜잭션을 되돌리는 것이 옳다. 한도 SET 실패는 og_apply_role 반환에 실패 목록을 담는다. 감사 실패만 .ok() 를 유지하되 pgrx::warning! 을 남긴다. (3) clippy::unwrap_used 를 CI 경고로 켜서 증가를 막는다 오류가 예측 가능해지고, 에이전트가 오류로부터 교정할 수 있는 비율이 올라간다(원칙 VIII의 측정 가능한 목표). 조용한 정합성 손상 경로가 닫힌다 조용히 삼키던 것을 오류로 올리면 지금까지 통과하던 워크로드가 실패할 수 있다 — 특히 확장 DDL은 뷰 의존 때문에 실패하기 쉬운 자리다. 단계적으로: 먼저 warning! 으로 관측하고, 발생 빈도를 확인한 뒤 error! 로 승격
ARCH-18 Studio에 인증 계층이 없고 임의 SQL 엔드포인트가 있다 High 고정 역할 풀: portal/server/index.js:21-29 new Pool({ user: process.env.PGUSER || 'dev', … max: 8 }) · 인증 코드 부재: index.js:344-366 요청 핸들러에 신원 확인 없음 · 임의 SQL: index.js:296-308 await pool.query(sql) · 평문: http.createServer · 대비: Bolt는 HELLO 자격증명으로 PG에 접속해 역할별 권한/RLS/감사를 유지한다(bolt/src/session.rs:168-184) 네 진입면 중 Studio만 "인증은 PostgreSQL 역할 하나뿐"이라는 불변식을 지키지 않는다. 결과: (a) 모든 요청이 한 역할로 실행되어 RLS가 요청자별로 적용되지 않고, (b) og_audit.principal 이 그 고정 역할로 기록되어 감사가 의미를 잃고, (c) POST /api/sql 이 그 역할의 모든 권한을 HTTP로 노출한다. 서버 자체의 설계 의도("얇게. 이 서버가 할 수 있는 것은 psql 세션도 할 수 있다")는 옳지만, 그 논리는 고정 역할이 최소 권한일 때만 성립한다 (1) 즉시: 기본 바인드를 127.0.0.1 로 강제하고, 비-로컬 바인드 시 기동을 거부하거나 인증 설정을 요구한다. README.md 의 Studio 절과 08_operations/ 에 "네트워크 노출 금지"를 명시. (2) 요청별 역할: Authorization 헤더의 자격증명으로 연결을 만드는 방식(Bolt와 동일한 모델)으로 바꾸면 RLS·감사·권한이 자동으로 정합해진다. 풀은 역할별로 갖는다. (3) POST /api/sql 을 기본 비활성(OG_STUDIO_ALLOW_SQL=1 필요)으로. (4) PGUSER 기본값 'dev' 를 제거하고 명시 요구 진입면 공통 불변식(06_protocol_surfaces.md)이 네 곳 모두에서 성립한다. Studio가 데모 이상으로 배포 가능해진다 (2)는 Studio의 UX를 바꾼다(로그인 화면 필요). (3)은 SQL 탭이 이 프로젝트의 핵심 데모 기능("컴파일된 SQL을 보고 그대로 실행")이므로 로컬에서는 기본 활성을 유지해야 한다. 이 항목의 최종 설계는 07_security/ 담당과 합의할 것

우선순위 제안

즉시 (조용한 오답 / 배포 차단)

순위 ID 이유 작업 크기
1 ARCH-01 조용한 오답. 한 줄로 막을 수 있다 매우 작음
2 ARCH-02 조용한 오답 또는 예기치 않은 실패. 캐시 키에 버전 추가 작음
3 ARCH-18 배포 시 즉시 위험. 바인드 제한 + SQL 엔드포인트 게이트 작음
4 ARCH-09 에이전트에게 거짓 보장. 최소한 (B) 명시화 작음
5 ARCH-16 에이전트에게 거짓 스키마. 최소한 (B) 명시화 작음

다음 (프로덕션 전제)

순위 ID 이유
6 ARCH-10 업그레이드 경로 없이는 운영 배포가 불가능하다
7 ARCH-03 spec 007 P0(읽기 복제)의 실현 여부가 여기 달려 있다
8 ARCH-08 불변식을 DB로 내리는 값싼 부분부터
9 ARCH-17 조용히 삼키는 4곳부터 warning! 으로 관측
10 ARCH-11 최소한 상한(ARCH-09와 함께)

구조 개선 (설계 여유가 있을 때)

ARCH-04(파일 분할) → ARCH-06(레지스트리 뒤집기) → ARCH-05(공용 IR) 순서. 셋은 같은 방향의 작업이므로 이 순서로 하면 각 단계가 다음 단계를 쉽게 만든다.

ARCH-15(재라벨링)는 (2) 배치 INSERT만 먼저 해도 효과가 크고 리스크가 없다.

정리 (낮은 비용, 낮은 위험)

ARCH-12(문서 드리프트), ARCH-13(shard 비트), ARCH-14(죽은 설정) — 문서와 주석 작업이다.


이 목록에 없는 것 (범위 밖)

다음은 아키텍처가 아니라 다른 카테고리의 소관이다:

  • Cypher 표면이 raw storage 대비 3홉에서 7.8배 비싼 것 (jsonb 프로젝션 + SPI 왕복) → PERF-
  • genai.token 이 og_catalog.setting 에 평문 저장되는 것 → SEC-
  • og_enable_rls 의 policy_expr 보간 → SEC-
  • Bolt / PG 구간 평문 → SEC- (ARCH-07 ③에서 아키텍처 측면만 다뤘다)
  • web/index.html 1,916줄 단일 파일 → FE-
  • bench/harness.py 1,275줄 → OPS-
  • openCypher TCK 통과율 게이트의 위치 (미확인) → OPS-
  • SHACL 지원 여부 (미확인) → DATA-

Forbidden / Required

Forbidden

  • ❌ 이 문서의 항목을 근거 없이 "해결됨"으로 표시하는 것. 커밋 해시와 함께 표시할 것.
  • ❌ ARCH-01 (2), ARCH-15 (3), ARCH-16 (A) 를 회귀 테스트 없이 착수하는 것 — 세 가지 모두 답이 바뀌는 변경이다.
  • ❌ 여러 ARCH 항목을 한 PR에 묶는 것. 각각 되돌릴 수 있어야 한다.

Required

  • ✅ 각 항목을 착수할 때 99_decisions/ 에 ADR을 남길 것 (배경 / 고려한 대안 / 선택 / 재평가 조건).
  • ✅ 항목이 해소되면 이 표에서 지우지 말고 "해소됨 (커밋 xxxxxxx)" 로 표시할 것 — 왜 그렇게 되어 있었는지가 다음 사람에게 필요하다.
  • ✅ 새 개선 포인트를 추가하면 반드시 파일:라인 근거를 붙일 것. 일반론 금지.
  • ✅ 심각도를 바꾸면 그 이유를 함께 적을 것.
이 페이지는 docs/01_architecture/08_improvements_architecture.md 에서 만들어졌습니다 · python3 web/build_docs.py