모든 태그

# Reverse ETL

2개의 글

관제탑과 대화하고 두 저장소를 잇다. 이모지로 진단을 부르고, 창고가 계산한 평소로 오탐을 지운다

이기종 DBMS 운영 관리 플랫폼 DBTower. 앞부분은 알림에서 진단까지의 왕복을 완성합니다. 회귀·이상·운영 경보가 밋밋한 텍스트에서 구조화된 Discord embed 카드가 됩니다(심각도 색·담당 팀·AI 1차 분석·질문이 미리 채워진 진단 딥링크). 알림에 돋보기 이모지를 달면 봇이 그 인스턴스를 AI로 진단해 답글을 붙이죠. 여기엔 함정이 둘 있었습니다. 왼쪽 돋보기(U+1F50D)와 오른쪽 돋보기(U+1F50E)가 서로 다른 유니코드라는 점, 그리고 웹훅이 쓴 메시지의 embed를 봇이 읽으려면 특권 인텐트가 필요하다는 점입니다. 후자는 발사 시점에 message_id를 인스턴스에 매핑해 권한 0개로 풀었습니다. 대상이 하필 죽어 있어 진단 도구가 전부 빈손이었을 때, AI 답글은 수치를 지어내는 대신 "근본원인을 확정하지 못했습니다"라고 답했습니다. 이번 실측에서 가장 인상적인 대목이었습니다. MCP 정적 토큰은 OAuth 2.1 브라우저 로그인으로 바꿨고(302 대신 401 함정, redirect_uri userinfo 우회), Vault 동적 자격증명으로 유출 창을 TTL 2분으로 줄였습니다. 뒷부분은 lakehouse(장기 분석계)와의 루프를 양방향으로 닫습니다. 받아오는 쪽에서는, lakehouse가 수개월 이력으로 계산한 요일×시간대 베이스라인을 V24 테이블로 받아 14일 창에 충분통계량 복원(Σx=n·m, Σx²=(n−1)s²+n·m²)으로 가중 병합했습니다. 실측 스파이크(psql 3,000회)의 판정이 장기 테이블 내용에 따라 뒤집혀 z=7.42로 발화했고, 관측 수가 101(장기 100+단기 1)로 찍히며 병합이 실제로 작동한 자국이 남았습니다. 내보내는 쪽에서는, 대기 이벤트(V25)와 오브젝트 크기(V26)를 주기 영속하는 잡을 신설하고 plan_snapshot 보존에 48시간 하한을 병행했습니다. 자연어 서빙은 Metabot이 Cloud 전용이라 생긴 갭을 MCP 도구 두 개(장기 마트 SELECT·Metabase 카드 생성)로 메웠고, 카드 76이 실제로 생성되어 bar 차트가 143ms에 렌더됐습니다.

남이 그대로 띄우게 만들고, 두 저장소가 되쓰기로 손잡다

여기까지 "전부 로컬에서 e2e 재현 가능"이라고 여러 번 써 왔는데, 남이 clone하면 아무것도 안 떴습니다. 원천이 없어 인스턴스 0개로 조용히 빈 결과가 나기 때문입니다. 그래서 데모 위성을 떼어내 셀프호스트 어플라이언스로 만들었습니다. docker-compose.standalone.yml과 demo 프로필로 DBTower 없이도 offload부터 게이트, dbt, 발행까지 e2e가 돌고, 격리 실측에서 외부 의존은 0이었습니다. 범용 도구로 넓히지는 않았습니다. DBTower를 셀프호스트하는 사람이 자기 관제 옆에 이 창고를 같이 띄우는 것까지가 목표라 초점을 좁힌 결정입니다. 그다음 두 저장소가 손을 잡았습니다. 원천을 query_snapshot 하나만 내리던 걸 테이블 스펙 레지스트리로 일반화해 백업·플랜·대기 이력까지 편입하는데, 전제가 셋 다 깨졌습니다. wait event는 원천에 영속 테이블이 없고, plan_snapshot은 카운트 기반 보존이라 하루가 닫히기 전 지워질 수 있고, backup_run은 verify가 나중에 UPDATE하는 사후 변이 테이블입니다. 그래서 워터마크·불변성·게이트 프로필을 스펙의 일부로 넣었고, 게이트를 그대로 재면 백업 안 도는 인스턴스가 정상인데도 completeness FAIL로 정상이 차단되는 걸 확인해 프로필로 그 축을 SKIP으로 남겼습니다. 방향이 반대인 일도 하나 했습니다. 장기 dow×hour 베이스라인을 계산해 원천 쪽으로 되쓰는 경로입니다. readonly 봉인을 깨지 않으려 별도 역할에 해당 테이블만 권한을 주고 단일 트랜잭션으로 32,498행 왕복을 실측했으며, 그 역할로 query_snapshot을 읽으면 permission denied가 나는 것까지 확인했습니다.