메타데이터 카탈로그 기반 조회¶
한 줄 요약: 메타데이터(데이터에 대한 설명)는 줄을 직접 거르지 않는다. SQL의 "어느 표에서(FROM), 어떤 값으로(WHERE)"를 정하는 재료다. 실제 거르기는 데이터가 있는 DB가 한다. 이게 되는 건 모든 데이터 표의 열 구조가 같아서, 표마다 다른 몇 가지만 메타데이터로 채우면 되기 때문이다.
📍 만난 상황¶
- 2026-11-09 — 재난 대시보드의 "중간 DB"를 설계하던 중이었다. 시니어가 "매니저 API로는 필터가 안 되니 DB를 하나 더 만들어 메타정보를 따로 보관하라"고 했다. 그 구조를 하루 종일 물었다. 15번 물음 → 그날 일지
- "메타데이터만 채우고 어떻게 필터를 하지?", "ClickHouse를 복제한 다음 거르나?"를 거듭 물었다.
- 17:01 실제로 보낸 SQL의 빈칸이 PostgreSQL 값으로 채워진 모습을 보고 마무리했다.
- 2026-11-10 — 대시보드 코드 전체를 읽히고 동작을 파악하던 중이었다. 09:40 직전에 "PG 메타데이터로 ClickHouse 쿼리를 완성하는 게 맞나?"라고 확인했다. "맞다, PG는 어느 표에 어떤 조건을 붙일지만 정한다"는 답을 들었다. 그래도 "DB를 어떻게 메타데이터로만 쓰지? PG를 조회해서 ClickHouse 쿼리 값으로 쓰는 흐름을 모르겠다"고 다시 물었다. 1번 물음 → 그날 일지
- 달라진 점: 어제는 "메타데이터 DB가 직접 거른다"고 봤다. 오늘은 구조는 스스로 맞혔고, 왜 가능한지와 단계별 흐름을 물었다. 한 단계 나아갔다.
🧩 쉽게 말하면¶
도서관의 목록 카드함을 떠올리면 된다. 카드에는 책 내용이 없다. "이 책은 3층 B서가, 분류는 재난" 같은 설명만 있다.
사서는 카드를 보고 어느 서가로 갈지 정한다. 그다음 서고에 가서 책을 꺼낸다. 카드함이 책을 꺼내 주지는 않는다.
카드함이 없으면 사서가 서가를 전부 돌며 확인해야 한다. 새 책이 올 때마다 위치를 다 외워야 한다.
📖 제대로 알기¶
메타데이터와 카탈로그¶
- 메타데이터: 데이터에 대한 데이터. 표 이름, 기간, 지역 기준 같은 "설명"이다.
- 침수흔적 43,112줄은 데이터다. "침수흔적은
norm_flood_trace표에 있고 2010~2025년 자료다"가 메타데이터다. - 카탈로그: 데이터셋마다 한 줄씩, 무엇이고 어디에 있고 어떻게 거르는지 적은 목록 표다. 대시보드에서는
app.dataset표다.
| 데이터셋 | PG에 있는 것 | ClickHouse에 있는 것 |
|---|---|---|
| 침수흔적 | 1줄(표 이름, 위치 단위, 시간 성격, 보유 기간) | 43,112줄 |
| 전체(그날 7개) | 7줄 + 열 설명 114줄 + 지역 코드 453줄 | 수십만 줄 |
왜 메타데이터만으로 되나: 표 모양이 같다¶
정규화 파이프라인이 원천 데이터를 모두 같은 공통 열을 가진 표로 바꿔 두었다. t_start, t_end, sgg_code, adm_code, h3_r9, row_key 같은 열이다.
그래서 열 이름은 서버 코드(plan.ts)에 고정한다. 표마다 다른 것은 세 가지뿐이다.
- 표 이름(
table_name): 어느 표를 읽나 - 지역 기준(
region_filter): 동 코드(adm_code)까지 채워졌나, 구 코드(sgg_code)만 있나 - 시간 성격과 보유 기간(
time_kind,t_min·t_max): 시간 조건을 붙일지, 언제부터 언제까지 있나
PG는 "SQL 틀의 빈칸을 데이터셋마다 어떻게 채울지" 알려 주는 설정표다.
이 구조를 받치는 세 개념¶
- 공통 키(조인 키): 서로 다른 표에 똑같이 들어 있는 값이다. 구 코드, H3 칸 번호, 시작 시각이 그렇다.
- 룩업(번역 표): 사람이 고른 말을 공통 키 값으로 바꾸는 표다.
app.region_code가 "역삼1동"을1168064000으로 바꾼다. - 푸시다운(pushdown): 거르는 조건을 데이터가 있는 DB로 보내서 거기서 거르게 하는 방식이다.
⚙️ 어떻게 동작하나¶
"역삼1동 · 2022년 8월"을 고른 경우다. 주소는 /?from=2022-08-01&to=2022-08-31®ion=1168064000이 된다.
- 지역 코드 풀기: 서버(
resolveFilter())가app.region_code에서1168064000을 찾는다. "역삼1동, 동 단위, 위 지역11680(강남구)"이 나온다. - 데이터셋 규칙 읽기:
app.dataset에서 표 이름, 지역 기준, 시간 성격, 보유 기간을 읽는다. 데이터 줄은 한 줄도 보지 않는다. - 헛조회 빼기: 보유 기간이 8월과 안 겹치는 데이터셋은 아예 묻지 않는다.
- 빈칸 채우기: 정해 둔 틀에 표 이름과 조건을 끼운다. 동 코드가 있는 표는
adm_code = '1168064000', 구 코드만 있는 표는 위 지역으로 넓혀sgg_code = '11680'이다. - 데이터 DB에서 거르기: 조립한 SQL을 플랫폼 API(dhub2-manager)를 거쳐 SQL 창구(sqlway) → ClickHouse로 보낸다. 걸러진 결과만 돌아온다.
sequenceDiagram
participant B as 브라우저
participant S as 서버 코드
participant M as PG 메타데이터
participant D as ClickHouse
B->>S: 2022-08, 역삼1동
S->>M: 1168064000은 어디?
M-->>S: 역삼1동, 동 단위, 위 지역 11680
S->>M: 데이터셋별 규칙은?
M-->>S: 표 이름, 지역 기준, 시간 성격
Note over S: SQL 틀의 빈칸 채우기
S->>D: manager와 sqlway를 거쳐 SQL 전송
D-->>S: 건수와 행
S-->>B: 지도와 목록
PG와 ClickHouse 사이에는 화살표가 없다. 둘을 잇는 것은 서버 코드가 조립한 SQL뿐이다.
새 데이터셋이 생기면 카탈로그에 한 줄만 추가한다. 표 모양이 같으니 서버 코드는 고치지 않는다.
🧪 예시¶
-
예시 1 · 일상에 빗대어: 택배 상담원의 책상에는 "지점 이름 → 번호" 표와 "물건 종류 → 창고" 표만 있다. 고객이 "강남 지점의 8월 냉장 물건이 몇 개냐"고 묻는다. 상담원은 "강남 = 11680번, 냉장 = 3번 창고"를 알아낸다. 그리고 3번 창고에 "11680번 지점의 8월분만 세어 달라"고 전화한다. 창고가 43,112개 중 679개라고 알려 준다. 모든 창고의 장부 양식이 같아서 이런 전화 한 통이 통한다.
-
예시 2 · 실제로는: 11/09에 실제로 ClickHouse에 보낸 문장이다(강남구 · 2022년 8월).
-- PG에서 읽은 값: table_name = norm_flood_trace, time_kind = year
-- region_code: 강남구 → 11680
SELECT count() AS n
FROM disaster.norm_flood_trace -- ①
WHERE sgg_code = '11680' -- ②
AND t_start <= '2022-08-31 23:59:59' -- ③
AND coalesce(t_end, t_start) >= '2022-08-01 00:00:00'
-- 결과: 679
table_name에서 왔다.
② 열 이름 sgg_code는 코드에 고정돼 있다. 값 11680만 룩업으로 바꿔 넣었다.
③ 기간 조건은 time_kind가 year라서 붙였다. 시간이 없는 데이터에 붙이면 0건이 나온다.
| 데이터셋 | 카탈로그 값 | 서버가 한 일 |
|---|---|---|
| 강우 10분 | 보유 기간 11-05뿐 | 묻지 않음 |
| 지하차도 | static(시간 없음) |
지역 조건만 |
| 지반함몰 | sgg, event |
구 코드 + 기간 |
⚖️ 비교 · 흔한 오해¶
내가 그린 그림 → 실제 그림
flowchart LR
W1["❌ ClickHouse"] -->|"데이터 흐름"| W2["PostgreSQL"]
W2 -->|"걸러진 것"| W3["서버"]
R2[("PostgreSQL<br/>메타데이터")] -->|"① 표 이름·코드"| R1["✅ 서버 코드"]
R1 -->|"② SQL"| R3[("ClickHouse<br/>데이터")]
R3 -->|"③ 걸러진 줄"| R1
위 줄은 데이터가 관을 따라 흐르는 그림이다. 아래 줄이 실제다. 서버 코드가 가운데서 두 DB를 따로 부른다.
오해 1. "메타데이터 DB가 줄을 거른다." "중간 DB"라는 이름이 데이터와 화면 사이에 끼인 층처럼 들린다. 하지만 PG에는 거를 데이터 줄이 없다.
오해 2. "데이터를 복제한 다음 거른다." 복제하지 않는다. 동기화로 가져오는 것도 행 수·열 이름 같은 요약뿐이다.
오해 3. "PG에 쿼리의 모든 재료가 들어 있어야 한다." (11/10) "메타데이터만으로 쿼리를 만든다"고 하니 열 이름까지 PG가 알려 줘야 할 것 같다. 그래서 "어떻게 메타데이터만으로?"가 이해되지 않았다. 실제로는 틀(열 이름, 조건 모양)은 코드에 있고, PG는 표마다 다른 빈칸 세 가지만 채운다. 정규화로 표 모양을 맞춰 둔 덕분이다.
오해 4. "매칭은 저절로 된다."
누군가 룩업 표를 찾아봐야 11680이 나온다. 그 "누군가"가 서버 코드다.
🛠️ 실무에서는¶
- 쓸 때: 데이터셋이 여러 개이고 계속 늘어날 때. 표 모양을 공통 열로 맞출 수 있을 때.
- 피할 때: 데이터셋이 한두 개로 고정이면 설정 파일 하나로 충분하다.
체크리스트 - [ ] 모든 데이터 표에 공통 열(구·동 코드, 칸, 시간)이 있나 - [ ] 카탈로그에 표 이름·지역 기준·시간 성격·보유 기간이 빠짐없이 있나 - [ ] 값을 SQL에 넣을 때 검증을 거치나 - [ ] 보유 기간·행 수 같은 바뀌는 값의 갱신 계획이 있나
문제가 생기면 먼저 볼 것 - 0건이 나올 때: 시간 없는 데이터에 기간 조건이 붙지 않았나. 동 코드가 없는 표에 동 코드로 거르지 않았나. - 데이터셋이 안 보일 때: 보유 기간이 잘못 적혀 헛조회 빼기에 걸리지 않았나.
❓ 자주 묻는 질문¶
- Q. 쿼리를 요청마다 새로 짜나? A. 아니다. 개수 세기, 칸별 묶기, 줄 받기 같은 틀 몇 개를 정해 두고 빈칸만 바꿔 끼운다.
- Q. 표 모양이 다른 데이터셋이 들어오면? A. 그대로는 못 쓴다. 먼저 정규화로 공통 열을 맞춰야 카탈로그에 한 줄 추가로 끝난다.
- Q. 메타데이터는 꼭 별도 DB에 둬야 하나? A. 아니다. 설정 파일에 둬도 된다. 대시보드는 즐겨찾기·수정 제안 같은 쓰기 기능 때문에 PG에 두었다.
🔭 더 깊이 보기¶
- 파라미터 바인딩은 왜 SQL 인젝션을 막나? — 값을 글자로 조립하는 이 구조에서 가장 큰 위험이라서.
- 푸시다운을 받은 DB는 어떻게 빨리 거르나(파티션, 정렬 키)? — 거르기를 넘긴 쪽의 속도가 곧 화면 속도라서.
- 관련 주제: 애플리케이션 서버와 DB의 역할 분담 · OLTP와 OLAP의 차이 · H3 격자 체계와 해상도 · PostgreSQL 데이터베이스와 스키마
📚 참고 자료¶
- AWS Glue란 무엇인가요? (AWS 한국어 문서) — 데이터 카탈로그가 "표 위치·열 정의를 모은 목록"으로 쓰이는 실제 예 · 링크
- SQL Injection Prevention Cheat Sheet (OWASP, 영문) — 값을 SQL에 끼울 때 바인딩을 써야 하는 이유 · 링크
- ClickHouse Docs (영문 공식 문서) — 조건을 받은 데이터 DB가 WHERE·GROUP BY를 실행하는 방식 · 링크
🗓️ 이력¶
- 2026-11-09: 메타데이터만 든 DB가 어떻게 필터를 하는지 15번 물음. 실제 SQL 빈칸이 PostgreSQL 값으로 채워지는 걸 보고 마무리 → 일지
- 2026-11-10: 구조는 맞혔지만 "메타데이터만으로 어떻게?"의 흐름을 다시 물음. 표 모양이 같아 빈칸 세 가지만 채운다는 걸 확인 → 일지