[{"content":"\rGoogle Colab에서 실습하기\r고객이 이사했다. 과거 주문의 배송지는? jaffle_shop의 고객 한 명이 서울에서 부산으로 이사했다고 하자. customers 테이블에서 city를 \u0026lsquo;부산\u0026rsquo;으로 UPDATE한다. 이제 과거 주문을 조회하면 배송지가 전부 \u0026ldquo;부산\u0026quot;으로 나온다. 실제로는 서울로 배송된 주문인데.\nDW 모델링 2편\r에서 다뤘던 \u0026ldquo;시점 데이터\u0026rdquo; 문제와 같은 구조다. OLTP는 현재 상태만 관리한다. DW는 \u0026ldquo;그 시점에 어떤 값이었는가\u0026quot;를 알아야 한다. 담당자가 바뀌면 과거 실적은 누구 기준으로 볼 것인가. 고객 등급이 바뀌면 과거 주문은 어느 등급으로 집계할 것인가. 같은 문제다.\nKimball이 이걸 체계적으로 정리했다. SCD(Slowly Changing Dimension) — 천천히 변하는 차원. 차원 속성이 바뀌면 어떻게 할 것인가를 유형별로 나눈 체계다. \u0026ldquo;천천히\u0026quot;라는 이름은 팩트 데이터(주문, 로그)처럼 끊임없이 쌓이는 것과 대비해서 붙었다. 고객 주소, 상품 카테고리, 사원 소속 부서. 자주 바뀌지 않지만, 바뀌기는 한다.\nSCD Type 1 - 덮어쓴다 가장 단순한 방법이다. 현재 값으로 UPDATE하고 끝낸다. 과거 이력은 사라진다.\n-- Type 1: 현재 값으로 덮어쓴다 UPDATE dim_customers SET city = \u0026#39;부산\u0026#39; WHERE customer_id = 1; 실행하고 나면 해당 고객의 과거 주문을 조인하든 현재 주문을 조인하든 전부 \u0026ldquo;부산\u0026quot;으로 나온다. 서울에 살던 시절의 정보는 없다.\nType 1이 맞는 경우가 있다. 오타 수정이 대표적이다. \u0026ldquo;서울특별시\u0026quot;를 \u0026ldquo;서울시\u0026quot;로 바꾸는 건 이력을 남길 이유가 없다. 코드 테이블의 설명 문구 변경, 고객 이름의 오타 교정. 과거를 알 필요가 없는 속성에 쓴다.\nSCD Type 2 - 이력을 쌓는다 과거 값을 보존해야 하면 Type 2를 쓴다. 기존 행을 닫고, 새 행을 추가한다.\n테이블에 세 개의 컬럼을 추가한다.\nvalid_from -이 행이 유효하기 시작한 시점 valid_to -이 행이 유효하지 않게 된 시점 (현재 행은 9999-12-31) is_current -현재 유효한 행인지 여부 -- 초기 상태: customer_id = 1, 서울 -- dim_customers_sk | customer_id | city | valid_from | valid_to | is_current -- 1001 | 1 | 서울 | 2025-01-01 | 9999-12-31 | true 고객이 부산으로 이사하면 두 단계를 실행한다.\n-- 1단계: 기존 행을 닫는다 UPDATE dim_customers SET valid_to = \u0026#39;2026-02-15\u0026#39;, is_current = false WHERE customer_id = 1 AND is_current = true; -- 2단계: 새 행을 추가한다 INSERT INTO dim_customers (dim_customers_sk, customer_id, city, valid_from, valid_to, is_current) VALUES (1002, 1, \u0026#39;부산\u0026#39;, \u0026#39;2026-02-15\u0026#39;, \u0026#39;9999-12-31\u0026#39;, true); 이제 한 customer_id에 행이 두 개다.\n-- dim_customers_sk | customer_id | city | valid_from | valid_to | is_current -- 1001 | 1 | 서울 | 2025-01-01 | 2026-02-15 | false -- 1002 | 1 | 부산 | 2026-02-15 | 9999-12-31 | true 여기서 dim_customers_sk 가 서로게이트 키(surrogate key)다. 한 고객에 여러 행이 생기니까 customer_id만으로는 행을 고유하게 식별할 수 없다. 별도 대리 키가 필요한 이유다. 설계 세부 내용은 Gold 편에서 다룬다.\n시점 조회는 이렇게 한다.\n-- 주문 시점의 고객 주소를 조인 SELECT o.order_id, o.order_date, c.city AS city_at_order_time FROM fct_orders o JOIN dim_customers c ON o.customer_id = c.customer_id AND o.order_date BETWEEN c.valid_from AND c.valid_to; 2025년 6월 주문은 \u0026ldquo;서울\u0026rdquo;, 2026년 3월 주문은 \u0026ldquo;부산\u0026rdquo;. 각 주문 시점의 실제 값이 나온다.\nDW 모델링 1편\r에서 \u0026ldquo;SCD Type 2 스토리지 부담이 작아졌다\u0026quot;고 언급했다. 클라우드 Columnar Storage에서는 행이 늘어나는 비용이 온프레미스 대비 훨씬 작다. Type 2를 더 적극적으로 쓸 수 있는 환경이다.\nSCD Type 3 -이전 값을 컬럼으로 남긴다 이력 깊이가 1단계면 충분할 때 쓴다. 별도 컬럼에 직전 값을 저장한다.\n-- Type 3: 이전 값을 컬럼으로 -- customer_id | city | previous_city -- 1 | 부산 | 서울 구현은 단순하다.\nUPDATE dim_customers SET previous_city = city, city = \u0026#39;부산\u0026#39; WHERE customer_id = 1; 행 수가 늘어나지 않는다. 대신 두 단계 전 값은 없다. 서울 → 부산 → 대전으로 바뀌면 \u0026ldquo;서울\u0026quot;은 사라진다.\n쓸 만한 사례가 있다. 조직 개편 전후 비교. \u0026ldquo;이 사원이 이번 개편 전에는 어느 부서였는가\u0026quot;만 알면 되는 경우. 직전 값 하나면 충분하고, 전체 이력은 필요 없다.\n어떤 Type을 고를 것인가 기준 Type 1 Type 2 Type 3 이력 보존 없음 전체 직전 1단계 구현 복잡도 낮음 높음 중간 스토리지 변동 없음 행이 계속 늘어남 컬럼 추가 시점 분석 불가 가능 제한적 적합 속성 오타, 코드 설명 주소, 등급, 소속 조직 개편 전후 판단 기준은 간단하다. \u0026ldquo;과거 시점의 값으로 분석해야 하는가?\u0026rdquo; 그렇다면 Type 2. 아니라면 Type 1. Type 3은 직전 값만 필요한 특수한 경우에 한정된다.\n하나의 테이블 안에서 컬럼별로 Type을 혼합할 수 있다. city는 Type 2로 이력을 쌓고, phone은 Type 1로 덮어쓴다. 전화번호의 과거 이력으로 분석할 일이 없으니까.\n실습 데이터 준비 jaffle_shop의 raw_customers에는 주소 컬럼이 없다. SCD를 시연하려면 임의의 데이터를 생성해야 한다.\nimport duckdb conn = duckdb.connect(\u0026#39;warehouse.duckdb\u0026#39;) # SCD 시연용 고객 데이터: city, membership_grade, updated_at 추가 conn.execute(\u0026#34;\u0026#34;\u0026#34; CREATE SCHEMA IF NOT EXISTS bronze; CREATE OR REPLACE TABLE bronze.customers_v2 AS SELECT id AS customer_id, first_name, last_name, CASE WHEN id % 3 = 0 THEN \u0026#39;서울\u0026#39; WHEN id % 3 = 1 THEN \u0026#39;부산\u0026#39; ELSE \u0026#39;대전\u0026#39; END AS city, CASE WHEN id % 4 = 0 THEN \u0026#39;Gold\u0026#39; WHEN id % 4 = 1 THEN \u0026#39;Silver\u0026#39; WHEN id % 4 = 2 THEN \u0026#39;Bronze\u0026#39; ELSE \u0026#39;Standard\u0026#39; END AS membership_grade, TIMESTAMP \u0026#39;2025-01-15 09:00:00\u0026#39; AS updated_at FROM read_csv_auto( \u0026#39;https://raw.githubusercontent.com/dbt-labs/jaffle_shop/main/seeds/raw_customers.csv\u0026#39; ); \u0026#34;\u0026#34;\u0026#34;) conn.execute(\u0026#34;SELECT * FROM bronze.customers_v2 LIMIT 5\u0026#34;).fetchdf() 변경 시뮬레이션 데이터도 만든다. 고객 몇 명이 이사하고, 등급이 올라간 상황을 시뮬레이션한다.\n# 변경분 데이터: 일부 고객이 이사했다 conn.execute(\u0026#34;\u0026#34;\u0026#34; CREATE OR REPLACE TABLE bronze.customers_v2_updated AS SELECT customer_id, first_name, last_name, CASE WHEN customer_id IN (1, 3, 5) THEN \u0026#39;제주\u0026#39; ELSE city END AS city, CASE WHEN customer_id IN (2, 4) THEN \u0026#39;Gold\u0026#39; ELSE membership_grade END AS membership_grade, CASE WHEN customer_id IN (1, 2, 3, 4, 5) THEN TIMESTAMP \u0026#39;2026-02-20 14:00:00\u0026#39; ELSE updated_at END AS updated_at FROM bronze.customers_v2; \u0026#34;\u0026#34;\u0026#34;) # 변경된 고객 확인 conn.execute(\u0026#34;\u0026#34;\u0026#34; SELECT a.customer_id, a.city AS before_city, b.city AS after_city, a.membership_grade AS before_grade, b.membership_grade AS after_grade FROM bronze.customers_v2 a JOIN bronze.customers_v2_updated b ON a.customer_id = b.customer_id WHERE a.city != b.city OR a.membership_grade != b.membership_grade \u0026#34;\u0026#34;\u0026#34;).fetchdf() dbt snapshot으로 SCD Type 2를 자동화한다 snapshot이란 위에서 Type 2를 SQL로 직접 구현했다. 기존 행을 닫고, 새 행을 넣고, valid_from/valid_to를 관리하고. 테이블이 하나일 때는 할 만하다. 차원 테이블이 10개, 20개로 늘어나면 이 로직을 매번 직접 짜는 건 현실적이지 않다.\ndbt snapshot이 이걸 대신 해준다. snapshot 파일 하나를 정의하면 dbt가 소스 데이터의 변경을 감지하고 valid_from/valid_to 행을 알아서 관리한다.\nsnapshot 파일 작성 import os os.makedirs(\u0026#39;jaffle_shop/snapshots\u0026#39;, exist_ok=True) %%writefile jaffle_shop/snapshots/snap_customers.sql {% snapshot snap_customers %} {{ config( target_schema=\u0026#39;snapshots\u0026#39;, unique_key=\u0026#39;customer_id\u0026#39;, strategy=\u0026#39;timestamp\u0026#39;, updated_at=\u0026#39;updated_at\u0026#39; ) }} select * from bronze.customers_v2 {% endsnapshot %} strategy='timestamp' -updated_at 컬럼을 기준으로 변경 여부를 판단한다. updated_at이 이전 스냅샷 시점보다 새로우면 변경된 것으로 본다.\nunique_key='customer_id' -어떤 행이 같은 행인지 식별하는 키다. 이 키 기준으로 이전 값과 현재 값을 비교한다.\nsnapshot 실행 from dbt.cli.main import dbtRunner # Colab의 ! 쉘 명령은 별도 프로세스를 띄운다. # DuckDB는 프로세스 간 동시 쓰기를 막는 파일 락을 건다. # dbtRunner로 같은 프로세스 안에서 실행하면 락 충돌이 없다. result = dbtRunner().invoke([\u0026#39;snapshot\u0026#39;, \u0026#39;--project-dir\u0026#39;, \u0026#39;jaffle_shop\u0026#39;, \u0026#39;--profiles-dir\u0026#39;, \u0026#39;jaffle_shop\u0026#39;]) 첫 실행이다. 모든 행이 신규이므로 그대로 들어간다. dbt가 자동으로 dbt_valid_from, dbt_valid_to 컬럼을 추가한다.\nconn.execute(\u0026#34;SELECT * FROM snapshots.snap_customers LIMIT 5\u0026#34;).fetchdf() dbt_valid_to가 전부 NULL이다. 현재 유효한 행이라는 뜻이다. dbt snapshot은 9999-12-31 대신 NULL을 쓴다.\n이제 변경 데이터를 투입하고 다시 실행한다.\n# 소스 테이블을 변경분으로 교체 conn.execute(\u0026#34;\u0026#34;\u0026#34; CREATE OR REPLACE TABLE bronze.customers_v2 AS SELECT * FROM bronze.customers_v2_updated; \u0026#34;\u0026#34;\u0026#34;) conn.close() # snapshot 재실행 result = dbtRunner().invoke([\u0026#39;snapshot\u0026#39;, \u0026#39;--project-dir\u0026#39;, \u0026#39;jaffle_shop\u0026#39;, \u0026#39;--profiles-dir\u0026#39;, \u0026#39;jaffle_shop\u0026#39;]) conn = duckdb.connect(\u0026#39;warehouse.duckdb\u0026#39;) # 이력이 생성된 고객 확인 conn.execute(\u0026#34;\u0026#34;\u0026#34; SELECT customer_id, city, membership_grade, dbt_valid_from, dbt_valid_to FROM snapshots.snap_customers WHERE customer_id IN (1, 2, 3) ORDER BY customer_id, dbt_valid_from \u0026#34;\u0026#34;\u0026#34;).fetchdf() customer_id = 1인 고객에 행이 두 개 생겼다. 첫 번째 행의 dbt_valid_to가 채워졌고, 두 번째 행이 현재 유효한 행이다. SQL 한 줄 안 쓰고 Type 2가 구현됐다.\ncheck 전략 updated_at 컬럼이 없는 소스도 있다. 2편\r에서 언급했듯, 데이터를 UPDATE하면서 updated_at을 안 바꾸는 시스템이 의외로 많다.\n이런 경우 check 전략을 쓴다. 지정한 컬럼의 값이 바뀌었는지 직접 비교한다.\n%%writefile jaffle_shop/snapshots/snap_customers_check.sql {% snapshot snap_customers_check %} {{ config( target_schema=\u0026#39;snapshots\u0026#39;, unique_key=\u0026#39;customer_id\u0026#39;, strategy=\u0026#39;check\u0026#39;, check_cols=[\u0026#39;city\u0026#39;, \u0026#39;membership_grade\u0026#39;] ) }} select customer_id, first_name, last_name, city, membership_grade from bronze.customers_v2 {% endsnapshot %} check_cols=['city', 'membership_grade'] -이 두 컬럼의 값이 이전과 다르면 변경으로 판단한다. updated_at이 필요 없다. 대신 매번 전체 행을 비교하므로 데이터가 크면 timestamp 전략보다 느리다.\nsnapshot을 Silver/Gold와 연결한다 snapshot은 Bronze도 Silver도 아닌 별도 스키마(snapshots)에 저장된다. 메달리온 아키텍처에서 이 위치를 정리하면 이렇다.\n소스 → [Bronze] → [Silver] → [Gold] ↑ Bronze → [Snapshot] ──┘ 1편\r에서 정의한 레이어 구조에 snapshot이 추가된 형태다. snapshot은 Bronze 데이터를 직접 바라보고, Silver나 Gold 모델이 snapshot 결과를 참조한다.\nSilver 모델에서 snapshot을 참조하는 구조는 이렇다.\n%%writefile jaffle_shop/models/staging/stg_customers_hist.sql with source as ( select * from {{ ref(\u0026#39;snap_customers\u0026#39;) }} ), cleaned as ( select customer_id, first_name, last_name, city, membership_grade, dbt_valid_from AS valid_from, coalesce(dbt_valid_to, \u0026#39;9999-12-31\u0026#39;::timestamp) AS valid_to, dbt_valid_to IS NULL AS is_current from source ) select * from cleaned dbt의 dbt_valid_from/dbt_valid_to를 valid_from/valid_to로 이름을 바꾸고, NULL을 9999-12-31로 변환했다. Gold에서 BETWEEN 조인을 걸 때 편하다.\nGold 팩트 테이블에서 시점 조인하면 이렇게 된다.\n-- Gold: 주문 시점의 고객 정보로 조인 select o.order_id, o.order_date, c.city AS city_at_order, c.membership_grade AS grade_at_order from stg_orders o join stg_customers_hist c on o.customer_id = c.customer_id and o.order_date \u0026gt;= c.valid_from and o.order_date \u0026lt; c.valid_to SCD 적용 패턴 정리 패턴 적용 대상 구현 방법 Type 1 (덮어쓰기) 오타, 코드 설명, 전화번호 단순 UPDATE Type 2 (이력 추가) 주소, 등급, 소속 부서 dbt snapshot (timestamp / check) Type 3 (이전 값 보존) 조직 개편 전후 비교 previous_ 컬럼 추가 혼합 하나의 테이블 내 컬럼별 구분 Type 1 + Type 2 병행 실무에서는 Type 2가 압도적이다. dbt snapshot이 처리해주니까 구현 부담도 크지 않다. Type 1은 이력이 필요 없는 속성에 한해서, Type 3은 직전 값만 필요한 드문 경우에 쓴다.\n실무 참고: Airflow에서 dbt snapshot 실행 3편\r에서 Airflow DAG로 dbt run → dbt test 순서를 잡았다. snapshot이 추가되면 순서가 바뀐다.\nfrom airflow import DAG from airflow.operators.bash import BashOperator from datetime import datetime with DAG( dag_id=\u0026#39;medallion_with_snapshot\u0026#39;, schedule=\u0026#39;0 6 * * *\u0026#39;, start_date=datetime(2026, 1, 1), catchup=False, ) as dag: # 1. snapshot 먼저 실행 -Bronze의 변경 이력을 캡처 run_snapshot = BashOperator( task_id=\u0026#39;dbt_snapshot\u0026#39;, bash_command=\u0026#39;cd /opt/dbt/jaffle_shop \u0026amp;\u0026amp; dbt snapshot\u0026#39;, ) # 2. Silver 변환 -snapshot 결과를 참조하는 모델이 있으니까 run_staging = BashOperator( task_id=\u0026#39;dbt_run_staging\u0026#39;, bash_command=\u0026#39;cd /opt/dbt/jaffle_shop \u0026amp;\u0026amp; dbt run --select staging\u0026#39;, ) # 3. Gold 변환 run_marts = BashOperator( task_id=\u0026#39;dbt_run_marts\u0026#39;, bash_command=\u0026#39;cd /opt/dbt/jaffle_shop \u0026amp;\u0026amp; dbt run --select marts\u0026#39;, ) run_snapshot \u0026gt;\u0026gt; run_staging \u0026gt;\u0026gt; run_marts 핵심은 run_snapshot \u0026gt;\u0026gt; run_staging 순서다. Silver 모델 중 stg_customers_hist가 snap_customers를 참조한다. snapshot이 먼저 실행되어야 Silver가 최신 이력을 반영한다. snapshot을 Silver 뒤에 돌리면 이번 배치에서 감지된 변경이 다음 배치에서야 Silver에 반영된다. 하루 늦는다.\n다음 글에서는 Gold 레이어를 다룬다. Silver에서 정제한 데이터와 snapshot 이력을 합쳐서 팩트·차원 테이블을 구성하는 과정이다. dbt marts 디렉토리가 본격적으로 등장한다.\nGoogle Colab에서 실습하기\r","permalink":"https://datanexus-kr.github.io/guides/etl-design/004-scd/","summary":"차원 데이터가 바뀌면 과거를 덮어쓸 것인가, 이력을 남길 것인가. SCD Type 1, 2, 3의 차이를 SQL로 직접 구현하고, dbt snapshot으로 프로덕션 패턴을 만든다.","title":"4. SCD - 고객 주소가 바뀌면 과거 주문은 어디로 배송된 걸로 남는가"},{"content":"\rGoogle Colab에서 실습하기\rBronze 데이터를 바로 쓰면 생기는 일 2편\r에서 Bronze에 원본을 있는 그대로 적재했다. 변환 없이. 그 원칙은 맞다. 문제는 Bronze 데이터가 분석에 쓸 수 있는 상태가 아니라는 것이다.\njaffle_shop의 bronze.orders를 보자. order_date 컬럼이 VARCHAR로 들어와 있다. 날짜 함수를 쓸 수 없다. status 컬럼에는 returned, return_pending, completed, placed, shipped가 섞여 있는데, 어느 값이 최종 상태인지 스키마만 봐서는 모른다.\nbronze.payments의 amount 컬럼은 센트 단위 정수다. 달러로 바꾸려면 100으로 나눠야 한다. 이걸 분석할 때마다 매번 나누는 건 실수를 부르는 구조다.\nSilver는 이런 것들을 한 번에 정리하는 레이어다. 타입을 맞추고, 컬럼명을 통일하고, 단위를 변환한다. 비즈니스 로직은 아직 넣지 않는다. \u0026ldquo;분석에 쓸 수 있는 깨끗한 상태\u0026quot;를 만드는 게 Silver의 역할이다.\nSilver에서 하는 일, 안 하는 일 경계를 지키는 게 중요하다. Silver에서 비즈니스 로직을 넣기 시작하면 Bronze와 Silver를 나눈 의미가 사라진다.\nSilver에서 하는 일:\n타입 캐스팅 - VARCHAR를 DATE, INTEGER를 DECIMAL로 컬럼명 표준화 - user_id와 userId를 user_id로 통일 단위 변환 - 센트를 달러로, 밀리초를 초로 중복 제거 - 같은 레코드가 두 번 적재된 경우 NULL 처리 - 빈 문자열을 NULL로 통일 Silver에서 안 하는 일:\nKPI 계산 - 매출, 마진율 같은 비즈니스 지표 테이블 조인 - 주문과 고객을 합쳐서 하나의 뷰로 만드는 것 집계 - GROUP BY로 요약하는 것 조인과 집계는 Gold의 몫이다. Silver는 개별 테이블 단위로 정제만 한다.\ndbt가 필요한 이유 1편\r에서 dbt를 도구로 소개했다. 왜 SQL 파일을 직접 실행하지 않고 dbt를 쓰는가.\nSQL 파일을 하나씩 실행하면 처음엔 문제가 없다. Silver 테이블이 5개, 10개로 늘어나면 상황이 달라진다. 어떤 테이블이 어떤 Bronze 테이블에 의존하는지, 어떤 순서로 실행해야 하는지, 마지막 실행이 언제인지 추적이 안 된다.\ndbt는 이걸 해결한다. SQL 파일 하나가 하나의 모델이다. ref() 함수로 모델 간 의존 관계를 선언하면 dbt가 실행 순서를 알아서 정한다. 변환 로직이 SQL 파일에 남으니 Git으로 이력 추적도 된다.\ndbt 프로젝트 세팅 Colab에서 dbt 프로젝트를 만든다.\n!pip install -q duckdb dbt-core dbt-duckdb import os # dbt 프로젝트 디렉토리 구조 생성 os.makedirs(\u0026#39;jaffle_shop/models/staging\u0026#39;, exist_ok=True) os.makedirs(\u0026#39;jaffle_shop/models/marts\u0026#39;, exist_ok=True) dbt 설정 파일을 만든다. DuckDB를 데이터베이스로 쓰도록 지정한다.\n%%writefile jaffle_shop/dbt_project.yml name: \u0026#39;jaffle_shop\u0026#39; version: \u0026#39;1.0.0\u0026#39; profile: \u0026#39;jaffle_shop\u0026#39; model-paths: [\u0026#34;models\u0026#34;] %%writefile jaffle_shop/profiles.yml jaffle_shop: target: dev outputs: dev: type: duckdb path: /content/warehouse.duckdb Silver 모델 작성 dbt에서는 models/staging/ 디렉토리에 Silver 레이어 모델을 둔다. stg_ 접두어가 staging(=Silver)을 뜻한다.\nstg_orders %%writefile jaffle_shop/models/staging/stg_orders.sql with source as ( select * from bronze.orders ), cleaned as ( select id as order_id, user_id as customer_id, cast(order_date as date) as order_date, status from source ) select * from cleaned Bronze의 id를 order_id로 바꿨다. 여러 테이블을 조인할 때 id만으로는 어느 테이블의 ID인지 알 수 없으니까. user_id도 customer_id로 바꿔서 의미를 명확히 했다. order_date를 DATE로 캐스팅했다.\nstg_customers %%writefile jaffle_shop/models/staging/stg_customers.sql with source as ( select * from bronze.customers ), cleaned as ( select id as customer_id, first_name, last_name from source ) select * from cleaned stg_payments %%writefile jaffle_shop/models/staging/stg_payments.sql with source as ( select * from bronze.payments ), cleaned as ( select id as payment_id, order_id, payment_method, amount / 100.0 as amount_dollars from source ) select * from cleaned amount를 100으로 나눠서 달러 단위로 바꿨다. 컬럼명도 amount_dollars로 변경해서 단위가 뭔지 이름에서 바로 읽힌다.\ndbt 실행 !cd jaffle_shop \u0026amp;\u0026amp; dbt run --select staging.* dbt가 stg_orders, stg_customers, stg_payments 세 모델을 실행한다. 각각 DuckDB에 뷰로 생성된다.\n결과 확인 import duckdb conn = duckdb.connect(\u0026#39;warehouse.duckdb\u0026#39;) # Silver 레이어 확인 conn.execute(\u0026#34;SELECT * FROM stg_orders LIMIT 5\u0026#34;).fetchdf() # 타입 확인 — order_date가 DATE로 바뀌었는가 conn.execute(\u0026#34;\u0026#34;\u0026#34; SELECT column_name, data_type FROM information_schema.columns WHERE table_name = \u0026#39;stg_orders\u0026#39; \u0026#34;\u0026#34;\u0026#34;).fetchdf() # payments의 amount가 달러 단위로 변환되었는가 conn.execute(\u0026#34;SELECT * FROM stg_payments LIMIT 5\u0026#34;).fetchdf() Bronze에서는 VARCHAR였던 order_date가 DATE로 바뀌었다. amount가 센트에서 달러로 변환됐다. 컬럼명이 통일됐다. 이게 Silver다.\nCTE 패턴 위 SQL에서 반복적으로 쓰인 패턴이 있다. with source as (...), cleaned as (...) select * from cleaned. dbt 커뮤니티에서 널리 쓰이는 CTE(Common Table Expression) 패턴이다.\nwith source as ( -- 1단계: Bronze에서 원본을 가져온다 select * from bronze.orders ), cleaned as ( -- 2단계: 정제 로직을 적용한다 select id as order_id, cast(order_date as date) as order_date from source ) -- 3단계: 최종 결과를 반환한다 select * from cleaned source → cleaned → select. 각 단계가 뭘 하는지 이름에서 읽힌다. 정제 로직이 복잡해지면 CTE를 추가하면 된다. renamed, filtered, deduplicated 같은 이름으로 단계를 나누는 팀도 있다.\n중복 제거 패턴 Bronze에 같은 레코드가 두 번 들어오는 경우가 있다. 소스 시스템에서 데이터를 다시 보냈거나, 증분 적재 로직에 버그가 있었거나. Silver에서 이걸 잡아야 한다.\nwith source as ( select * from bronze.orders ), deduplicated as ( select *, row_number() over ( partition by id order by _loaded_at desc ) as row_num from source ), cleaned as ( select id as order_id, user_id as customer_id, cast(order_date as date) as order_date, status from deduplicated where row_num = 1 ) select * from cleaned row_number()로 같은 id가 여러 개 있으면 가장 최근에 적재된 것만 남긴다. 2편\r에서 추가한 _loaded_at 메타데이터 컬럼이 여기서 쓰인다.\nSilver를 함부로 바꾸면 Gold가 깨진다 Gold 모델은 Silver 테이블의 컬럼명, 타입, 단위를 믿고 쓴다. stg_orders의 order_date가 DATE라는 전제로 Gold에서 날짜 함수를 쓰고 있는데, 누군가 Silver에서 컬럼명을 ordered_at으로 바꾸면 Gold 모델이 전부 에러를 뱉는다.\n컬럼을 추가하는 건 괜찮다. 기존 컬럼의 이름이나 타입을 바꾸는 게 위험하다. dbt의 ref() 함수가 의존 관계를 추적하니까 어디가 영향 받는지는 알 수 있다.\n실무 참고: Airflow에서 dbt 실행 Airflow에서 dbt를 실행하는 방법은 여러 가지다. 가장 간단한 건 BashOperator로 dbt run을 호출하는 것이고, 더 정교하게 하려면 cosmos 라이브러리를 쓴다.\nfrom airflow import DAG from airflow.operators.bash import BashOperator from datetime import datetime with DAG( dag_id=\u0026#39;silver_transformation\u0026#39;, schedule=\u0026#39;0 6 * * *\u0026#39;, start_date=datetime(2026, 1, 1), catchup=False, ) as dag: # Bronze 적재 완료를 기다린 뒤 Silver 변환 실행 run_staging = BashOperator( task_id=\u0026#39;dbt_run_staging\u0026#39;, bash_command=\u0026#39;cd /opt/dbt/jaffle_shop \u0026amp;\u0026amp; dbt run --select staging\u0026#39;, ) # dbt test로 Silver 데이터 품질 검증 test_staging = BashOperator( task_id=\u0026#39;dbt_test_staging\u0026#39;, bash_command=\u0026#39;cd /opt/dbt/jaffle_shop \u0026amp;\u0026amp; dbt test --select staging\u0026#39;, ) run_staging \u0026gt;\u0026gt; test_staging dbt run 다음에 dbt test를 건다. Silver 변환이 끝나면 바로 품질 검증을 돌린다. 테스트가 실패하면 Gold 변환으로 넘어가지 않는다. 불량 데이터가 Gold까지 올라가는 걸 막는 구조다.\ncosmos 라이브러리를 쓰면 dbt 모델 하나하나를 Airflow 태스크로 분리할 수 있다. stg_orders가 실패해도 stg_customers는 독립적으로 성공한다. 모델이 수십 개로 늘어나면 이 세분화가 의미 있어진다.\n다음 글에서는 SCD(Slowly Changing Dimension)를 다룬다. 고객의 주소가 바뀌었을 때 과거 주소를 어떻게 보존하는가. Type 1, 2, 3의 차이와 선택 기준.\nGoogle Colab에서 실습하기\r","permalink":"https://datanexus-kr.github.io/guides/etl-design/003-silver-layer/","summary":"Bronze에 쌓아둔 원본 데이터를 정제하고 표준화한다. 타입을 맞추고, 컬럼명을 통일하고, 중복을 제거한다. dbt로 이 과정을 SQL 모델로 정의한다.","title":"3. Silver 레이어 - Bronze를 분석 가능한 상태로 올린다"},{"content":"\rGoogle Colab에서 실습하기\r원본을 건드리면 돌아갈 곳이 없다 1편\r에서 Bronze 레이어의 원칙을 정했다. 소스 시스템에서 가져온 데이터를 변환 없이 저장한다. 타입 캐스팅도 안 하고, 컬럼명도 안 바꾼다.\n원칙은 간단한데 실제로 지키기가 어렵다. \u0026ldquo;날짜 컬럼 타입이 문자열인데 DATE로 바꿔서 넣으면 안 되나?\u0026rdquo; 같은 유혹이 생긴다. 안 된다. Bronze에서 타입을 바꾸면 원본 복원이 불가능해진다. 소스 시스템에서 \u0026quot;2026-02-30\u0026quot; 같은 잘못된 날짜가 넘어왔을 때, DATE로 캐스팅하면 에러가 나거나 NULL로 바뀐다. 원본이 뭐였는지 알 수 없게 된다.\nBronze는 보험이다. Silver 변환 로직에 버그가 있어도, 소스 시스템이 갑자기 스키마를 바꿔도, Bronze에서 다시 시작할 수 있다. 이 보험을 포기하면 문제가 생길 때마다 소스 시스템에서 데이터를 다시 끌어와야 한다. 소스 시스템 담당자가 협조적이라는 보장은 없다.\nFull Load와 Incremental Load Bronze에 데이터를 넣는 방법은 크게 두 가지다.\nFull Load 는 소스 테이블 전체를 매번 가져와서 덮어쓴다. 단순하다. 소스에 있는 그대로가 Bronze에 있으니까 정합성 고민이 없다. 대신 데이터가 커지면 비용이 늘어난다. 주문 테이블이 1억 건인데 하루 신규 주문이 1만 건이라면, 나머지 9,999만 건은 어제와 똑같은 데이터를 매번 다시 가져오는 셈이다.\nIncremental Load 는 마지막 적재 이후에 변경된 데이터만 가져온다. 효율적이다. 1만 건만 가져오면 된다. 대신 복잡하다. \u0026ldquo;마지막 적재 이후\u0026quot;를 어떻게 판단할 건지, 삭제된 데이터는 어떻게 감지할 건지 정해야 한다.\n어떤 걸 쓸지는 테이블 특성에 따라 다르다.\n구분 Full Load Incremental Load 구현 난이도 낮음 높음 네트워크/비용 데이터 크기에 비례 변경분에 비례 삭제 감지 자동 (전체를 덮어쓰니까) 별도 처리 필요 적합한 대상 코드 테이블, 소규모 마스터 대용량 트랜잭션 실무에서는 섞어 쓴다. 코드 테이블이나 상품 마스터처럼 건수가 적은 테이블은 Full Load로 단순하게 가져간다. 주문, 로그, 이벤트처럼 건수가 많은 테이블은 Incremental Load로 변경분만 가져간다.\n증분의 기준을 잡는 법 Incremental Load에서 가장 중요한 건 \u0026ldquo;무엇이 변경되었는가\u0026quot;를 판단하는 기준이다. 흔히 쓰는 방법이 세 가지 있다.\n타임스탬프 기반. 소스 테이블에 updated_at 같은 수정일시 컬럼이 있으면 가장 간단하다. 마지막 적재 시점 이후의 행만 가져온다. 조건이 하나 있다. 소스 시스템이 수정일시를 정직하게 갱신해야 한다. 데이터를 UPDATE하면서 updated_at을 안 바꾸는 시스템이 의외로 많다.\n자동 증가 키 기반. order_id처럼 단조 증가하는 PK가 있으면 마지막으로 가져온 ID 이후의 행만 가져온다. INSERT는 잡히지만 UPDATE는 못 잡는다. 주문번호가 한 번 발행되면 바뀌지 않는 로그성 테이블에 맞다.\nCDC(Change Data Capture). 소스 데이터베이스의 변경 로그를 직접 읽는다. Debezium 같은 도구가 MySQL이나 PostgreSQL의 WAL(Write-Ahead Log)을 캡처해서 INSERT, UPDATE, DELETE를 전부 잡아낸다. 가장 정확하지만 인프라 구성이 따로 필요하다.\n타임스탬프 기반: WHERE updated_at \u0026gt; \u0026#39;마지막 적재 시점\u0026#39; 자동 증가 키: WHERE order_id \u0026gt; 마지막_적재_ID CDC: 데이터베이스 변경 로그 캡처 DuckDB로 두 방식을 직접 비교한다 1편\r에서 세팅한 환경을 이어서 쓴다.\nimport duckdb conn = duckdb.connect(\u0026#39;warehouse.duckdb\u0026#39;) Full Load 시뮬레이션 Full Load는 간단하다. 기존 데이터를 지우고 전체를 다시 넣는다.\n# 소스 데이터가 변경된 상황을 시뮬레이션 # 실제로는 소스 시스템에서 SELECT * 로 전체를 가져온다 conn.execute(\u0026#34;\u0026#34;\u0026#34; -- Full Load: 통째로 교체 CREATE OR REPLACE TABLE bronze.orders AS SELECT * FROM read_csv_auto( \u0026#39;https://raw.githubusercontent.com/dbt-labs/jaffle_shop/main/seeds/raw_orders.csv\u0026#39; ); \u0026#34;\u0026#34;\u0026#34;) print(\u0026#34;Full Load 완료:\u0026#34;, conn.execute(\u0026#34;SELECT count(*) FROM bronze.orders\u0026#34;).fetchone()[0], \u0026#34;건\u0026#34;) CREATE OR REPLACE TABLE이 핵심이다. 매번 테이블을 새로 만든다. 이전 데이터는 사라지고 소스의 현재 상태가 그대로 들어온다.\nIncremental Load 시뮬레이션 Incremental Load는 한 단계가 더 있다. 마지막으로 가져온 지점을 기억해야 한다.\n# 워터마크 테이블: 마지막 적재 지점을 기록 conn.execute(\u0026#34;\u0026#34;\u0026#34; CREATE TABLE IF NOT EXISTS bronze.watermarks ( table_name VARCHAR PRIMARY KEY, last_loaded_id INTEGER, last_loaded_at TIMESTAMP DEFAULT current_timestamp ); \u0026#34;\u0026#34;\u0026#34;) # 현재 워터마크 확인 watermark = conn.execute(\u0026#34;\u0026#34;\u0026#34; SELECT COALESCE(last_loaded_id, 0) FROM bronze.watermarks WHERE table_name = \u0026#39;orders\u0026#39; \u0026#34;\u0026#34;\u0026#34;).fetchone() last_id = watermark[0] if watermark else 0 print(f\u0026#34;마지막 적재 ID: {last_id}\u0026#34;) # 증분 적재: last_id 이후 데이터만 가져온다 conn.execute(f\u0026#34;\u0026#34;\u0026#34; INSERT INTO bronze.orders SELECT * FROM read_csv_auto( \u0026#39;https://raw.githubusercontent.com/dbt-labs/jaffle_shop/main/seeds/raw_orders.csv\u0026#39; ) WHERE id \u0026gt; {last_id}; \u0026#34;\u0026#34;\u0026#34;) # 워터마크 갱신 conn.execute(\u0026#34;\u0026#34;\u0026#34; INSERT OR REPLACE INTO bronze.watermarks (table_name, last_loaded_id, last_loaded_at) SELECT \u0026#39;orders\u0026#39;, MAX(id), current_timestamp FROM bronze.orders; \u0026#34;\u0026#34;\u0026#34;) print(\u0026#34;Incremental Load 완료\u0026#34;) watermarks 테이블이 증분 적재의 핵심이다. 어디까지 가져왔는지를 기록해두고, 다음 적재 때 그 이후만 가져온다. 이 패턴을 하이 워터마크(High Watermark) 라고 부른다.\n메타데이터 컬럼을 붙인다 Bronze에 원본 데이터만 넣으면 나중에 답이 안 나오는 질문이 생긴다. \u0026ldquo;이 데이터가 언제 적재된 건가?\u0026rdquo; \u0026ldquo;어느 소스에서 온 건가?\u0026rdquo;\n원본 컬럼은 그대로 두고, 메타데이터 컬럼을 추가한다.\nconn.execute(\u0026#34;\u0026#34;\u0026#34; CREATE OR REPLACE TABLE bronze.orders_with_meta AS SELECT *, current_timestamp AS _loaded_at, \u0026#39;jaffle_shop\u0026#39; AS _source_system, \u0026#39;full\u0026#39; AS _load_type FROM read_csv_auto( \u0026#39;https://raw.githubusercontent.com/dbt-labs/jaffle_shop/main/seeds/raw_orders.csv\u0026#39; ); \u0026#34;\u0026#34;\u0026#34;) conn.execute(\u0026#34;SELECT * FROM bronze.orders_with_meta LIMIT 3\u0026#34;).fetchdf() _loaded_at, _source_system, _load_type. 언더스코어로 시작하는 이유는 원본 컬럼과 구분하기 위해서다. 원본에 loaded_at이라는 컬럼이 있을 수도 있으니까.\n이 메타데이터가 있으면 Silver 변환에서 문제가 생겼을 때 \u0026ldquo;언제 적재한 데이터까지는 정상이고, 이후부터 이상하다\u0026quot;는 식으로 범위를 좁힐 수 있다. 3편에서 쓰이는 _loaded_at 중복 제거 패턴도 여기서 출발한다.\n적재 패턴 정리 Bronze 적재 패턴을 정리하면 이렇다.\n패턴 적용 대상 구현 Full Load (덮어쓰기) 코드 테이블, 소규모 마스터 CREATE OR REPLACE TABLE Full Load (스냅샷) 일별 현황 보관이 필요한 경우 파티션 키로 적재일 사용 Incremental (타임스탬프) updated_at이 있는 테이블 WHERE updated_at \u0026gt; 워터마크 Incremental (자동 증가 키) 로그, 이벤트, 주문 WHERE id \u0026gt; 워터마크 CDC 삭제 감지가 필요한 경우 Debezium + Kafka Full Load 중에 스냅샷 방식이 하나 더 있다. 덮어쓰기가 아니라 적재일 기준으로 매일의 전체 상태를 따로 저장하는 방식이다. 상품 마스터의 어제 상태와 오늘 상태를 비교하고 싶을 때 쓴다. 스토리지를 많이 먹지만, 1편\r에서 얘기했듯 클라우드 환경에서 스토리지 비용은 무시할 수 있는 수준이다.\n실무 참고: Airflow로 Bronze 적재 Bronze 적재를 Airflow DAG으로 짜면 테이블마다 Full Load / Incremental Load를 구분해서 태스크를 나눌 수 있다.\nfrom airflow import DAG from airflow.operators.python import PythonOperator from datetime import datetime import duckdb def load_full(table_name, source_url, **context): \u0026#34;\u0026#34;\u0026#34;Full Load: 전체 교체\u0026#34;\u0026#34;\u0026#34; conn = duckdb.connect(\u0026#39;warehouse.duckdb\u0026#39;) conn.execute(f\u0026#34;\u0026#34;\u0026#34; CREATE OR REPLACE TABLE bronze.{table_name} AS SELECT *, current_timestamp AS _loaded_at, \u0026#39;{table_name}\u0026#39; AS _source_system, \u0026#39;full\u0026#39; AS _load_type FROM read_csv_auto(\u0026#39;{source_url}\u0026#39;) \u0026#34;\u0026#34;\u0026#34;) conn.close() def load_incremental(table_name, source_url, key_column, **context): \u0026#34;\u0026#34;\u0026#34;Incremental Load: 워터마크 이후만\u0026#34;\u0026#34;\u0026#34; conn = duckdb.connect(\u0026#39;warehouse.duckdb\u0026#39;) wm = conn.execute(f\u0026#34;\u0026#34;\u0026#34; SELECT COALESCE(last_loaded_id, 0) FROM bronze.watermarks WHERE table_name = \u0026#39;{table_name}\u0026#39; \u0026#34;\u0026#34;\u0026#34;).fetchone() last_id = wm[0] if wm else 0 conn.execute(f\u0026#34;\u0026#34;\u0026#34; INSERT INTO bronze.{table_name} SELECT *, current_timestamp AS _loaded_at FROM read_csv_auto(\u0026#39;{source_url}\u0026#39;) WHERE {key_column} \u0026gt; {last_id} \u0026#34;\u0026#34;\u0026#34;) conn.close() with DAG( dag_id=\u0026#39;bronze_ingestion\u0026#39;, schedule=\u0026#39;0 5 * * *\u0026#39;, start_date=datetime(2026, 1, 1), catchup=False, ) as dag: # 소규모 마스터 → Full Load load_customers = PythonOperator( task_id=\u0026#39;load_customers_full\u0026#39;, python_callable=load_full, op_kwargs={\u0026#39;table_name\u0026#39;: \u0026#39;customers\u0026#39;, \u0026#39;source_url\u0026#39;: \u0026#39;...\u0026#39;}, ) # 대용량 트랜잭션 → Incremental Load load_orders = PythonOperator( task_id=\u0026#39;load_orders_incremental\u0026#39;, python_callable=load_incremental, op_kwargs={ \u0026#39;table_name\u0026#39;: \u0026#39;orders\u0026#39;, \u0026#39;source_url\u0026#39;: \u0026#39;...\u0026#39;, \u0026#39;key_column\u0026#39;: \u0026#39;id\u0026#39;, }, ) # 병렬 실행 — 테이블 간 의존 관계가 없으니까 [load_customers, load_orders] 소규모 마스터는 load_full, 대용량 트랜잭션은 load_incremental. 테이블 특성에 맞게 함수를 나눠서 호출한다. 테이블 간에는 의존 관계가 없으니 Airflow가 병렬로 실행한다.\n다음 글에서는 Silver 레이어를 다룬다. Bronze에 쌓아둔 원본 데이터를 정제하고 표준화하는 과정이다. dbt를 본격적으로 쓰기 시작한다.\nGoogle Colab에서 실습하기\r","permalink":"https://datanexus-kr.github.io/guides/etl-design/002-bronze-layer/","summary":"Bronze에 데이터를 넣는 방법은 두 가지다. 전체를 덮어쓰거나, 바뀐 것만 가져오거나. 어떤 방식을 고르느냐에 따라 파이프라인의 복잡도가 완전히 달라진다.","title":"2. Bronze 레이어 - 원본을 있는 그대로 쌓는다"},{"content":"\rGoogle Colab에서 실습하기\r데이터 레이크가 늪이 되는 과정 데이터 레이크에 파일을 쏟아놓고 바로 분석하려는 팀이 있다. 처음엔 빠르다. CSV 올리고 SQL 한 줄이면 결과가 나온다.\n3개월 지나면 상황이 달라진다. 누가 올린 파일인지 모른다. 원본인지 정제된 건지 구분이 안 된다. 같은 매출 테이블인데 부서마다 숫자가 다르다. 데이터 늪(data swamp)이라고 부르는 상태다.\n원인은 단순하다. 원본과 가공물이 같은 공간에 섞여 있기 때문이다. 레이어를 나누면 이 문제가 풀린다.\nBronze, Silver, Gold 메달리온 아키텍처는 데이터를 세 개의 레이어로 나눈다. Databricks가 이름을 붙여서 널리 퍼졌지만, 개념 자체는 전통 DW의 레이어드 접근과 같다.\n소스 시스템 → [Bronze] → [Silver] → [Gold] → BI / 분석 원본 적재 정제·표준화 비즈니스 집계 Bronze 는 원본 그대로다. 소스 시스템에서 가져온 데이터를 변환 없이 저장한다. CSV든 JSON이든 API 응답이든 있는 그대로. 데이터 계보(lineage)의 출발점이다. 여기서 뭔가를 바꾸면 원본을 잃는다.\nSilver 는 정제와 표준화다. Bronze 데이터의 타입을 맞추고, 중복을 제거하고, 키를 통합한다. \u0026ldquo;분석에 쓸 수 있는 상태\u0026quot;로 만드는 레이어다. 비즈니스 로직은 아직 넣지 않는다.\nGold 는 비즈니스 관점의 집계다. 팩트 테이블, 차원 테이블, KPI 마트. 최종 사용자가 직접 쿼리하는 레이어다. DW 모델링 1편\r에서 다뤘던 스타스키마가 여기 해당한다.\n각 레이어의 역할이 명확하다는 게 핵심이다. Bronze에서는 변환하지 않는다. Silver에서는 비즈니스 로직을 넣지 않는다. Gold에서만 비즈니스 관점의 가공이 들어간다. 이 규칙이 깨지면 레이어를 나눈 의미가 없다.\n전통 DW 레이어와의 대응 DW 모델링 시리즈\r에서 Raw → Staging → Integration → Mart 구조를 다뤘다. 메달리온과 이름만 다르고 역할은 거의 같다.\n메달리온 전통 DW 하는 일 Bronze Raw / Staging 원본 적재, 변환 없음 Silver Integration (3NF / Data Vault) 정제, 표준화, 키 통합 Gold Mart (Star Schema) 비즈니스 집계, 분석용 전통 DW에서는 Staging과 Integration 사이에 ETL 서버가 무거운 변환을 처리했다. 메달리온은 ELT 패러다임이다. 일단 Bronze에 적재하고, DW 엔진 안에서 Silver와 Gold를 만든다. 변환을 별도 서버가 아니라 DW 엔진의 컴퓨팅 파워로 처리한다는 점이 다르다.\n이 시리즈의 실습 환경 시리즈 전체에서 사용할 도구는 세 가지다. 전부 무료이고, Google Colab에서 클라우드 계정 없이 바로 돌릴 수 있다.\n도구 역할 DuckDB 로컬 DW 엔진. Columnar Storage 기반이라 BigQuery/Snowflake와 같은 방식으로 동작한다 dbt-core + dbt-duckdb 변환 레이어. SQL로 Bronze → Silver → Gold를 정의한다 Soda Core 데이터 품질 검증. 레이어 간 품질 게이트를 건다 DuckDB를 고른 이유가 있다. 설치가 pip install 한 줄이면서, 실제 클라우드 DW와 동작 방식이 같다. Parquet, CSV를 네이티브로 읽고, SQL로 분석하고, Columnar Storage라 컬럼 기반 스캔이 된다. 로컬에서 돌리지만 클라우드 DW의 축소판이라고 보면 된다.\n환경 세팅 Colab 셀에서 아래를 실행하면 준비 끝이다.\n# 도구 설치 !pip install -q duckdb dbt-core dbt-duckdb import duckdb # DuckDB 데이터베이스 생성 conn = duckdb.connect(\u0026#39;warehouse.duckdb\u0026#39;) print(f\u0026#34;DuckDB {duckdb.__version__} 준비 완료\u0026#34;) 샘플 데이터 준비 시리즈에서 사용할 샘플은 간단한 이커머스 데이터다. 주문, 고객, 상품 세 테이블. DW 모델링 2편\r에서 다뤘던 구조와 같은 도메인이다.\n# Bronze 레이어: 원본 그대로 적재 conn.execute(\u0026#34;\u0026#34;\u0026#34; CREATE SCHEMA IF NOT EXISTS bronze; CREATE OR REPLACE TABLE bronze.orders AS SELECT * FROM read_csv_auto(\u0026#39;https://raw.githubusercontent.com/ dbt-labs/jaffle_shop/main/seeds/raw_orders.csv\u0026#39;); CREATE OR REPLACE TABLE bronze.customers AS SELECT * FROM read_csv_auto(\u0026#39;https://raw.githubusercontent.com/ dbt-labs/jaffle_shop/main/seeds/raw_customers.csv\u0026#39;); CREATE OR REPLACE TABLE bronze.payments AS SELECT * FROM read_csv_auto(\u0026#39;https://raw.githubusercontent.com/ dbt-labs/jaffle_shop/main/seeds/raw_payments.csv\u0026#39;); \u0026#34;\u0026#34;\u0026#34;) # 적재 확인 conn.execute(\u0026#34;SELECT count(*) as cnt FROM bronze.orders\u0026#34;).fetchdf() Bronze에 적재했다. CSV를 읽어서 DuckDB에 넣었을 뿐, 어떤 변환도 하지 않았다. 타입 캐스팅도 안 했고, 컬럼명도 바꾸지 않았다. 이게 Bronze의 원칙이다.\n# Bronze 데이터 확인 conn.execute(\u0026#34;SELECT * FROM bronze.orders LIMIT 5\u0026#34;).fetchdf() 여기서 바로 분석 쿼리를 던지고 싶은 유혹이 생긴다. 참아야 한다. Bronze 데이터를 직접 분석에 쓰면 3개월 뒤에 데이터 늪에 빠진다. 다음 글에서 Bronze를 Silver로 올리는 과정을 다룬다.\n왜 이렇게까지 나누는가 레이어를 나누면 느려지지 않냐는 질문을 받는다. 저장 공간도 더 쓰고, 변환 단계도 늘어나니까.\n맞다. 대신 세 가지를 얻는다.\n재처리가 가능하다. Silver 로직에 버그가 있으면 Bronze에서 다시 만들면 된다. 원본이 살아있으니까. Bronze 없이 Silver만 있으면 소스 시스템에서 다시 끌어와야 한다.\n문제 추적이 된다. Gold의 숫자가 이상하면 Silver를 보고, Silver가 이상하면 Bronze를 본다. 어느 레이어에서 문제가 생겼는지 특정할 수 있다.\n역할이 분리된다. 데이터 엔지니어는 Bronze→Silver를 책임지고, 분석 엔지니어는 Silver→Gold를 책임진다. 서로의 영역을 건드리지 않아도 된다.\n클라우드 환경에서 스토리지 비용은 거의 무시할 수 있는 수준이다. 레이어를 하나 더 두는 비용보다, 데이터 늪에 빠졌을 때의 비용이 훨씬 크다.\n실무 참고: Airflow DAG 이 시리즈의 실습은 Colab + dbt로 진행하지만, 실무에서는 Airflow가 파이프라인을 스케줄링하고 오케스트레이션한다. 메달리온 아키텍처의 Bronze → Silver → Gold 흐름을 Airflow DAG으로 짜면 이런 모양이다.\nfrom airflow import DAG from airflow.operators.bash import BashOperator from datetime import datetime with DAG( dag_id=\u0026#39;medallion_pipeline\u0026#39;, schedule=\u0026#39;0 6 * * *\u0026#39;, # 매일 오전 6시 start_date=datetime(2026, 1, 1), catchup=False, ) as dag: bronze = BashOperator( task_id=\u0026#39;load_bronze\u0026#39;, bash_command=\u0026#39;python scripts/load_bronze.py\u0026#39;, ) silver = BashOperator( task_id=\u0026#39;run_silver\u0026#39;, bash_command=\u0026#39;cd dbt_project \u0026amp;\u0026amp; dbt run --select staging\u0026#39;, ) gold = BashOperator( task_id=\u0026#39;run_gold\u0026#39;, bash_command=\u0026#39;cd dbt_project \u0026amp;\u0026amp; dbt run --select marts\u0026#39;, ) bronze \u0026gt;\u0026gt; silver \u0026gt;\u0026gt; gold bronze \u0026gt;\u0026gt; silver \u0026gt;\u0026gt; gold. 의존 관계가 한 줄로 읽힌다. Bronze 적재가 끝나야 Silver가 돌고, Silver가 끝나야 Gold가 돈다. Airflow가 이 순서를 보장하고, 실패하면 알림을 보내고, 재실행도 처리한다.\ndbt는 \u0026ldquo;무엇을 변환할지\u0026quot;를 정의하고, Airflow는 \u0026ldquo;언제, 어떤 순서로 실행할지\u0026quot;를 정의한다. 역할이 다르다.\n다음 글에서는 Bronze 레이어를 본격적으로 다룬다. Full Load와 Incremental Load의 차이, 증분 적재의 기준 컬럼을 어떻게 잡는지.\nGoogle Colab에서 실습하기\r","permalink":"https://datanexus-kr.github.io/guides/etl-design/001-medallion-architecture/","summary":"Bronze, Silver, Gold. 데이터를 레이어별로 나눠서 적재하면 뭐가 달라지는가. DuckDB와 dbt로 직접 구성해 본다.","title":"1. 메달리온 아키텍처 - 데이터를 세 겹으로 쌓는 이유"},{"content":"자체 30문항 기준 NL2SQL 정확도를 80%까지 올렸다(이전 글\r).\n같은 모델을 공개 벤치마크(BIRD Mini-Dev)로 돌려보니 숫자가 완전히 달랐다. BIRD는 11개 도메인 DB에 걸쳐 500개 질문이 담긴 NL2SQL 표준 벤치마크다.\n80% → 56%\n기존에 학습한 유통 도메인에서는 잘 맞췄는데, 처음 보는 도메인으로 넘어가니 절반 수준이다.\n이 56%를 넘겨보려고 9번 실험했다. (처음엔 솔직히 금방 될 줄 알았다.)\n한 줄 결론 SQL 생성 시도 횟수를 늘린다고 될 문제가 아니라, 처음부터 한 번에 맞춰야 되는 문제였다.\n왜 SQL을 여러 개 만들어 고르는 방식(multi-candidate)을 시도했나 SQL을 하나만 만들지 말고 여러 개를 만든 뒤에, 그중에서 가장 좋은 걸 고른다\nNL2SQL에서 이미 널리 쓰이는 방식이다.\nself-consistency (같은 질문을 여러 번 생성해 다수결로 선택) execution-based selection (실행 결과로 후보를 거르는 방식) multi-agent pipeline (여러 에이전트가 분업해서 SQL 생성) BIRD 리더보드 상위 모델들도 대부분 이 방향이다.\n대신, 두 가지를 가정했다.\n한 번 생성에는 정확도 한계가 있다 여러 개 만들고 잘 고르면 그 한계를 넘을 수 있다 1. 힌트를 더 주면 좋아질까 (4번 실패) 결과: BIRD에서는 효과 없음. 오히려 정답을 바꿔버린 경우 까지도 발생했다.\nLLM이 더 잘 고르게 힌트를 주자\n예를 들어:\n어떤 집계 함수 써야 하는지 어떤 컬럼이 \u0026ldquo;매출\u0026quot;인지 관련 컬럼 후보 목록이 어떤게 있는지 하지만, 결과는:\n자체 30문항: 유지 BIRD: 변화 없음 일부 케이스에서는 결과가 더 나빠졌다. 원래 products.price를 잘 고르던 질문에서 힌트를 주자 order_items.unit_price로 바뀌었다. 힌트가 정답을 바꿔버렸다.\n60%는 컬럼 선택이 아니라 SQL \u0026ldquo;형태\u0026quot;의 문제였다.\n예:\nSUM(CASE WHEN ...) COUNT(CASE WHEN ...) 둘 다 맞는 SQL이지만 NULL 처리 차이 때문에 결과가 달라진다.\n힌트로 컬럼을 바꿀 수는 있지만, SQL 작성 방식을 바꾸지는 못한다.\n2. 여러 개 만들고 고르면 되지 않을까 (3번 실패) 선택지를 여러 개 만들어도 답이 안 바뀌었다. 결국 첫 번째로 만든 SQL을 그대로 썼다.\n설정:\nk=3 temperature=0.3 결과 기반 선택 정확도 56% (변화 없음)\n지표 값 후보 3개가 같은 결과 92% 첫 번째 후보 채택 100% selector가 단 한 번도 결과를 바꾸지 않았다.\n\u0026ldquo;+8% 오른 것처럼 보였던\u0026rdquo; 착시 처음엔 48% → 56%로 보였다.\n다시 채점해보니 원래도 56%였다. 채점 로직 변경으로 생긴 착시였다. (이걸 계기로 채점 드리프트 가드도 만들었다.)\n다양성을 늘리면 될까? temperature ↑ 프롬프트 변형 5개 추가 하지만, 결과는:\nSQL 텍스트는 달라짐 실행 결과는 동일 62%의 질문에서 5개 후보가 전부 같은 결과\n결론적으로, temperature를 올리고 프롬프트를 바꿔도 실행 결과는 달라지지 않았다.\n3. 시스템이 강제로 다른 후보를 만들면? (3번 실패) 결과: 강제로 선택지를 여러개 만들어도 selector가 정답을 못 고른다. 실행 결과만으로는 판별 자체가 불가능하다.\nLLM이 다양성을 못 만들면 시스템이 강제로 만들어보자.\n실험 1: 컬럼 강제 올바른 컬럼 강제 → 60% 성공 잘못된 컬럼 강제 → 0% 성공 schema binding이 정확도를 결정한다\n실험 2: selector 검증 정답 / 오답 / 그럴듯한 오답 / 변형 → 4개 후보 중 고르게 함\n28.6% (거의 랜덤)\n가장 치명적인 사례 DB에 저장된 실제 값은 체코어 VYBER(현금 인출)였는데, LLM은 이걸 모르는 채로 SQL을 만들어서 후보 4개 모두 WHERE 조건에 영어(cash withdrawal)를 썼다. 당연히 매칭되는 행이 없다.\n결과:\n4개 모두 빈 결과 (0 row) selector는 4개가 같은 결과를 내자 \u0026ldquo;합의\u0026quot;로 판단 오답 선택 정답이 아니면, 합의를 해도 의미가 없다\n실험 3: 점수 기반 selector 전략을 바꿨다. 실행 결과만 보는 게 아니라, 반환된 값의 분포, 컬럼 수, 행 수 등 여러 신호를 종합해 각 후보에 점수를 매기는 V2 selector를 만들었다.\n결과:\nV1: 40% V2: 33% 더 정교하게 만들었는데 오히려 더 나빠졌다.\n한 케이스(qid 819)가 이유를 잘 보여준다. V2가 정답(gold) SQL에 55점, 명백히 틀린(obvious_wrong) SQL에 75점을 매겼다. 정답보다 오답을 더 높이 평가한 셈이다.\n실행 결과만으로는 어떤 SQL이 더 \u0026ldquo;맞는지\u0026rdquo; 판단할 수 없다\n접은 방향과 남긴 방향 세 방향 모두 폐기:\n힌트 기반 보정 → 실패 LLM multi-candidate → 실패 실행 결과 기반 선택 → 실패 공통 원인 문제는 \u0026ldquo;선택\u0026rdquo; 단계가 아니라 \u0026ldquo;생성하기 이전\u0026rdquo; 단계다.\nSQL을 실행한 뒤 고르는 방식으로는 안 되고, 실행 전에 이미 올바른 테이블/컬럼이 잡혀 있어야 한다.\n그래서 방향을 바꿨다 Schema 이해를 강화해서 한 번에 맞추기\nSchema Binding Plan SQL을 바로 만들지 않는다.\n먼저 사용할 테이블 / 컬럼 / join 조건을 JSON으로 생성 시스템이 검증 그 다음 SQL 생성 3차 실험에서 확인했다:\nLLM은 binding만 맞으면 100% 따른다.\n문제는 SQL 생성이 아니고, schema 해석 단계 문제였다.\n9번 실패에서 남은 것 실험은 실패했지만 교훈은 남았다.\n채점 드리프트 가드. 채점 로직이 조금씩 바뀌어도 이전 결과와 비교할 수 있도록 맞춰주는 코드. 이게 없었다면 이번 실험이 \u0026ldquo;+8%p 성공\u0026quot;으로 잘못 기록됐다.\nsignal classifier. \u0026ldquo;정답을 고를 수 있는 실마리가 있는가\u0026quot;를 STRONG/MISLEADING 등 4단계로 분류하는 도구. \u0026ldquo;selector가 약한 건지, 아니면 판별 근거 자체가 없는 건지\u0026quot;를 숫자로 나눠볼 수 있게 만들었다.\n강제 binding 검증 코드. LLM에게 \u0026ldquo;이 컬럼을 써라\u0026quot;고 지시했을 때, 실제로 생성된 SQL에 그 컬럼이 들어갔는지 자동으로 확인하는 코드(SQL 파서 sqlglot 사용). 다음 단계 schema grounding에도 그대로 쓸 수 있다.\n중단 조건 / 실험 설계 체계. 실험 전에 \u0026ldquo;여기서 안 되면 접는다\u0026quot;는 기준을 미리 코드에 박아두고, 작은 스팟 체크로 빠르게 방향을 결정하는 방식.\n그리고 하나 더.\nSOTA가 맞다고 해서, 내 문제에도 맞는 건 아니다\nAI 3개에게 물었더니 10개 중 8개가 multi-candidate를 추천했다. 실험 데이터가 없었다면 그대로 따라갔을 것이다.\n마무리 8번 글에서는 \u0026ldquo;80%는 시작\u0026quot;이라고 썼었는데, 그건 학습된 도메인 기준이었다. 처음 보는 도메인(Unseen Domain)에서는 56%다.\n이게 진짜 시작점이다.\n","permalink":"https://datanexus-kr.github.io/posts/datanexus/009-multi-candidate-seal/","summary":"자체 30문항에서 정확도 80%를 찍었지만, BIRD 공개 벤치마크 50문항에서는 56%였다. 9번의 실험으로 \u0026lsquo;후보 여러 개 만들어서 고르기\u0026rsquo; 가설을 세 방향에서 모두 접었다. 남은 건 스키마 이해와 방법론.","title":"9. 공개 벤치마크에서 56%: 9번 실험하고 접은 것들"},{"content":"\rGEO 최적화 Guide — 전체 시리즈\n1. GEO란 무엇인가 - SEO 너머의 AI 인용 전략\r2. AI마다 인용하는 소스가 다르다\r3. On-Site GEO 기술 구조 - 상품 DB에서 JSON-LD까지\r4. Off-Site GEO - 공식 사이트를 안 보는 AI에게 선택받는 법\r5. AEO - 코딩 에이전트가 읽는 문서는 왜 다른가 ← 현재 글\rAI가 읽는 문서는 한 종류가 아니다 4편\r까지 On-Site와 Off-Site GEO를 정리했다. 공식 사이트의 JSON-LD, 외부 디렉토리, 커뮤니티 채널까지. 소비자가 ChatGPT나 Perplexity에 뭔가 물어볼 때 우리 브랜드가 인용 소스로 올라오게 하는 작업이었다.\n근데 AI가 읽는 문서가 이게 전부가 아니다.\n개발자가 Claude Code나 Cursor한테 \u0026ldquo;이 API 연동해줘\u0026quot;라고 시키면, 에이전트가 혼자 API 문서를 크롤링한다. 이때 에이전트가 문서를 처리하는 방식이 사람이 보는 페이지와 다르다.\n이걸 AEO(Agentic Engine Optimization) 라고 부른다. 며칠 전에 정리된 개념\r이라 국내에는 논의가 거의 없는데, GEO 하는 입장에서 한 번은 짚고 넘어갈 만하다.\nAEO와 GEO는 뭐가 다른가 같은 AI라도 누가 쓰느냐에 따라 최적화가 달라진다.\n항목 GEO AEO 타겟 ChatGPT, Perplexity, Gemini Claude Code, Cursor, Cline, Aider 소비자 질문하는 사람 (간접) 코드 짜는 에이전트 (직접) 대상 콘텐츠 상품 페이지, 브랜드 정보 API 문서, 개발자 포털 핵심 포맷 JSON-LD, Schema.org Markdown, llms.txt, skill.md 측정 지표 인용률 토큰 효율, 파싱 성공률 실패 모드 답변에 안 뜸 에이전트가 엉뚱한 API 호출 겹치는 원칙도 있다. SSR 기반 서빙, robots.txt 점검, 구조화된 콘텐츠. 이 세 가지는 두 영역 모두에서 깔려야 한다. GEO를 제대로 깐 조직은 AEO 진입 장벽이 낮다.\nGEO는 \u0026ldquo;콘텐츠가 답변에 인용되느냐\u0026quot;를 본다. AEO는 \u0026ldquo;에이전트가 API를 제대로 쓰느냐\u0026quot;를 본다. 후자가 실패하면 아무도 모르는 사이에 잘못된 코드가 나간다.\n에이전트는 사람처럼 문서를 읽지 않는다 사람이 개발자 포털에 도착하면 메뉴를 훑고, Getting Started를 클릭하고, 샘플 코드를 Run 해보고, 관련 링크 몇 개를 타고 들어가면서 4~8분 머문다. 이 동작이 분석 도구에 전부 기록된다.\n에이전트는 한두 번의 HTTP GET으로 페이지를 통째로 가져와서 파싱하고 끝낸다. 스크롤도 클릭도 없다. GA에는 체류 시간 400ms짜리 요청 한 건만 남는다.\n서버 로그를 보면 User-Agent에서 가려낼 수 있다.\n에이전트 User-Agent Claude Code axios/1.8.4 Cursor got (sindresorhus/got) Cline, Junie curl/8.4.0 Windsurf colly Aider, OpenCode Headless Chromium (Playwright) 기존에 그냥 \u0026ldquo;알 수 없는 크롤러\u0026quot;로 묶이던 트래픽 중 상당수가 여기일 수 있다.\n긴 문서는 에이전트가 끝까지 읽지 않는다 에이전트는 컨텍스트 제한이 있다. Claude나 GPT 계열이 보통 100K~200K 토큰 사이다. 문서 하나가 이 창을 넘거나 근접하면 에이전트가 조용히 다음 중 하나를 한다.\n뒷부분을 잘라먹는다. 중요한 내용이 뒤에 있었다면 답변이 틀린다 더 짧은 다른 문서로 넘어간다 청킹하다가 레이턴시만 늘고 오류가 난다 포기하고 자기가 학습한 지식으로 답을 만든다. 이게 환각이다 실제로 API 문서 하나가 10만 토큰을 넘는 케이스도 있다. 이쯤 되면 단일 문서가 에이전트 컨텍스트를 혼자 다 써버린다.\n그래서 문서 길이가 지표가 된다. 권장 기준은 이렇다.\n콘텐츠 유형 토큰 권장 Quick Start / Getting Started 15K 이하 개별 API 레퍼런스 25K 이하 컨셉 가이드 20K 이하 (세부는 링크로) 전체 API 레퍼런스 리소스·엔드포인트 단위로 쪼개기 GEO에는 이런 제약이 없다. 소비자용 AI는 필요한 스니펫만 뽑아가면 그만이니까. AEO는 문서 전체가 컨텍스트에 들어가야 해서 길이 설계가 필요하다.\n파일 네 개로 시작한다 복잡한 기술 없이 파일 네 개 정도로 시작하면 된다.\n먼저 robots.txt. 4편에서 다룬 것과 같은 파일인데, 코딩 에이전트 User-Agent도 같이 고려해야 한다. 무심코 전체 차단이 걸려 있으면 에이전트가 문서를 아예 못 본다.\n다음이 llms.txt. /llms.txt 위치에 Markdown으로 올리는 에이전트용 사이트맵이다. 페이지 이름이 아니라 \u0026ldquo;여기 들어가면 뭘 알 수 있는지\u0026quot;를 설명한다. 페이지별 토큰 수도 같이 적어두면 에이전트가 읽을지 말지 판단할 수 있다.\n# MyService Documentation ## Getting Started - [Quick Start](/docs/quickstart): 5분 안에 첫 API 호출 (8K tokens) - [Authentication](/docs/auth): OAuth 2.0, API Key 인증 (12K tokens) ## API Reference - [Users API](/docs/api/users): 유저 CRUD (12K tokens) - [Events API](/docs/api/events): 이벤트 스트리밍, 웹훅 (8K tokens) skill.md 는 서비스가 \u0026ldquo;무엇을 할 수 있는지\u0026quot;를 선언적으로 적는 파일이다. 긴 문서를 다 안 읽어도 capability를 빨리 파악하게 해준다. What I can accomplish, Required inputs, Constraints, Key documentation 네 섹션이 기본 구성이다.\n마지막이 AGENTS.md. 프로젝트 루트에 두는 README의 에이전트 버전이다. 코딩 에이전트가 프로젝트를 열면 가장 먼저 찾는다. 오픈소스 쪽에서는 이미 깔고 시작하는 경우가 늘고 있다.\n한국 기업 관점에서 AEO는 누구 얘기인가 솔직히 말해서 국내 대부분 기업은 AEO 1순위 대상이 아니다. 개발자 포털을 운영하는 회사가 많지 않다. 이커머스, 유통, 금융, 서비스업은 소비자용 GEO만 제대로 해도 효과가 훨씬 크다.\nAEO를 진지하게 볼 만한 경우는 이런 곳이다.\nAPI를 공개하는 회사: 결제 PG, 배송, 지도, 인증, 데이터 API 사업자. 개발자가 Cursor로 연동 코드를 짜는 게 이미 디폴트 내부 개발 플랫폼을 운영하는 대기업: 그룹 공통 API, 인증 게이트웨이, 사내 데이터 플랫폼 문서가 대상 오픈소스/개발자 도구: GitHub에 프로젝트를 공개하고 있다면 AGENTS.md는 거의 필수에 가까움 MCP 서버 제공자: skill.md는 직결되는 컨벤션 내부 데이터 플랫폼을 에이전트에 붙이는 작업을 돌려봤는데, 막상 에이전트가 내부 위키와 메타데이터를 잘 못 읽어서 답이 엉망인 경우가 자주 나왔다. 이때 AEO 원칙 몇 개만 적용해도 결과가 달라진다. 문서를 Markdown으로 제공하고, 토큰 수를 페이지에 박고, 네비게이션 노이즈를 걷어내는 것만으로 파싱 품질이 체감될 만큼 올랐다.\n파일 네 개로 다 해결되는 건 아니다. 조직별 용어 차이나 부서별 암묵적 맥락은 여전히 남는다. 이건 다른 차원 문제라 파일 설계만으로는 안 풀린다.\nGEO를 했다면 추가 비용은 크지 않다 이미 GEO를 구축했다면 이행 비용이 생각보다 작다. 겹치는 작업이 많다.\nrobots.txt를 AI 크롤러 관점에서 재점검 (GPTBot, ClaudeBot, PerplexityBot 외에 코딩 에이전트 User-Agent도) 주요 문서 페이지별 토큰 수 계산 (글자 수 ÷ 4로 근사) /llms.txt 초안 작성. 주요 문서 목록과 토큰 수 상위 3~5개 API에 skill.md 작성 내부 GitHub 저장소에 AGENTS.md 배치 개발자 문서를 Markdown으로도 서빙. URL에 .md 붙이면 원본 반환하는 식 서버 로그에서 코딩 에이전트 트래픽 세그먼트 분리해서 baseline 잡기 robots.txt 재점검과 llms.txt 초안은 반나절이면 끝난다. skill.md와 AGENTS.md도 상위 API 몇 개부터 돌리면 부담이 크지 않다.\n","permalink":"https://datanexus-kr.github.io/guides/geo-optimization/005-what-is-aeo/","summary":"GEO가 소비자 AI를 위한 최적화였다면 AEO는 코딩 에이전트를 위한 최적화다. 문서 길이 제약, llms.txt, skill.md, AGENTS.md까지 필요한 파일들을 정리한다.","title":"5. AEO - 코딩 에이전트가 읽는 문서는 왜 다른가"},{"content":"Claude Code 성능이 들쭉날쭉할 때, 더 많은 도구를 붙이면 나아질 거라 생각했다. 반대였다. 환경을 걷어낼수록 응답 품질이 올라갔다.\n스킬은 빼라 남이 만든 스킬셋(슈퍼파워즈 계열 등)을 마구잡이로 얹으면 시스템 프롬프트에 노이즈가 쌓인다. 모델이 도구 사용법을 파악하느라 자원을 쓰다 보면 정작 코드를 읽고 판단하는 데 집중하지 못한다. 순정 상태의 Claude가 훨씬 똑똑하다는 건 써보면 바로 체감된다.\nsettings.json 3줄로 풀 추론 강제 ~/.claude/settings.json에 아래 세 가지를 추가하면 된다.\n{ \u0026#34;effortLevel\u0026#34;: \u0026#34;max\u0026#34;, \u0026#34;env\u0026#34;: { \u0026#34;CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING\u0026#34;: \u0026#34;1\u0026#34;, \u0026#34;CLAUDE_CODE_DISABLE_1M_CONTEXT\u0026#34;: \u0026#34;1\u0026#34; } } effortLevel: max 는 매 턴 최대 추론을 강제한다. Opus 4.6 전용 설정이다.\nDISABLE_ADAPTIVE_THINKING 은 모델이 \u0026ldquo;이건 쉬우니 생각 안 해도 되겠다\u0026quot;고 스스로 추론을 건너뛰는 걸 차단한다. 이게 꺼져 있지 않으면 가짜 SHA, 없는 API 버전 같은 환각이 잦아진다.\nDISABLE_1M_CONTEXT 는 컨텍스트를 200k로 제한해서 추론 집중도를 높인다. 1M 컨텍스트를 그냥 두면 토큰을 5~7배 과소비하는 케이스가 나온다.\n모델이 발전해도 변하지 않는 것 명확한 요구사항을 논리적이고 구조적으로 전달하는 능력은 여전히 사람 몫이다. 프롬프팅 기교가 아니라 사고력의 문제다. 모델이 아무리 좋아져도 이 부분은 채워지지 않는다.\n에너지를 쓸 곳 프롬프트 엔지니어링이나 스킬셋 조립에 매달리는 건 모델이 알아서 해결할 영역에 에너지를 쓰는 거다. 레버리지가 큰 쪽은 따로 있다. 메모리 체계, 온톨로지, 파이프라인 설계 같은 시스템 아키텍처를 파는 게 훨씬 크게 돌아온다.\n외부 스킬셋을 무분별하게 추가하면 System Prompt 비대화로 모델 본래의 추론 성능이 저하된다. effortLevel: max와 Adaptive Thinking 비활성화, 1M Context 제한은 환각 방지와 비용 절감에 직접 영향을 준다. 프롬프트 엔지니어링보다 시스템 아키텍처와 데이터 구조 설계에 집중하는 게 더 높은 레버리지를 낸다. ","permalink":"https://datanexus-kr.github.io/curations/2026-04/2026-04-15-claude-code-performance-optimization-settings/","summary":"순정 상태 유지, settings.json 3줄로 풀 추론 강제, 시스템 아키텍처 설계에 집중하는 것이 Claude Code 성능을 실질적으로 높이는 방법이다.","title":"Claude Code 성능을 높이는 설정과 설계의 본질"},{"content":"QueryRouter를 구현하고(이전 글\r) 30문항짜리 벤치마크를 처음 돌렸다. EX(Execution Accuracy) 66.67%, 30문항 중 20개. 네 번의 측정-개선 루프로 80%까지 올린 과정이다.\n방법은 단순했다. 30문항을 한 번 돌리고, 틀린 쿼리를 유형별로 나눈 다음 영향이 큰 부분만 손봤다. 그리고 다시 돌렸다. 이 과정을 네 번 반복했다.\n시작점: 모든 쿼리가 LLM으로 간다 Phase 0.5에서 Vanna에 DDL과 Glossary YAML(25개 비즈니스 용어)을 학습시킨 상태였다. PoC DB는 전자상거래 샘플로 테이블 21개, 행이 23만 개쯤 된다. 여기에 맞춰 30문항 테스트셋을 만들고 EX를 처음 측정했더니 66.67%가 나왔다. 30개 중 20개다.\n막상 돌려보니 한 가지 문제가 바로 드러났다. 쉬운 쿼리든 어려운 쿼리든 전부 LLM이 SQL을 생성했다. \u0026ldquo;이번 달 총 매출은?\u0026rdquo; 같은 단순 집계도, \u0026ldquo;분기별 YoY 성장률을 구해줘\u0026rdquo; 같은 복잡한 쿼리도 둘 다 Vanna가 처음부터 끝까지 만들었다. LLM이 만든 SQL은 같은 질문에도 결과가 조금씩 달라질 수 있다. 단순 집계처럼 패턴이 뻔한 쿼리까지 LLM을 태울 이유는 없었다.\n그래서 QueryRouter를 만들었다.\n사이클 1: QueryRouter 도입 QueryRouter는 쿼리를 세 가지로 분류한다.\nDETERMINISTIC: 패턴이 정해진 쿼리. \u0026ldquo;총 매출 합계\u0026rdquo;, \u0026ldquo;카테고리별 주문 건수\u0026quot;처럼 SQL 템플릿으로 풀 수 있는 것들. LLM을 안 거치고 바로 SQL을 만든다 HYBRID: glossary 용어가 포함되어 있으면서 조건이 복잡한 쿼리. 규칙으로 SQL 초안을 만들고 LLM이 검증한다 PROBABILISTIC: 위 두 가지에 해당하지 않는 자유 형식 쿼리. Vanna에 그대로 위임한다 MVP라서 분류기를 거창하게 만들지 않았다. 키워드 매칭과 정규식으로 분기한다. \u0026ldquo;합계\u0026rdquo;, \u0026ldquo;총\u0026rdquo;, \u0026ldquo;평균\u0026rdquo; 같은 키워드가 있으면 DETERMINISTIC 후보로 보고, glossary에 등록된 용어가 쿼리에 포함되어 있는지 체크한다.\n30문항을 돌렸더니 EX 70%. +3.33%p. 숫자만 보면 미미한데, 안을 들여다보면 의미 있는 변화가 두 개 있었다.\n하나는 동의어 인식률이 33%에서 67%로 뛴 것이다. \u0026ldquo;매출\u0026quot;을 \u0026ldquo;Revenue\u0026quot;로 물어봤을 때 이전에는 못 알아듣던 걸 Router가 glossary의 동의어 목록을 보고 잡아낸다.\n다른 하나는 P95 응답시간(전체 쿼리 중 느린 쪽 5% 구간의 값)이 26초에서 3.3초로 87% 줄어든 것이다. DETERMINISTIC 경로는 LLM을 안 거치니까 SQL 템플릿을 바로 실행한다. 비용도 줄고 속도도 빠르다.\n근데 여기서 예상 못한 문제가 나왔다.\n\u0026ldquo;상위 10명\u0026rdquo; 함정: Fake Determinism DETERMINISTIC으로 분기된 쿼리가 20개였는데, 그중 7개가 틀렸다. 35%다.\n대표적인 케이스가 \u0026ldquo;쿠폰 사용 상위 10명 고객\u0026quot;이다. Router는 \u0026ldquo;상위\u0026quot;를 보고 계층 탐색 템플릿으로 보냈다. 하지만 실제 의도는 ORDER BY coupon_count DESC LIMIT 10이었다.\n같은 단어지만 의미가 다르다. \u0026ldquo;상위 카테고리\u0026quot;는 계층이고, \u0026ldquo;상위 10명\u0026quot;은 랭킹이다.\n이 상태가 더 위험하다. LLM은 맥락으로 보정할 여지가 있다. 하지만 DETERMINISTIC으로 분기되면 그대로 실행된다. 틀린 결과가 그대로 나간다.\n사이클 2: top-N 패치 + sanity check 세 가지를 같이 수정했다.\n우선 top-N 패턴을 정규식으로 잡아서 랭킹 표현이 먼저 매칭되면 HIERARCHY로 보내지 않도록 했다. 이 경우 HYBRID나 PROBABILISTIC으로 넘긴다.\nTOP_N_PATTERNS = [ r\u0026#34;상위\\s*\\d+\u0026#34;, # \u0026#34;상위 10명\u0026#34; r\u0026#34;top\\s*\\d+\u0026#34;, # \u0026#34;top 5\u0026#34; r\u0026#34;가장\\s*(많|높|큰)\u0026#34;, # \u0026#34;가장 많은\u0026#34; ] 이 규칙을 추가하니 Fake Det Rate가 35%에서 20%까지 내려갔다.\n다음은 sanity check다. DETERMINISTIC 경로로 SQL을 실행한 뒤 결과가 비정상이면 LLM fallback으로 한 번만 재시도한다. \u0026ldquo;비정상\u0026quot;의 기준은 두 개, 결과가 0건이거나 NULL 비율이 70%를 넘는 경우다.\nif det_result.row_count == 0 or det_result.null_ratio \u0026gt; 0.7: return await self._execute_probabilistic(query) # 1회만 재시도 무한 재귀에 빠지면 안 되니까 fallback은 딱 한 번만 한다. PROBABILISTIC 결과가 나오면 그게 맞든 틀리든 그대로 반환한다.\n마지막으로 few-shot exemplar 보강. 틀린 쿼리들의 오류 유형을 분류해보니 wrong_mapping(테이블/컬럼 매핑 오류)이 6건, wrong_formula(계산식 오류)가 2건이었다. 각 유형에 맞는 SQL 예시를 Vanna 학습 데이터에 추가했다. \u0026ldquo;이번 달 매출은?\u0026ldquo;이라는 질문에 DATE_TRUNC 패턴을 쓰는 SQL 예시, \u0026ldquo;순매출\u0026quot;을 구할 때 할인과 반품을 빼는 공식 예시 같은 것들이다.\n결과는 EX 76.67%. +6.67%p. HYBRID 분기가 1건에서 6건으로 늘었다. Router가 \u0026ldquo;확신 없으면 LLM에 넘긴다\u0026quot;는 방향으로 움직인 건데, 설계 의도대로다.\n사이클 3: is_active 함정, 그리고 정답이 틀릴 수 있다는 것 wrong_mapping 중에 한 패턴이 계속 잡혔다. 고객 테이블을 조회할 때 is_active=true 필터를 안 넣는 거다. 비활성 고객(탈퇴, 휴면)이 포함되면 집계가 달라진다. few-shot으로 예시를 여러 번 줘도 안 먹혔다.\n여기서는 접근을 바꿨다. 이건 지식 문제가 아니라 정책이다. \u0026ldquo;활성 고객만 집계한다\u0026quot;는 건 비즈니스 규칙이고, LLM이 추론할 영역이 아니다. 그래서 sqlparse로 SQL을 파싱해서, is_active 컬럼이 있는 테이블이 쿼리에 포함되면 WHERE 조건을 강제로 붙이도록 했다.\n단순 문자열 치환이 아니라 SQL AST(Abstract Syntax Tree, SQL 문법 구조를 파싱한 트리) 레벨에서 처리했다. subquery 안에는 건드리지 않고, GROUP BY 뒤에 조건이 붙는 실수도 안 생긴다. DET/HYBRID/PROB 모든 경로에서 SQL 생성 직후, 실행 직전에 적용된다.\nis_active rule을 넣고 벤치마크를 돌렸다. EX가 56.67%로 떨어졌다.\n문제는 다른 데 있었다. gold SQL(정답 SQL)에 is_active=true 조건이 없었다. 시스템은 비즈니스 정책에 맞게 필터를 넣었는데, 정답지가 그 정책 없이 작성되어 있으니 strict 비교에서 불일치로 판정된 거다.\n시스템 문제가 아니라 기준 문제였다. BIRD 벤치마크에서도 비슷한 사례가 있는데, strict scoring이 인간 전문가 판단과 62%만 일치하고 나머지 38%는 맞는 SQL을 틀렸다고 판정하는 false negative였다.\n사이클 3.1: gold SQL 수정 → 80% 작업은 크게 어렵지 않았다. gold SQL 8건에 is_active=true 조건을 추가했다. 시스템이 생성하는 SQL과 정답 SQL의 정책이 일치하도록 맞춘 거다.\n재측정 결과 EX 80.00%. 목표로 잡았던 80%에 도달했다.\nPhase 0.5부터 여기까지의 흐름을 표로 정리하면 이렇다.\n버전 EX 핵심 변화 틀린 건수 Phase 0.5 66.67% Vanna + Glossary RAG 10/30 v1 70.00% QueryRouter 추가 9/30 v2 76.67% top-N 수정 + sanity check + few-shot 7/30 v3.1 80.00% is_active hard rule + gold 정합화 6/30 +13.33%p 누적 개선. easy 난이도는 100%, Fake Det Rate는 35%에서 13.3%까지 내려갔다. hard 난이도는 37.5%로 변화 없는데, formula/JOIN 복잡도 문제라 DataHub synonym 확장이나 exemplar 보강이 필요한 영역이다.\n이 과정에서 배운 것들 이번 케이스에서 분명해진 게 있다. is_active 필터는 LLM이 해결할 문제가 아니었다. few-shot을 여러 번 넣어도 해결되지 않았다. 정책은 규칙으로 강제하는 게 훨씬 빠르고 확실했다.\n평가 기준도 설계 대상이다. gold SQL이 정책을 반영하지 않으면, 정상 동작이 오히려 오답이 된다. 이번에는 8건이었지만, 테스트셋이 수백 개가 되면 나중에 잡기가 훨씬 어려워진다. 초기에 gold SQL이 정책을 반영하는지부터 맞춰야 한다.\n변수를 하나씩 고정해서 본 게 컸다. Router를 넣을 때, is_active를 넣을 때, 매번 같은 30문항으로 돌려서 delta를 봤다. 이렇게 하지 않았으면 is_active 문제에서 \u0026ldquo;EX가 떨어졌으니 rule이 잘못됐다\u0026quot;고 판단했을 수 있다. 실제로는 rule은 제대로 동작했고 gold SQL이 문제였다. baseline을 고정하고 한 번에 하나만 바꾸는 게 지루해 보여도, 나중에 원인을 분리할 수 있는 유일한 방법이다.\nFake Determinism은 규칙 분류기의 구조적 한계다. top-N 패턴 같은 예외 규칙으로 대응했지만, 이런 예외가 늘어나면 유지보수가 어려워진다. Phase 2에서 LLM 분류기를 검토해야 할 수 있다. 당장은 보류다.\n오류 유형 분류가 없었으면 다음 액션이 안 보였을 것이다. \u0026ldquo;틀림\u0026quot;으로 뭉뚱그리지 않고 wrong_mapping, wrong_formula, wrong_aggregation으로 나눴기 때문에 \u0026ldquo;매핑 exemplar를 보강하자\u0026rdquo;, \u0026ldquo;계산식을 glossary에 명시하자\u0026rdquo; 식으로 구체적으로 움직일 수 있었다.\n80%는 시작이다 80%는 MVP 최소 기준이고, Phase 2 목표는 90%다. 그 사이에 아직 안 쓴 카드가 여러 장 남아 있다.\n지금 EX 80%는 로컬 YAML 25개 용어 + 규칙 기반 Router + hard rule로 달성한 수치다. 잔여 실패 6건은 전부 hard 난이도로, formula/JOIN 복잡도 문제다. 지금 구조에서는 추가 튜닝으로 올릴 수 있는 폭이 크지 않다.\nDataHub 연동 (v4) 이 바로 다음이다. synonym을 확장하고 linked_columns로 테이블/컬럼 매핑을 강화하면, 현재 wrong_mapping 4건 중 일부가 해결된다. 동의어 인식률은 현재 67%에서 90-95%까지 올라갈 것으로 본다. EX 기준으로는 +3-5%p 영역이다.\nFew-shot 확대 는 hard 쿼리에 직접 영향을 준다. 지금 학습 데이터는 30문항 테스트셋 + 추가 예시 쿼리 수준이다. 윈도우 함수, CTE, 다중 JOIN에 맞는 예시 쿼리를 충분히 추가하면 hard EX 37.5%가 올라갈 여지가 크다.\nTool Memory 자동 학습 은 Phase 1.5에서 활성화 예정이다. 사용자 질문 → SQL 실행 성공 → Q-SQL 쌍 자동 학습으로, 쓰면 쓸수록 정확도가 올라가는 구조다. 수동 few-shot 추가와 달리 스케일이 된다.\nLLM 분류기 전환 은 Phase 2 과제다. 현재 Router의 키워드 매칭 방식은 Fake Det Rate를 13.3%까지 내렸지만 구조적 한계가 있다. LLM 분류기로 전환하면 자연어 중의성 처리가 근본적으로 개선된다.\nApache AGE 그래프 쿼리 는 Phase 1.5-2 범위다. 현재 그래프 DB가 없어서 Multi-hop 추론이 안 된다. \u0026ldquo;A 공장 이슈가 B 제품 공급망에 미친 영향은?\u0026rdquo; 같은 관계 탐색 쿼리는 지금 구조로는 처리 불가다. AGE가 붙으면 RCQ(Relationship CQ) 영역이 열리고, Router의 DETERMINISTIC 경로에 Cypher 템플릿이 추가된다.\nPRD가 그리는 경로를 정리하면 이렇다.\n접근법 EX 현재 상태 GPT-4o 단독 ~51% 참고용 RAG 추가 (Vanna) ~70-75% 완료 온톨로지 + RAG ≥ 80% (MVP) 완료 (YAML) + DataHub + Few-shot + Tool Memory ≥ 90% (Phase 2) 예정 + Graphiti + Agent Memory ≥ 95% (Phase 3) 예정 현실적으로 DataHub 연동 + hard 쿼리 예시 보강으로 83-87% 구간은 도달 가능하다. 90%는 Tool Memory + LLM 분류기 전환까지 필요하고, 95%는 Graphiti까지 가야 한다.\n","permalink":"https://datanexus-kr.github.io/posts/datanexus/008-pdca-ex-80/","summary":"라우팅 설계를 붙인 뒤 30문항 벤치마크로 NL2SQL EX(Execution Accuracy)를 66.67%에서 80%까지 올렸다. 4사이클 동안 뭘 고쳤고 어디서 꺾였는지 정리한다.","title":"8. NL2SQL 정확도 66%에서 80%까지, 4번의 측정-개선 루프"},{"content":"용어 엔진 설계는 끝났다. \u0026ldquo;VIP 고객\u0026quot;이 뭔지, \u0026ldquo;순매출\u0026quot;이 어떤 산식인지 정의된 상태다.\n근데 여기서 막혔다.\n사용자가 \u0026ldquo;지난달 VIP 고객 매출 알려줘\u0026quot;라고 치면, 시스템 안에서 뭐가 돌아가야 하나.\n어디서 답을 가져올 건지부터 정해야 한다 답을 만들 수 있는 소스가 여러 개다. 그래프를 탈 수도 있다. SQL을 짜서 DW를 긁어올 수도 있고, 아예 벡터 검색으로 과거 질의를 가져올 수도 있다.\n어디로 보내느냐에 따라 답이 달라진다.\n\u0026ldquo;VIP 고객 매출\u0026quot;을 SQL로 보내면 Vanna가 테이블을 조합해서 숫자를 뽑는다. 그래프로 보내면 용어 정의와 관계부터 찾아온다. 세 군데 다 보내서 합치면 되지 않냐 싶다. 실제로 해보면… 바로 충돌 난다.\n같은 \u0026ldquo;이탈률\u0026quot;인데 마케팅 보고서 수치와 CRM 대시보드 수치가 다르다. 기준이 다른 거다. 사람이야 맥락을 보고 골라내지만, 에이전트한테는 그냥 서로 다른 숫자 두 개다.\n질문이 들어오면 누가 먼저 움직이나 질문이 들어오면 제일 먼저 Router에서 걸린다. 용어 메타데이터의 term_type 을 보고 metric 이면 SQL 쪽, concept 면 그래프 쪽으로 넘기는데, 여기서 잘못 보내면 뒤는 전부 틀어진다. 용어마다 타입이랑 연결 컬럼을 다 박아놨다. 그래서 그냥 읽고 분기하면 끝이다.\n…이게 없으면 어떻게 되냐. 결국 모델한테 \u0026ldquo;이거 SQL이야 그래프야?\u0026rdquo; 물어보게 된다. 몇 번 해보면 바로 느낌 온다. 오래 못 간다.\n제일 골치 아픈 건 Supervisor다.\n여러 소스에서 답이 올라왔는데 숫자가 다를 때, 누구 말을 들을 건지를 정해야 한다. 기준을 하나로 안 밀면 매번 싸운다. 마케팅이 \u0026ldquo;이탈률 12%\u0026ldquo;라고 하고 CRM이 \u0026ldquo;이탈률 8%\u0026ldquo;라고 하면, 산식이 다를 뿐 둘 다 틀린 건 아니다.\n그래서 그냥 우선순위를 박아버렸다. 이름은 HoT(Hierarchy of Truth)라고 붙였다. 용어 엔진의 표준 정의가 이기고, SQL 실행 결과가 그 다음, 벡터 검색은 맨 뒤다. 이게 없으면 충돌 날 때마다 모델한테 \u0026ldquo;둘 중 뭐가 맞아?\u0026ldquo;를 물어봐야 한다.\nGraph DBA는 Cypher 쿼리를 날리기 전에 스키마를 한 번 더 검증하는 역할이다. 미등록 용어로 쿼리를 짜면 실행 전에 거른다.\n아직 모르는 것 제일 불안한 건 복합 질문이다.\n\u0026ldquo;VIP 고객의 최근 3개월 구매 패턴을 분석해줘\u0026rdquo; 같은 건 그래프도 타야 하고 SQL도 돌려야 한다. Router가 term_type만 보고 분기하면 단일 질문은 대부분 맞을 것 같은데, 이런 게 들어오면 쪼개야 한다. 어디서 자르고 어떤 순서로 보내는지. 설계상으로는 그려놨는데 실제로 돌려봐야 감이 잡힐 것 같다.\nHoT도 걸린다. 정의가 6개월 전 거면 어쩔 건지. Staleness 감지를 넣어두긴 했는데 이건 나중 문제다.\nGraph DBA는 반대로 너무 막을까봐 걱정이다.\n다음은 실제 DB에서 돌려본다 하위 모듈 설계는 일단 끝냈다. 이제 sql-tutorial DB 21테이블로 A/B 테스트를 돌린다. EX(Execution Accuracy) +15%p는 찍혀야 한다. 안 찍히면 설계를 갈아엎어야 한다. 여기서 감으로 계속하다가 터지면 나중에 다 꼬인다.\n","permalink":"https://datanexus-kr.github.io/posts/datanexus/007-multi-agent-router/","summary":"용어 정의는 끝났다. 근데 질문이 들어왔을 때 그래프를 탈지 SQL을 짤지 벡터 검색을 돌릴지를 누가 정하나. 라우터를 설계하면서 부딪힌 것들.","title":"7. 질문이 들어오면, 라우팅은 누가 결정하나"},{"content":"Conway 유출이 나온 지 얼마 안 됐는데, Anthropic이 바로 실물을 꺼냈다. Claude Managed Agents.\n에이전트를 만들 때 늘 따라붙던 것들이 있다. 인프라, 상태 관리, 권한, 오케스트레이션. 이걸 전부 묶어서 관리형 런타임으로 올려버렸다. 프로토타입에서 프로덕션까지 몇 달 걸리던 작업을 며칠로 줄인다는 얘기도 나온다 (Managed Agents 공식 발표).\nConway 유출 때 예상했던 방향이긴 했다. 근데 이렇게 빨리, 그리고 이 정도 완성도로 나올 줄은 몰랐다.\n뭐가 나왔나 세션, 하네스, 샌드박스를 분리한 구조다.\n예전에는 이게 한 컨테이너 안에 다 들어 있었다. 그래서 한 번 터지면 세션 데이터 날아가고, 디버깅하려면 사용자 데이터가 들어 있는 환경까지 같이 건드려야 했다.\n이번에는 그걸 쪼갰다.\nAnthropic 쪽에서는 이걸 \u0026ldquo;두뇌와 손의 분리\u0026quot;라고 표현했는데, 표현이 꽤 정확하다 (Scaling Managed Agents: Decoupling the brain from the hands). 추론하는 쪽(두뇌)과 실제로 실행하는 쪽(손)을 따로 스케일링할 수 있게 만든 구조다.\n그리고 이름이 Agents다. 복수형.\n멀티 에이전트가 기본 전제라는 뜻이다. 작업이 복잡해지면 하위 에이전트를 띄워서 쪼개는 방식. 이미 Notion, Asana, Sentry 같은 데서 프로덕션에 넣었다고 한다.\n가격도 단순하다. 토큰 + 세션 시간당 과금. 대기 시간은 빠진다 (Managed Agents 소개 영상).\n이런 구조면 실험에서 배포까지 가는 속도는 꽤 빨라질 것 같다.\n하네스의 유통기한 엔지니어링 블로그에서 흥미로운 얘기가 하나 나온다 (Scaling Managed Agents (Engineering Blog)).\n하네스라는 게 결국 \u0026ldquo;모델이 못하는 걸 전제로 만든 코드\u0026quot;인데, 모델이 좋아지면 그 전제가 무너진다는 거다.\n실제로 Sonnet 4.5에서는 컨텍스트 불안 문제를 하네스로 보완했는데, Opus 4.5에서는 그 문제가 그냥 사라졌다고 한다.\n이러면 하네스를 경쟁력으로 보는 가정 자체가 깨진다.\n잘 짜는 게 경쟁력이었던 시기가 있었는데, 이제는 그게 계속 유지될 거라고 보기 어렵다.\n그래서 \u0026ldquo;메타 하네스\u0026quot;라는 얘기가 나온다. 구현은 바뀌어도 인터페이스는 유지되는 구조.\n근데 여기까지 오면 생각이 조금 달라진다.\n에이전트 루프를 잘 짜는 것 자체가 오래 버틸 수 있는 영역인가? 모델이 한 단계 올라갈 때마다 다시 손봐야 하는 구조라면, 투자 대비 유지력이 떨어진다.\n적어도 지금 시점에서는, 도메인 쪽이 더 오래 버틸 가능성이 높다.\nDataNexus가 안전한 이유 계속 반복되는 문제가 있다.\n같은 \u0026ldquo;이탈률\u0026quot;인데 팀마다 정의가 다르고,T_CUST_MST 같은 테이블은 이름만 보고는 사람도 헷갈린다.\nLLM이 이걸 알아맞히는 건 더 어렵다.\nDDL에는 이런 맥락이 없다.\n사람 머릿속에 있던 정의를 꺼내서 온톨로지로 만드는 쪽으로 간다.\n그리고 그걸 모델에 같이 넣는다.\n한 번만 붙여봐도 SQL이 완전히 다르게 나온다.\n근데 이 정의들은 외부에 공개돼 있지 않다. 크롤링으로 모을 수도 없다.\n예를 들어 \u0026ldquo;활성 고객\u0026rdquo; 하나만 해도마케팅팀은 30일, CRM팀은 90일, 어떤 팀은 \u0026ldquo;로그인 1회 이상\u0026quot;으로 잡는다.\n거의 내부에서 정해놓은 룰이다. 그래서 런타임으로 해결되는 종류가 아니다.\n배포 채널이 하나 늘었다 이걸 위협으로 보지는 않았다.\n오히려 배포 채널 하나가 더 생긴 느낌에 가깝다.\nManaged Agents 위에서 DataNexus 온톨로지 엔진을 돌린다고 생각하면, 굳이 인프라를 직접 들고 갈 이유가 줄어든다.\n이미 MCP 래핑을 계획해둔 상태였는데, 이 구조랑 자연스럽게 이어진다.\n범용 AI는 결국 범용이다.\n특정 회사의 DW 안에서\u0026ldquo;왜 이 수치가 이상한지\u0026quot;까지 설명하려면그 안에 쌓인 지표 산식과 팀별 해석 방식이 필요하다.\n그건 온톨로지 쪽이다.\n규제 산업이라는 벽 법률, 의료, 회계, 제조.\n이쪽은 장벽이 높다. 데이터 때문이 아니라 그쪽 로직 때문이다.\n예를 들어 병원 데이터를 생각해 보면, 외부에서 접근 가능한 정보만으로는 실제 로직을 구성하기가 거의 불가능하다.\n보험 청구 하나만 해도 내부 규칙이 겹겹이 쌓여 있다.\n글로벌 AI 회사 입장에서 이걸 전부 파고드는 건 효율이 안 맞는다.\nDW/BI도 비슷하다.\n테이블 수천 개, 약어 컬럼, 팀마다 다른 정의.\n그냥 데이터 문제가 아니라, 계속 해석이 붙는 쪽이다.\n인프라는 금방 따라잡힌다 재밌는 건 속도다.\nManaged Agents 발표 나오고 몇 시간 지나서핵심 기능을 그대로 구현한 오픈소스 프레임워크가 바로 올라왔다 (Multica).\n항상 비슷하게 흘러간다. 하나 뜨면 금방 복제된다.\n누가 인프라를 만들면, 곧바로 다른 쪽에서 비슷한 걸 만든다.\n이쪽은 계속 비슷해진다. 어디 걸 써도 큰 차이가 안 난다.\n방향 확인 인프라는 위로 올라간다. 아래에 남는 건 데이터랑 정의 쪽이다.\n다음은 MCP 래핑이다.\n","permalink":"https://datanexus-kr.github.io/posts/datanexus/006-managed-agents-and-ontology/","summary":"Conway 유출이 나온 지 얼마 안 돼서 Anthropic이 Claude Managed Agents를 정식 발표했다. 에이전트 인프라가 플랫폼에 흡수되는 흐름 속에서, DataNexus의 온톨로지가 왜 안전한지를 정리했다.","title":"6. 에이전트 인프라를 직접 안 만들어도 될 때, 하네스는 점점 무효화된다. 그러면 온톨로지는?"},{"content":" Claude 프롬프트 매우 빠르게 만드는 32가지 숏컷! 이런 커맨드는 따로 정의하지 않아도 Claude, ChatGPT, Gemini 같은 LLM이 자연스럽게 잘 알아듣는 패턴이예요. 왜냐구요? 훈련 데이터에 이미 수억 번 나왔기 때문이죱. instruction tuning, structured prompt 같은 과정과 설계 덕분입니다. 그러니 가뜩이나 바쁜 우리에게 시간을 줄여주는 방법이죠. — @lucas_flatwhite (원문: @rubenhassid)\n프롬프트를 길게 쓸수록 잘 나올 거라는 생각은 착각이다. 잘 설계된 명령어 하나가 서너 문단의 지시보다 정확한 결과를 뽑아낸다. 이 숏컷 목록은 LLM의 instruction tuning 과정에서 이미 학습된 패턴을 그대로 활용하기 때문에 별도의 프롬프트 엔지니어링 없이도 즉시 동작한다.\n출력 형태를 제어하는 명령어 32가지 중 실무에서 가장 빈번하게 쓰이는 축은 출력 형태를 바꾸는 그룹이다. /ELI5 는 복잡한 개념을 비전문가에게 전달할 때, /TLDR 은 장문의 보고서를 훑을 때, /EXEC SUMMARY 는 경영진 보고용 요약이 필요할 때 각각 효과적이다. /CHECKLIST 와 /STEP-BY-STEP 은 같은 정보를 실행 가능한 형태로 바꿔준다. /FORMAT AS 를 붙이면 표, JSON, 마크다운 등 원하는 포맷을 강제할 수 있다.\n핵심은 같은 질문이라도 출력 형태를 바꾸는 것만으로 쓰임새가 완전히 달라진다는 점이다. 회의록을 /TLDR 로 요약한 뒤 /CHECKLIST 로 변환하면 바로 업무 할당이 가능해진다.\n사고 품질을 높이는 메타인지 명령어 눈에 띄는 그룹은 AI의 사고 과정 자체를 제어하는 명령어들이다. /CHAIN OF THOUGHT 는 중간 추론 과정을 모두 드러내고, /FIRST PRINCIPLES 는 가정을 걷어내고 근본 원리부터 다시 쌓는다. /DELIBERATE THINKING 은 성급한 답변을 억제하고, /NO AUTOPILOT 은 뻔한 패턴 반복을 막는다.\n/REFLECTIVE MODE 와 /EVAL-SELF 는 생성된 답변을 모델 스스로 비판하게 만든다. /SYSTEMATIC BIAS CHECK 까지 더하면 편향까지 점검하는 셈이다. 단순 질의응답을 넘어 AI를 사고 파트너로 활용하려면 이 그룹을 의식적으로 조합해야 한다.\n역할과 관점을 전환하는 명령어 /ACT AS 는 가장 널리 알려진 역할 부여 패턴이고, /DEV MODE 와 /PM MODE 는 직군별 관점을 즉시 전환한다. /MULTI-PERSPECTIVE 와 /PARALLEL LENSES 는 하나의 사안을 여러 각도에서 동시에 조명한다. /SWOT 과 /COMPARE 는 의사결정 프레임워크를 바로 적용한다.\n/AUDIENCE 와 /TONE 을 조합하면 같은 내용을 초보자용 친절한 설명과 전문가용 기술 문서로 각각 뽑아낼 수 있다. /GUARDRAIL 은 답변의 범위를 명시적으로 제한하여 모델이 주제를 벗어나는 것을 방지한다.\n따로 외울 필요 없이, 필요한 상황에서 한 번씩 써보면 자연스럽게 체득된다. 프롬프트 작성 시간을 줄이면서도 결과 품질을 끌어올리는 가장 실용적인 접근법이다.\n핵심만 뽑으면\nLLM의 instruction tuning으로 이미 학습된 슬래시 명령어 패턴을 활용하면 별도 정의 없이 즉시 동작한다 /CHAIN OF THOUGHT, /FIRST PRINCIPLES, /NO AUTOPILOT 등 메타인지 명령어를 조합해 AI의 사고 품질 자체를 제어할 수 있다 출력 형태 전환(/TLDR → /CHECKLIST)과 역할 전환(/DEV MODE, /PM MODE)을 연결하면 하나의 입력으로 다양한 산출물을 생성한다 소스\nhttps://x.com/i/status/2041125496755470589\r","permalink":"https://datanexus-kr.github.io/curations/2026-04/2026-04-07-llm-prompt-slash-command-shortcuts/","summary":"별도 정의 없이 Claude, ChatGPT, Gemini에 던지면 즉시 작동하는 슬래시 명령어 32가지를 분류하고, 실무에서 조합해 쓰는 방법을 정리했다.","title":"LLM이 바로 알아듣는 32가지 프롬프트 숏컷 명령어"},{"content":" 한국투자증권 Open API의 공식 GitHub 레포입니다. 주요 내용을 정리하면: 개요: LLM(ChatGPT, Claude 등)과 Python 개발자 모두가 한투 Open API를 쉽게 활용할 수 있도록 만든 샘플 코드 모음입니다. Star 1k, Fork 509로 꽤 활발한 프로젝트네요. 핵심 구조: - examples_llm/ — LLM이 단일 API 기능을 탐색·호출하기 쉽도록 기능 단위로 분리 - examples_user/ — 사용자가 실제 자동매매에 활용할 수 있는 상품별 통합 예제 - MCP/ — MCP 서버 지원 (토픽에 claude, mcp 태그가 있음) 지원 카테고리: 국내주식, 국내채권, 국내선물옵션, 해외주식, 해외선물옵션, ELW, ETF/ETN 특징적인 부분: - REST API + WebSocket 실시간 시세 모두 지원 - kis_devlp.yaml로 실전/모의투자 환경 전환 - uv 패키지 매니저 기반 환경 설정\n증권사 인터페이스를 다루다 보면 문서 설명보다 실제 구동되는 코드가 절실할 때가 많다. 유용하다. 대형 금융사가 공개한 저장소는 이런 갈증을 해결하며 실무에 바로 쓰기 좋을 만큼 높은 완성도를 보여준다.\nAI 에이전트를 위한 구조적 설계 단순한 API 호출을 넘어 AI 에이전트가 도구로 활용하기 적합한 구조를 갖췄다. 기능별로 분할된 디렉토리는 외부 AI 모델이 특정 함수를 탐색하고 실행하는 과정을 돕는다. 명확하다. 최신 기술 트렌드를 반영해 MCP 서버를 지원하는 대목도 돋보인다.\n기존 방식은 사용자가 수백 페이지의 문서를 읽고 로직을 직접 구현해야 했다. 반면 이 저장소는 AI가 함수를 직접 호출하도록 유도해 개발 효율을 높인다. 편리하다. 데이터 엔지니어는 복잡한 파이프라인 구축 수고를 덜 수 있다.\n기초 자산 데이터를 연동할 때도 구조적 이점이 크다. API 응답 값이 정형화되어 있어 별도의 후처리 과정이 줄어든다. 깔끔하다. 작은 차이가 전체적인 개발 속도를 결정한다.\n실전 매매 환경의 확장성 패키지 매니저와 설정 파일을 활용해 개발 환경 구축 시간을 대폭 단축했다. 설정 파일 하나만 수정하면 실전 투자와 모의 투자 환경을 자유롭게 오갈 수 있다. 직관적이다. 주문은 REST API로 처리하고 시세는 웹소켓으로 실시간 수신하는 전형적인 구조를 따른다.\n국내외 주식을 포함해 채권과 선물옵션까지 다루는 카테고리가 방대하다. 파생 상품 데이터도 끊김 없이 호출하며 실제 자동매매에 즉시 투입 가능한 예제들을 제공한다. 개별 기능을 조합하는 번거로움이 사라진다.\n파이썬 기반 금융 서비스를 만드는 개발자에게는 훌륭한 기준점이 된다. 환경 설정부터 데이터 수신까지 매끄러운 흐름을 보여준다. 쾌적하다. 인프라 자원을 효율적으로 사용하면서도 빠른 응답 속도를 유지하는 점이 강점이다.\n공식 저장소의 예제를 실행해 보는 것만으로도 안정적인 자동매매 시스템의 기틀을 마련할 수 있다.\n핵심만 뽑으면\nLLM이 API를 도구로 호출하기 쉽도록 기능 단위로 분절된 디렉토리 구조 채택 MCP 서버 지원을 통해 Claude 등 최신 AI 모델과의 연동성 강화 uv 패키지 매니저와 YAML 설정을 활용한 신속한 실전-모의 투자 환경 전환 소스\nhttps://x.com/i/status/2039681334038442123\r","permalink":"https://datanexus-kr.github.io/curations/2026-04/2026-04-05-kis-open-api-official-github-analysis/","summary":"LLM 에이전트와 파이썬 환경에 최적화된 증권사 API 공식 샘플 코드의 구조를 분석한다.","title":"국내 대형 증권사 Open API 활용을 위한 공식 저장소 분석"},{"content":" CleanShot X에 돈을 내는 게 싫어서 macOS용 무료 오픈 소스 스크린샷 및 화면 녹화 도구를 만들었습니다. 저는 Linux에서 Flameshot을 몇 년 동안 사용해 왔는데, Mac으로 바꾸니까 무료로 쓸 만한 게 없더라고요. CleanShot X가 좋긴 한데, 스크린샷 하나에 29달러는 좀 아닌 것 같았어요. 그래서 제가 직접 만들었습니다. macshot - 네이티브 Swift/AppKit, Electron 없음, 가볍습니다. 기능: 캡처, 주석 추가, 복사/저장을 한 번에 18가지 주석 도구 (화살표, 도형, 텍스트, 픽셀화, 흐림 효과, 번호 매기기 마커, 이모티콘 스탬프 등) 화면 녹화 (MP4/GIF) (시스템 오디오 + 마이크) 자동 스티칭으로 스크롤 캡처 OCR 텍스트 추출 (30개 이상 언어) Google Drive, imgbb 또는 S3 호환 저장소에 업로드 한 번의 클릭으로 PII 자동 삭제 (이메일, 전화번호, API 키) 그라데이션 배경의 뷰티 모드 캡처 후 편집 + 여러 캡처 합성용 편집기 창 그 외 다양한 기능 설치: brew install sw33tlie/macshot/macshot 또는 GitHub 릴리스에서 DMG를 다운로드하세요. 완전 오픈 소스 (GPLv3): 이 작업을 꽤 오랫동안 해왔고, 방금 대규모 업데이트 (v3.4)를 출시했습니다. 다른 Mac 사용자들의 피드백을 받고 싶습니다.\n화면을 기록하려고 앱을 켰는데 메모리를 수백 메가씩 잡아먹는 걸 보면 한숨이 나온다. 유명 유료 앱은 훌륭하지만 매달 나가는 구독료나 결제 비용이 발목을 잡는다. 가벼우면서도 엔지니어의 가려운 곳을 긁어주는 오픈 소스 도구인 macshot이 반가운 이유다.\nNative Swift 기반의 퍼포먼스 Electron을 걷어내고 Swift와 AppKit으로만 빌드하여 메모리 점유율을 극단적으로 낮췄다. 시스템 자원을 거의 쓰지 않으면서 실행 속도는 즉각적이다. 웹 기술로 포장된 무거운 도구들과는 확연히 다른 네이티브 환경의 쾌적함을 선사한다.\n다른 OS에서 오픈 소스 툴을 쓰던 감각 그대로 직관적인 인터페이스를 제공한다. 툴이 편하다. 화살표나 텍스트 삽입 같은 주석 도구들이 풍부해 별도의 편집기를 띄울 필요가 없다. 흐림 효과나 픽셀화 기능까지 한 흐름에 담아내어 작업 단계를 대폭 줄였다.\nPII 자동 삭제와 워크플로우 민감 정보(PII)를 클릭 한 번으로 가려주는 기능은 보안 사고를 막아준다. 유용하다. 코드 리뷰를 위해 화면을 공유하거나 기술 문서를 작성할 때 수동으로 마스킹하던 번거로움이 사라진다. 이미지 내 텍스트를 즉각 추출해 클립보드에 담아주는 기능도 매끄럽게 작동한다.\n스크롤 이미지 생성은 OS 제조사의 프레임워크를 활용해 수직 또는 수평 이미지를 자연스럽게 합성한다. 아주 매끄럽다. 긴 로그 기록이나 웹 페이지 전체를 하나의 파일로 만드는 과정이 단순해진다. 화면 녹화는 영상과 움짤 형식을 지원하며 시스템 오디오와 마이크를 동시에 제어한다.\n저장소 연동과 오픈 소스의 가치 저장소 서비스와의 연동을 통해 촬영 즉시 업로드 링크를 생성한다. 빠르다. 팀원들과 이미지를 공유할 때 파일 전송 단계를 생략할 수 있어 워크플로우가 간결해진다. 터미널 명령어로 간단히 설치할 수 있는 배포 방식도 매력적이다.\n오픈 소스 라이선스를 따르는 프로젝트라 신뢰가 가며 최근 업데이트로 안정성도 확보했다. 든든하다. 멀티 모니터 환경에서도 끊김 없는 기능을 지원하며 다중 화면을 가로지르는 드래그 합성도 가능하다. 유료 앱 못지않은 완성도를 보여주는 강력한 대안이다.\n생산성 도구에 대한 고민을 덜어주는 네이티브 앱의 가치를 다시금 확인하게 된다.\n핵심만 뽑으면\nElectron 대신 Native Swift/AppKit을 사용하여 메모리 사용량을 8MB 수준으로 최적화함 PII 자동 삭제 및 S3 호환 저장소 연동 등 엔지니어의 실무 워크플로우를 고려한 기능 배치 Apple Vision 프레임워크를 활용한 고성능 스크롤 캡처 스티칭 구현 소스\nhttps://github.com/sw33tLie/macshot\r","permalink":"https://datanexus-kr.github.io/curations/2026-04/2026-04-05-macshot-native-macos-screenshot-tool-review/","summary":"구독료 부담을 덜어주면서도 강력한 기능을 제공하는 네이티브 기반 오픈 소스 macOS용 도구 macshot을 살펴본다.","title":"유료 앱의 대안으로 부상한 네이티브 macOS 도구 macshot"},{"content":"데이터 사전은 왜 6개월을 못 버티는가 차세대 정보계 프로젝트에서 여러 벤더와 1년 넘게 작업한 적이 있다. 초반에 데이터 사전을 야심차게 구축했다. \u0026ldquo;매출\u0026rdquo;, \u0026ldquo;원가\u0026rdquo;, \u0026ldquo;순매출\u0026quot;의 정의를 벤더별로 맞추고, 테이블 매핑까지 꼼꼼하게 채웠다. 반년쯤 지나니까 신규 테이블은 등록이 안 되고, 기존 정의는 스키마 변경을 반영하지 못하고, 부서 간 해석 차이는 그대로 방치됐다. \u0026ldquo;어차피 안 맞으니까\u0026quot;가 팀의 공식 입장이 됐다.\n카탈로그든 위키든 데이터 사전이든 이름만 다를 뿐 결과는 같다. 만들 때는 열심히 채우는데 유지보수를 못 해서 방치된다.\nKarpathy가 며칠 전 X에 LLM 지식 베이스 아이디어를 올렸고, 후속으로 전체 아키텍처를 GitHub Gist에 공개했다 (Karpathy LLM Wiki). 개인 지식 베이스 구축 얘기인데, DataNexus 카탈로그 만들면서 부딪힌 문제와 비슷했다.\nRAG는 매번 처음부터 시작한다 대부분의 LLM 문서 활용 방식은 RAG다. NotebookLM, ChatGPT 파일 업로드, 사내 문서 검색 시스템 전부 원본을 청크로 쪼개서 벡터 검색하는 방식이다. 잘 되는 것 같은데, 한 가지 불만이 있다. 어제 다섯 개 문서를 종합해서 답변한 내용을 오늘 다시 물어도 LLM은 같은 검색부터 시작한다. 아무것도 기억하지 않는다.\nDataNexus에서도 이게 걸렸다. 1편\r에서 온톨로지를 RAG Store에 넣어서 맥락을 주입하는 구조를 설계했는데, 온톨로지가 실제 스키마와 달라지면 NL2SQL이 틀린 SQL을 만든다. RAG Store 자체를 최신으로 유지하는 문제는 아직 풀지 못했다.\n위키를 LLM한테 맡긴다 Karpathy의 접근은 간단하게 말하면 이렇다. RAG처럼 매번 원본을 뒤지는 대신, LLM이 마크다운 위키를 직접 관리하게 한다. 원본 자료(Raw Sources)는 사람이 넣고, LLM이 읽어서 위키(Wiki)에 정리한다. 위키의 구조와 규칙을 정의하는 설정 문서(Schema)가 있어야 LLM이 아무렇게나 쓰지 않고 일관된 방식으로 정리한다.\n새 소스가 들어오면(Ingest) LLM이 요약하고 관련 페이지 10~15개를 갱신한다. 위키에 질문하면(Query) 답변 과정에서 나온 발견이 다시 위키에 기록된다. 주기적으로 모순이나 오래된 정보를 잡아내는 점검(Lint)도 돌린다.\nKarpathy 본인은 Obsidian을 열어두고 LLM 에이전트와 나란히 작업한다고 한다. Obsidian이 IDE, LLM이 프로그래머, 위키가 코드베이스라는 비유를 썼는데, 개발자한테는 바로 와닿는 비유다.\n여기서 내가 눈여겨본 건 Schema다. 3편\r에서 DataHub의 Business Glossary를 온톨로지 저장소로 쓰면서 관계 유형이 4가지밖에 안 돼서 고생했다는 얘기를 썼다. Karpathy의 Schema도 비슷한 역할이다. LLM한테 \u0026ldquo;이 용어는 이런 관계 유형으로만 연결해\u0026quot;라고 알려주는 규칙서. 이게 없으면 LLM이 제멋대로 정리해서 오히려 엉망이 될 수 있다.\n진짜 병목은 유지보수다 데이터 사전이든 위키든 작성은 어렵지 않다. 프로젝트 초반에 사람 몇 명 붙이면 된다. 문제는 그 다음이다. 페이지가 100개를 넘어가면 교차참조 갱신, 요약 업데이트, 모순 탐지에 드는 시간이 눈에 띄게 불어난다. 사람은 이 시점에서 슬슬 손을 놓는다.\nDataNexus도 여기가 걱정이다. 용어 등록, 관계 설정, DozerDB 동기화까지는 만들었다. 근데 DW 스키마는 오픈 이후에도 계속 바뀐다. 단계별 릴리즈가 이어지면 테이블이 추가되고, \u0026ldquo;순매출\u0026rdquo; 계산식에 새로운 차감 항목이 생기기도 한다. 이걸 카탈로그에 반영하는 걸 누가 할 것이냐가 문제다.\nLLM이라면 파일 15개를 동시에 업데이트할 수 있다. DataHub에서 이미 MCL(Metadata Change Log) 이벤트를 발행하고 있으니, LLM이 이 이벤트를 받아서 영향받는 용어 페이지를 갱신하고 교차참조를 업데이트하는 구조가 가능하다. 4편\r에서 설계한 SKOS 호환 레이어의 규칙이 Schema 역할을 하면 된다.\n1편에서 \u0026ldquo;변경을 감지해서 자동으로 RAG Store를 갱신하는 파이프라인이 필요하다\u0026quot;고 썼는데, 그때는 막연했다. Karpathy의 Ingest/Query/Lint 패턴을 보고 나서야 그 파이프라인의 밑그림이 잡혔다.\n물론 이게 바로 될 거라고 생각하지는 않는다. LLM이 온톨로지를 자동 갱신할 때 틀린 관계를 만들 수도 있고, 도메인 전문가의 검수가 어디까지 필요한지도 아직 모른다. 이건 메타데이터 변경 감지 파이프라인을 만들면서 부딪혀 볼 문제다.\n사람이 못 하는 일 Karpathy가 1945년 Memex까지 끌어온 건, 이 문제가 새로운 게 아니라는 얘기일 거다. 80년 전 구상인데 사람이 관리 비용을 감당 못 해서 매번 실패했다.\nKarpathy도 코드 짜는 데 쓰던 LLM 토큰을 요즘은 지식 정리에 더 많이 쓴다고 했는데, DataNexus 만들면서 나도 그쪽으로 가고 있다. 용어 정리, 매핑, 해석 차이 맞추기. 사람이 하면 6개월이면 포기한다. LLM한테 맡기면 어떻게 될지 실험해 볼 생각이다.\nDataNexus를 설계하고 구축하는 과정을 기록합니다. GitHub\r| LinkedIn\r","permalink":"https://datanexus-kr.github.io/posts/datanexus/005-llm-wiki-and-metadata-maintenance/","summary":"RAG는 매번 처음부터 답을 찾는다. Karpathy는 LLM이 위키를 직접 유지보수하게 해서 지식이 쌓이는 구조를 제안했다. DataNexus의 온톨로지 카탈로그가 방치되지 않으려면 같은 원리가 필요하다.","title":"5. 메타데이터 유지보수를 자동화하는 방법: Karpathy의 LLM Wiki 구조"},{"content":"\rGEO 최적화 Guide — 전체 시리즈\n1. GEO란 무엇인가 - SEO 너머의 AI 인용 전략\r2. AI마다 인용하는 소스가 다르다\r3. On-Site GEO 기술 구조 - 상품 DB에서 JSON-LD까지\r4. Off-Site GEO - 공식 사이트를 안 보는 AI에게 선택받는 법 ← 현재 글\r5. AEO - 코딩 에이전트가 읽는 문서는 왜 다른가\rJSON-LD를 넣었는데 왜 블로그가 인용되나 3편\r까지 On-Site GEO를 다뤘다. 상품 DB에서 JSON-LD를 뽑아내고, SSR로 HTML \u0026lt;head\u0026gt;에 꽂고, Rich Results Test로 검증까지 했다. 기술적으로 빠진 게 없다.\n근데 ChatGPT에 \u0026ldquo;○○ 브랜드 추천 제품\u0026quot;을 물어보면 공식 사이트가 아니라 네이버 블로그와 TripAdvisor가 인용된다. Perplexity에서는 Reddit 스레드가 출처로 올라온다.\n자사 사이트를 아무리 잘 만들어도, AI가 주로 보는 곳이 외부 채널이면 효과가 절반이다.\n2편\r에서 정리한 플랫폼별 인용 소스를 다시 보면 이렇다:\n플랫폼 1순위 인용 소스 비중 ChatGPT 디렉토리/리스팅 (Yelp, G2 등) 49% Perplexity Reddit/커뮤니티 31% Gemini 공식 사이트 52% Google AIO YouTube 1위 도메인 Gemini 빼면 공식 사이트 비중이 크지 않다. ChatGPT 인용의 절반이 외부 디렉토리에서 나온다. 이 영역을 안 건드리면 인용 점유율의 반을 그냥 놓치는 셈이다.\n이게 Off-Site GEO다.\nOff-Site GEO는 뭐가 다른가 1편\r에서 On-Site와 Off-Site를 간단히 구분했다. 좀 더 파보면 이렇다.\nOn-Site GEO는 자사 사이트를 AI가 읽기 좋게 만드는 거다. JSON-LD, Schema.org, SSR. 개발팀이 코드를 고쳐서 해결한다.\nOff-Site GEO는 방향이 다르다. AI가 참고하는 외부 채널에서 브랜드를 관리해야 한다. 디렉토리 프로필, 커뮤니티 언급, YouTube 영상. 여기는 마케팅팀과 PR의 영역이다.\n항목 On-Site GEO Off-Site GEO 대상 자사 도메인 외부 플랫폼 핵심 기술 JSON-LD, SSR, FAQ Schema 디렉토리 관리, 커뮤니티, YouTube 담당 개발팀 마케팅 / PR / 브랜드 통제 수준 높음 (직접 수정) 낮음 (간접 영향) 효과 플랫폼 Gemini (52%) ChatGPT, Perplexity, AIO 둘 중 하나만 하면 안 된다. On-Site로 공식 데이터 품질을 높이고, Off-Site로 외부 채널의 브랜드 일관성을 맞춰야 한다. 세트로 움직여야 효과가 난다.\n플랫폼별 Off-Site 전략 ChatGPT: 디렉토리와 리스팅이 절반 ChatGPT 인용의 49%가 Yelp, TripAdvisor, G2, Capterra 같은 서드파티 디렉토리에서 나온다 (Yext). 자사 사이트보다 디렉토리 프로필이 먼저 인용된다.\n왜 이런 일이 생기냐면, ChatGPT는 자체 검색 인덱스가 약하다. Bing 검색 레이어에 의존하는데, Bing이 디렉토리 사이트의 도메인 권위도를 높게 친다. 디렉토리에 올라간 정보가 ChatGPT 답변에 먼저 반영된다.\n바로 할 수 있는 것:\n업종별 핵심 디렉토리(Yelp, Google Business, G2, Capterra, TripAdvisor 등)에 프로필이 있는지 확인한다. 없으면 만들고, 있으면 정보가 최신인지 점검 NAP 일관성 을 맞춘다. Name, Address, Phone이 모든 디렉토리에서 동일해야 한다. \u0026ldquo;주식회사 ○○\u0026ldquo;과 \u0026ldquo;(주)○○\u0026ldquo;이 섞여 있으면 AI는 별개 엔티티로 인식할 수 있다 리뷰를 관리한다. AI는 리뷰 수와 평점을 신뢰 지표로 쓴다. 리뷰가 0개인 프로필은 인용 가능성이 낮다 Perplexity: Reddit과 커뮤니티가 소스 Perplexity 인용의 31%가 Reddit을 포함한 커뮤니티 스레드에서 온다. 공식 발표보다 실사용자 토론을 더 신뢰한다.\n단순히 Reddit에 글을 쓰라는 얘기가 아니다. Perplexity가 Reddit을 좋아하는 이유는 질문-답변 구조 가 AI 파싱에 최적화되어 있기 때문이다. \u0026ldquo;이 제품 어때?\u0026rdquo; → \u0026ldquo;6개월 썼는데 ○○은 좋고 ○○은 별로\u0026rdquo; 같은 대화가 AI 입장에서 가장 인용하기 쉬운 포맷이다.\n여기서 신경 쓸 것:\n자사 브랜드나 카테고리가 언급되는 서브레딧을 파악하고 정기적으로 모니터링한다 자사 제품 관련 질문에 실질적으로 도움이 되는 답변을 단다. 광고성 글은 Reddit 커뮤니티에서 즉시 다운보트 당한다 한국 시장은 좀 다르다. Reddit 대신 디시인사이드, 클리앙, 뽐뿌가 비슷한 역할을 한다. Perplexity가 한국어 쿼리에서 이 사이트들을 얼마나 인용하는지는 아직 데이터가 부족하다. 직접 테스트해볼 영역이다 Google AI Overview: YouTube가 급부상 Google AI Overview에서 YouTube가 인용 1위 도메인이다 (Ahrefs Brand Radar). 반년 사이에 점유율이 34% 늘었다.\n2편에서도 짚었는데, 인용되는 영상의 특징이 의외다. 조회수 1,000도 안 되는 영상이 인용되고, 좋아요 수십 개짜리도 수두룩하다. AI가 보는 건 인기도가 아니라 정보가 얼마나 잘 정리되어 있느냐다.\n인용 잘 되는 영상을 보면 패턴이 있다:\n요소 설명 인용 기여도 타임스탬프/챕터 영상 내 구간별 주제 구분 높음 설명란 구조화 목차, 링크, 핵심 내용 요약 높음 명확한 제목 질문형 또는 \u0026ldquo;How to\u0026rdquo; 형식 중간 자막/트랜스크립트 자동 생성이라도 있으면 파싱 가능 중간 조회수/좋아요 인기 지표 낮음 이미 올린 영상이라도 설명란에 타임스탬프를 넣으면 AI 인용 가능성이 올라간다. \u0026ldquo;이 영상에서 다루는 내용: 1. ○○ 2. ○○\u0026rdquo; 식으로 목차를 깔고, 관련 링크를 배치하면 된다. 제목은 \u0026ldquo;○○ 하는 법\u0026rdquo;, \u0026ldquo;○○ vs ○○ 비교\u0026rdquo; 같은 검색 의도가 명확한 형태가 유리하다.\n그 전에 먼저: robots.txt 점검 Off-Site를 챙기기 전에 확인할 게 하나 있다. 자사 사이트가 AI 크롤러를 막고 있지는 않은지.\nrobots.txt에서 GPTBot이나 PerplexityBot을 차단하면, 해당 AI는 자사 사이트를 크롤링하지 못한다. On-Site GEO가 완벽해도 읽을 수 없으면 의미가 없다.\n2편에서 다뤘던 경쟁사 robots.txt 분석을 직접 해볼 수 있는 도구를 만들었다. 도메인 리스트를 넣으면 AI 크롤러 10개의 허용/차단 현황을 히트맵으로 보여준다.\nGoogle Colab에서 실습하기\rAPI 키 없이 Python 표준 라이브러리만으로 돌아간다. 경쟁사 도메인을 바꿔가며 업계 전체 현황을 파악할 수 있다.\nrobots.txt에서 뭘 읽어낼 수 있나 경쟁사가 GPTBot을 차단하고 있다면, 그 AI 플랫폼에서 우리가 인용될 확률이 상대적으로 높아진다. 경쟁자가 빠진 자리니까.\n반대로 경쟁사가 전면 개방하고 있는데 우리만 차단하고 있다면, AI 검색에서 경쟁사만 노출되고 우리는 안 보인다.\nGPTBot을 차단해도 ChatGPT-User(브라우징 모드)는 별도 User-Agent다. 브라우징 모드에서는 여전히 접근 가능할 수 있다. Google-Extended를 차단해도 기본 Googlebot은 영향 없다. 검색 노출은 유지하면서 AI 학습만 차단하는 식으로 세분화할 수 있다.\n# 검색은 허용하되 AI 학습만 차단하는 예시 User-agent: Googlebot Allow: / User-agent: Google-Extended Disallow: / User-agent: GPTBot Disallow: / User-agent: ChatGPT-User Allow: / 이 설정이면 구글 검색에는 정상 노출되지만, Gemini AI 학습과 ChatGPT 학습 데이터에서는 제외된다. ChatGPT 브라우징 모드에서는 접근을 허용해서 실시간 인용은 가능하게 두는 식이다.\n업종별 Off-Site 채널 우선순위 업종마다 AI가 주로 참고하는 외부 채널이 다르다.\n업종 1순위 Off-Site 채널 2순위 비고 커머스/유통 Google Business + 디렉토리 YouTube 리뷰 상품 카탈로그 보호 vs AI 노출 균형 SaaS/B2B G2, Capterra 리뷰 Reddit (r/SaaS 등) 리뷰 수가 인용 확률을 직접 좌우 호텔/여행 TripAdvisor, Booking YouTube 투어 가격/가용성 데이터의 신선도가 핵심 식품/소비재 커뮤니티 리뷰 YouTube 먹방/리뷰 한국은 네이버 블로그 영향이 여전히 큼 금융/핀테크 뉴스/미디어 전문 포럼 규제 이슈로 AI 크롤러 차단하는 경우 많음 커머스가 특히 어렵다. 상품 가격과 재고 정보를 AI에 노출하면 경쟁사가 실시간으로 가져갈 수 있다. 차단하면 AI 검색에서 사라진다. 열어서 AI 검색에 노출될 것인지, 닫아서 카탈로그를 지킬 것인지. 정답은 없고 업종 내 경쟁 상황에 따라 다르다.\nOff-Site GEO 체크리스트 바로 실행할 수 있는 것부터:\n이번 주\n자사 robots.txt에서 AI 크롤러 차단 여부 확인 → Colab 분석기\r로 진단 경쟁사 3곳의 robots.txt 비교 분석 주요 디렉토리(Google Business, 업종별 핵심 디렉토리)에 프로필 존재 여부 확인 이번 달\n디렉토리 프로필 정보 업데이트 (NAP 일관성 확인) 기존 YouTube 영상에 타임스탬프/챕터/설명란 구조화 적용 자사 브랜드가 언급되는 커뮤니티/서브레딧 목록 작성 분기\nAI 플랫폼별 인용 모니터링 체계 구축 Off-Site 채널별 브랜드 일관성 감사 robots.txt 정책을 GEO 전략에 맞게 재설계 ","permalink":"https://datanexus-kr.github.io/guides/geo-optimization/004-offsite-geo-strategy/","summary":"On-Site GEO를 완벽하게 적용해도 AI 인용의 절반은 외부 채널에서 결정된다. 플랫폼별 Off-Site 전략과 robots.txt 진단법을 다룬다.","title":"4. Off-Site GEO - 공식 사이트를 안 보는 AI에게 선택받는 법"},{"content":"\rGEO 최적화 Guide — 전체 시리즈\n1. GEO란 무엇인가 - SEO 너머의 AI 인용 전략\r2. AI마다 인용하는 소스가 다르다\r3. On-Site GEO 기술 구조 - 상품 DB에서 JSON-LD까지 ← 현재 글\r4. Off-Site GEO - 공식 사이트를 안 보는 AI에게 선택받는 법\r5. AEO - 코딩 에이전트가 읽는 문서는 왜 다른가\rJSON-LD를 어디서 만들어서 어디에 넣느냐 이전 글\r에서 플랫폼별 인용 소스가 다르다는 걸 확인했다. Gemini는 공식 사이트를, ChatGPT는 디렉토리를, Perplexity는 커뮤니티를 선호한다. 공통점이 하나 있다. 어떤 플랫폼이든 구조화 데이터가 있는 페이지의 인용 확률이 높다는 거다.\n그래서 On-Site GEO의 기술적 핵심은 결국 이 질문으로 수렴한다. 상품 마스터 DB에 있는 데이터를 어떻게 가공해서 HTML \u0026lt;head\u0026gt;에 JSON-LD로 꽂을 것인가.\n단순해 보이지만, 실제로 손대보면 얽혀 있는 게 한둘이 아니다. 상품 DB 필드명은 약어 투성이고, AI가 이해할 수 있는 속성은 DB에 없고, SPA로 만들어진 사이트는 크롤러가 JSON-LD를 못 읽는다. 이 글에서는 이 문제들을 어떤 구조로 풀 수 있는지 다룬다.\nGEO 시스템의 동심원 구조 GEO 시스템은 안쪽에서 바깥으로 확장되는 4개 레이어로 이루어진다.\n레이어 구성요소 역할 Core 상품 마스터 DB SSOT(Single Source of Truth). 모든 데이터의 원천 Channel 웹사이트 / 모바일 앱 JSON-LD 삽입, SSR 렌더링 API 상품 조회 API AI 에이전트가 호출할 수 있는 인터페이스 Agent ChatGPT / Gemini / Perplexity 최종 소비자 접점 Core에서 시작해서 Channel을 거쳐 Agent까지 데이터가 흘러간다. 각 레이어를 지날 때마다 데이터의 형태가 바뀐다. DB의 raw 필드가 구조화된 JSON-LD가 되고, 그게 AI 답변의 인용 출처가 된다.\n여기서 놓치기 쉬운 게 API 레이어다. JSON-LD만 잘 넣으면 되는 거 아닌가 싶지만, ChatGPT Plugins이나 MCP(Model Context Protocol) 같은 AI 에이전트 연동까지 고려하면 별도의 API 레이어가 필요하다. 지금 당장은 아니더라도 설계 단계에서 고려해두면 나중에 덜 고생한다.\n데이터 파이프라인 3단계 상품 description을 통째로 관리하지 않고, 필드 단위로 분해하면 AI가 정확히 인용한다. 이게 파이프라인의 핵심 아이디어다.\n1단계: DB 정제 - 기존 필드 매핑 기존 상품 마스터 DB에서 Schema.org 필드로 매핑하는 단계다. 새 데이터를 만드는 게 아니라 있는 데이터를 정리하는 거다.\nDB 필드 → Schema.org 필드 ───────────────────────────────────────── PROD_NM → name BRND_CD (코드 변환) → brand.name GTIN_13 → gtin13 PRC_AMT → offers.price STCK_YN → offers.availability IMG_URL → image CTG_NM → category 필드 수는 업종에 따라 15~18개 정도다. 대부분 이미 DB에 있는 값이라 개발 공수가 크지 않다. 다만 코드 값을 사람이 읽을 수 있는 값으로 변환하는 작업이 필요하다. BRND_CD = P1042를 brand.name = \u0026quot;○○식품\u0026quot;으로 바꿔야 AI가 이해한다.\n이 단계에서 가장 많이 막히는 게 GTIN이다. GS1 표준 식별자인데, 같은 상품이라도 용량이나 맛이 다르면 GTIN이 달라야 한다. \u0026ldquo;초코스틱 오리지널\u0026quot;과 \u0026ldquo;초코스틱 아몬드\u0026quot;를 하나의 대표코드로 묶어놓으면 AI는 둘을 구분하지 못한다.\n2단계: LLM 추출 - AI 기반 속성 자동 생성 DB에 없는데 AI 인용에 필요한 속성들이 있다. 타겟 사용자, 사용 상황, 감성 키워드 같은 것들이다. 이걸 사람이 일일이 쓰면 SKU가 수천 개일 때 현실적으로 불가능하다.\nLLM이 기존 상품 설명, 리뷰, 카테고리 정보를 읽고 자동으로 추출하게 한다.\n소스 필드 설명 예시 DB @type Schema.org 유형 Product DB name 상품명 그램 16 DB gtin13 GS1 식별자 8801056038800 LLM targetUser 타겟 사용자 대학생, 직장인 LLM occasion 사용 상황 입학 선물, 업무용 LLM sentiment 감성 키워드 가벼운, 세련된 LLM nutrition 영양 정보 무설탕 LLM safety 안전 정보 CAS 9002-88-4 LLM 추출 필드는 업종마다 다르다. 식품이면 영양정보와 원재료가 핵심이고, 호텔이면 부대시설과 체크인 시간이 중요하다. 화학/B2B라면 물성 데이터와 인증 정보가 들어간다.\n이 단계에서 1015개 필드가 추가된다. 1단계와 합치면 상품 하나에 2533개 필드가 구조화되는 셈이다.\n3단계: JSON-LD 출력 - 자동 변환 및 SSR 배포 1단계와 2단계에서 모인 필드를 Schema.org 규격의 JSON-LD로 변환하고, SSR을 통해 HTML \u0026lt;head\u0026gt;에 자동 주입한다.\n{ \u0026#34;@context\u0026#34;: \u0026#34;https://schema.org\u0026#34;, \u0026#34;@type\u0026#34;: \u0026#34;Product\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;초코스틱 오리지널\u0026#34;, \u0026#34;gtin13\u0026#34;: \u0026#34;8801234567890\u0026#34;, \u0026#34;brand\u0026#34;: { \u0026#34;@type\u0026#34;: \u0026#34;Brand\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;○○식품\u0026#34; }, \u0026#34;description\u0026#34;: \u0026#34;초콜릿 코팅된 바삭한 스틱 과자. 1봉 46g 기준 200kcal.\u0026#34;, \u0026#34;offers\u0026#34;: { \u0026#34;@type\u0026#34;: \u0026#34;Offer\u0026#34;, \u0026#34;price\u0026#34;: 1500, \u0026#34;priceCurrency\u0026#34;: \u0026#34;KRW\u0026#34;, \u0026#34;availability\u0026#34;: \u0026#34;https://schema.org/InStock\u0026#34; }, \u0026#34;nutrition\u0026#34;: { \u0026#34;@type\u0026#34;: \u0026#34;NutritionInformation\u0026#34;, \u0026#34;calories\u0026#34;: \u0026#34;200 calories\u0026#34;, \u0026#34;servingSize\u0026#34;: \u0026#34;1봉 (46g)\u0026#34; } } 이 JSON-LD가 \u0026lt;head\u0026gt; 태그 안에 들어가면 1편\r에서 다룬 Invisible GEO 가 완성된다. 사용자 눈에는 안 보이지만 AI와 검색엔진이 파싱한다.\n식품 상품의 GEO 적용 전후가 어떻게 달라지는지 데모로 확인할 수 있다.\nDemo - JSON-LD Before/After Description 작성 4대 원칙 파이프라인에서 가장 사람 손이 많이 타는 부분이 상품 설명(description)이다. AI가 인용하기 좋은 description에는 패턴이 있다.\n사실 기반 - 객관적 정보만 넣는다. \u0026ldquo;업계 최고\u0026quot;나 \u0026ldquo;고객 만족 1위\u0026rdquo; 같은 광고 문구는 AI가 무시한다.\n100~300자 - AI가 참조하기 적절한 길이다. 너무 짧으면 맥락이 없고, 너무 길면 핵심이 묻힌다.\n자연어 키워드 - Princeton/Georgia Tech 연구\r에서 확인된 것처럼 키워드를 반복 삽입하면 AI 가시성이 오히려 떨어진다. 자연스러운 문장 안에 키워드를 녹여야 한다.\nSKU별 고유성 - 같은 템플릿을 복사해서 상품명만 바꾸면 AI는 중복 콘텐츠로 판단한다. 상품마다 고유한 설명이 있어야 한다.\n\u0026lt;!-- 나쁜 예: 광고 문구 + 키워드 반복 --\u0026gt; \u0026lt;meta name=\u0026#34;description\u0026#34; content=\u0026#34;회사소개\u0026#34;/\u0026gt; \u0026lt;!-- 좋은 예: 사실 기반, 자연어, 적절한 길이 --\u0026gt; \u0026lt;meta name=\u0026#34;description\u0026#34; content=\u0026#34;○○케미칼은 글로벌 석유화학 전문기업으로, 연간 매출 15조원 규모의 PE/PP 제품을 50개국에 공급합니다. ESG 경영과 탄소중립 2050을 선도합니다.\u0026#34;/\u0026gt; SSR이 필수인 이유 JSON-LD를 만들었어도 AI 크롤러가 못 읽으면 소용없다. 여기서 SPA(Single Page Application)가 발목을 잡는다.\nSPA는 브라우저에서 JavaScript를 실행해야 콘텐츠가 렌더링된다. 사람 눈에는 정상으로 보이지만, GPTBot이나 Google-Extended 같은 AI 크롤러는 대부분 JS를 실행하지 않는다. \u0026lt;head\u0026gt;에 JSON-LD를 넣었어도 서버가 빈 HTML을 보내면 크롤러 입장에서는 없는 거나 마찬가지다.\nSSR(Server-Side Rendering)로 전환하면 서버에서 완성된 HTML을 보내기 때문에 크롤러가 JS 실행 없이 JSON-LD를 바로 읽는다.\nNext.js App Router 기준으로 보면 이렇다:\n// app/product/[id]/page.tsx export default async function ProductPage({ params }) { const product = await fetchProduct(params.id); const jsonLd = { \u0026#34;@context\u0026#34;: \u0026#34;https://schema.org\u0026#34;, \u0026#34;@type\u0026#34;: \u0026#34;Product\u0026#34;, \u0026#34;name\u0026#34;: product.name, \u0026#34;gtin13\u0026#34;: product.gtin, \u0026#34;brand\u0026#34;: { \u0026#34;@type\u0026#34;: \u0026#34;Brand\u0026#34;, \u0026#34;name\u0026#34;: product.brand }, \u0026#34;description\u0026#34;: product.description, \u0026#34;image\u0026#34;: product.imageUrl, \u0026#34;offers\u0026#34;: { \u0026#34;@type\u0026#34;: \u0026#34;Offer\u0026#34;, \u0026#34;price\u0026#34;: product.price, \u0026#34;priceCurrency\u0026#34;: \u0026#34;KRW\u0026#34;, \u0026#34;availability\u0026#34;: \u0026#34;https://schema.org/InStock\u0026#34;, \u0026#34;url\u0026#34;: product.pageUrl } }; return ( \u0026lt;\u0026gt; \u0026lt;script type=\u0026#34;application/ld+json\u0026#34; dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /\u0026gt; \u0026lt;ProductDetail product={product} /\u0026gt; \u0026lt;/\u0026gt; ); } 서버에서 fetchProduct로 DB 데이터를 가져오고, JSON-LD 객체를 만들어서 \u0026lt;script\u0026gt; 태그로 주입한다. 이 HTML이 크롤러에게 그대로 전달된다.\nSSR 도입이 부담스러운 경우, 과도기적으로 Google Tag Manager(GTM)를 활용해 JSON-LD를 주입하는 방법도 있다. 완전한 SSR보다는 효과가 떨어지지만 SPA를 당장 전환할 수 없을 때 쓸 수 있는 우회 방안이다.\nSSR 적용 시 주의점 항목 장점 단점 극복 방안 SEO 최적화 크롤러가 JS 없이 즉시 인식 초기 개발 비용 SDK/공통 모듈 제공 데이터 반영 DB 변경 시 자동 업데이트 서버 부하 증가 Redis 캐싱 + ISR 활용 중앙 관리 전체 사이트 일괄 적용 개발팀 의존도 관리 콘솔에서 비개발자 관리 검증 자동화 빌드 시 유효성 검증 포함 레거시 시스템 전환 GTM 하이브리드 병행 서버 부하는 Redis 캐싱과 ISR(Incremental Static Regeneration)로 상당 부분 해소된다. 상품 정보가 바뀌지 않는 한 캐시된 HTML을 그대로 쓰면 된다.\n데이터 신선도가 인용을 좌우한다 구조화를 잘 해놔도 오래된 데이터는 밀린다.\nPerplexity에서 높은 인용을 받은 페이지를 분석해보면, 4분의 3 이상이 한 달 이내에 업데이트된 것이었다. ChatGPT Shopping은 피드를 15분 간격으로 갱신한다 (OpenAI). 석 달 넘게 손 안 댄 페이지는 AI 인용 순위에서 밀려날 가능성이 높다.\n신선도 관리 기준을 잡으면 이렇다:\n핵심 데이터 (가격, 재고, 프로모션): 24시간 이내 갱신 일반 데이터 (상품 설명, 이미지): 7일 이내 갱신 정적 데이터 (브랜드 정보, 회사 소개): 월 1회 점검 sitemap.xml에서 lastmod 날짜를 실제 업데이트 시점에 맞춰 갱신하고, IndexNow API로 변경 사항을 검색엔진에 즉시 알려주는 것도 효과가 있다.\n// next-sitemap.config.js module.exports = { siteUrl: \u0026#39;https://www.example.com\u0026#39;, generateRobotsTxt: true, changefreq: \u0026#39;daily\u0026#39;, transform: async (config, path) =\u0026gt; ({ loc: path, changefreq: path.includes(\u0026#39;/product/\u0026#39;) ? \u0026#39;daily\u0026#39; : \u0026#39;weekly\u0026#39;, priority: path.includes(\u0026#39;/product/\u0026#39;) ? 0.9 : 0.5, lastmod: new Date().toISOString(), }), }; 검증 - 넣었으면 확인해야 한다 JSON-LD를 넣었다고 끝이 아니다. 실제로 크롤러가 잘 읽는지 확인해야 한다.\nGoogle Rich Results Test - search.google.com/test/rich-results\r에서 URL을 입력하면 구조화 데이터가 정상 인식되는지 바로 확인할 수 있다.\ncurl로 크롤러 시뮬레이션 - AI 크롤러의 User-Agent로 직접 요청을 보내서 JSON-LD가 HTML에 포함되어 오는지 확인한다.\n# GPTBot으로 요청 curl -A \u0026#34;GPTBot\u0026#34; https://www.example.com/product/12345 | grep \u0026#34;application/ld+json\u0026#34; # HTML 소스에서 JSON-LD 추출 curl -s https://www.example.com/product/12345 \\ | grep -oP \u0026#39;\u0026lt;script type=\u0026#34;application/ld\\+json\u0026#34;\u0026gt;.*?\u0026lt;/script\u0026gt;\u0026#39; SPA인데 SSR 전환이 안 된 상태라면, curl 결과에 JSON-LD가 안 나올 수 있다. 이게 바로 SSR이 필수인 이유다.\n폼에 상품 정보를 입력하면 JSON-LD가 자동 생성되는 빌더도 만들어두었다. 구조를 직접 만져보면 감이 잡힌다.\nDemo - JSON-LD 빌더 실무에서 자주 부딪히는 문제들을 정리하면 이렇다:\n증상 원인 해결 JSON-LD가 크롤링 안됨 robots.txt 차단 GPTBot, Google-Extended Allow 설정 AI가 데이터를 인용 안함 Schema.org 타입 오류 Rich Results Test로 검증 API 응답 속도 느림 캐싱 미적용 Redis 캐싱 + 필드 최소화 SSR 전환 후 서버 부하 매 요청마다 DB 조회 ISR + Redis 캐싱 병행 ","permalink":"https://datanexus-kr.github.io/guides/geo-optimization/003-geo-data-pipeline/","summary":"상품 마스터 DB의 데이터가 어떤 파이프라인을 거쳐 HTML \u003chead\u003e의 JSON-LD가 되는지. 3단계 파이프라인 구조와 SSR 기반 자동 배포 아키텍처를 다룬다.","title":"3. On-Site GEO 기술 구조 - 상품 DB에서 JSON-LD까지"},{"content":"팀 내 문서가 노션, 깃허브 이슈, S3 버킷에 흩어져 있을 때, 소스마다 커넥터를 따로 만드는 작업이 병목이 된다. OpenDocuments는 그 연결 작업을 미리 구현해 놓은 오픈소스 RAG 플랫폼이다.\n커넥터가 12개 이상이다 노션, 깃허브, S3, PDF, Jupyter 노트북까지 연결된다. Ollama를 쓰면 외부 API 호출 없이 서버 안에서만 데이터가 돈다. 망 분리된 금융사나 유통사 환경에서 쓰기 좋은 구조다.\nSQLite와 LanceDB 기반이라 도커 하나로 설치가 끝난다. 무거운 인프라 없이 붙이니 사이드 프로젝트나 팀 내부 도구로 올리기 부담이 없다. 설정만 마치면 파일이 들어올 때 임베딩과 저장이 동시에 처리된다. 관리자가 분류 작업을 따로 할 필요가 없다.\n하이브리드 검색이 기본이다 벡터 검색과 키워드 방식을 섞은 하이브리드 검색에 리랭킹까지 붙어 있다. 한국어와 영어를 섞어서 질문해도 맥락을 잡아 출처를 명시해 준다. 답변 근거 문서를 표기해 주니까 할루시네이션 걱정이 줄어든다.\n로컬에서 쓰다가 외부 고성능 API로 전환하는 것도 설정 한 줄이다. MCP 서버를 지원하니 코딩 에이전트에 바로 붙일 수 있고, 파서나 커넥터를 직접 추가하는 플러그인 구조도 열려 있다.\n핵심만 뽑으면\nOllama 연동으로 외부 API 없이 온프레미스 RAG를 올리니 망 분리 환경에서도 쓸 수 있다 하이브리드 검색에 리랭킹이 붙어 있어 한국어-영어 교차 질의에서도 출처가 맞게 나온다 MCP 서버를 지원하니 코딩 에이전트와 사내 지식 베이스를 바로 연결한다 원문: https://news.hada.io/topic?id=27910\r","permalink":"https://datanexus-kr.github.io/curations/2026-04/2026-04-01-opendocuments-local-rag-platform/","summary":"노션, 깃허브, S3에 파편화된 사내 문서를 로컬 LLM으로 묶어 질의응답하는 오픈소스 플랫폼. 외부 API 없이 온프레미스로 돌린다.","title":"팀의 파편화된 지식을 하나로 묶는 로컬 RAG, OpenDocuments"},{"content":"에이전트를 도입해도 업무 구조 자체가 바뀌지 않으면 결국 기존에 하던 일을 에이전트가 대신할 뿐이다. 생산성 확장은 일어나지 않는다. 리드 호프만이 말하는 지휘자 전략은 그 착각을 깨는 데서 출발한다.\n연주자 말고 지휘자가 돼라 개별 업무를 직접 처리하는 방식은 한계가 있다. 여러 에이전트를 적재적소에 배치하고 흐름을 관리하는 쪽으로 역할이 바뀐다. 쉽지 않다. 에이전트가 맥락을 잃거나 엉뚱한 방향으로 튀는 걸 잡아주는 작업이 생각보다 번거롭다. 모로코 운전사가 챗봇으로 사업을 일군 사례가 나오는데, 도구의 성능보다 쓰겠다는 의지가 앞섰다는 점이 더 눈에 들어온다.\nSaaS 해자가 무너지고 있다 기존 소프트웨어 기업들이 쌓아온 기능 우위가 흔들린다. 도구를 만드는 비용이 낮아지면서 유통사나 식료품 기업도 자체 엔지니어를 고용하기 시작했다. 범용 기능보다 조직에 딱 맞는 맞춤형 해결책의 무게가 커졌다.\n프롬프트를 직접 쓰지 마라 AI에게 최적의 프롬프트를 짜게 시키는 게 기본이다. 음성 입력으로 텍스트보다 풍부한 맥락을 빠르게 전달할 수 있다. 에이전트에게 특정 전문가의 정체성을 부여해 내 논리를 비판하게 만드는 방식이 중급 활용법이다. 모델의 학습 데이터가 과거에 멈춰 있다는 점을 인지하고 실시간 검색을 섞어 쓰는 것도 기본 중 기본이다.\n결과물보다 과정을 공개하라 실무 적용 능력을 증명하려면 결과물보다 작업 흐름을 공유하는 편이 낫다. 대형 플랫폼이 닿지 않는 개인화된 경험에 집중할 때 차별화가 생긴다. 누구나 뽑을 수 있는 범용 콘텐츠는 기계에 맡기고, 고유한 맥락이 담긴 작업에 집중한다.\n핵심만 뽑으면\n프롬프트를 직접 쓰지 말고 AI에게 짜게 시키면 결과 품질이 다르다 SaaS의 기능 축적 우위가 AI 기반 저비용 맞춤 구축으로 대체되고 있어서 범용 도구보다 조직 특화 해결책이 유리해진다 개별 실행자 역할에서 여러 에이전트를 관리하는 지휘자 역할로 전환하는 게 지금 시점의 실질적인 생산성 확장 경로다 원문: https://eopla.net/magazines/40952\r","permalink":"https://datanexus-kr.github.io/curations/2026-04/2026-04-01-ai-era-five-percent-conductor-strategy/","summary":"에이전트를 붙여도 업무 구조 자체가 바뀌지 않으면 생산성 확장은 없다. 리드 호프만이 말하는 지휘자 전략은 그 착각을 깨는 데서 시작한다.","title":"AI 시대를 장악하는 에이전트 지휘자의 사고법"},{"content":"법률 데이터를 AI 에이전트에 넣으려면 공공기관 XML부터 파싱해야 한다. 표 구조가 깨지고, 조문 번호 체계가 꼬이고, 전처리에만 반나절이 빠진다. 법망은 그 과정을 미리 해결해놓은 서비스다.\nJSON으로 받으면 뭐가 다른가 공공기관이 주는 법령 데이터는 기계가 바로 쓰기엔 까다롭다. 법망은 복잡한 표 구조까지 일관된 JSON 규격으로 파싱해서 준다. 국내 법령 대부분을 매주 갱신한다.\n토큰 소모가 줄어든다. XML이나 HTML에서 텍스트를 뽑아내는 전처리가 필요 없으니 파이프라인이 단순해진다. 표 안의 수치도 배열로 깔끔하게 나와서, 모델이 맥락을 잘못 읽을 가능성이 낮아진다.\n벡터 검색이 내장돼 있다 단어 매칭으로는 조문의 맥락을 잡기 어렵다. 법망은 주요 조문을 pgvector 기반 벡터 데이터로 변환해뒀다. API 하나 호출하면 의미 기반 검색이 된다. 인프라를 따로 세울 필요가 없다는 게 핵심이다.\n모델이 조문을 직접 참조해서 답변을 생성하는 RAG 구조를 바로 태울 수 있다.\n연동이 간단하다 인증 절차가 없다. API 키 발급 같은 과정 없이 바로 호출된다. Rate Limit도 넉넉한 편이고, 사용자 로그를 남기지 않아 프로토타이핑 단계에서 부담이 없다.\n개정 이력 비교도 API로 된다. 전문가가 아니어도 조문이 어떻게 바뀌었는지 추적할 수 있다.\n핵심만 뽑으면\n법령 데이터를 JSON으로 정제해서 주니까 전처리 공수가 거의 없다 pgvector 기반 시맨틱 검색이 내장돼 있어서 RAG 파이프라인에 바로 붙인다 인증 없이 호출 가능하고 Rate Limit도 넉넉해서 빠르게 시작할 수 있다 관련 글\n왜 DataNexus를 만드는가\r— 도메인 지식의 구조화가 AI 정확도를 결정한다 법률 데이터, 파이프를 넘어 뇌를 얻다\r— korean-law-mcp와 DataNexus 온톨로지의 시너지 원문: https://news.hada.io/topic?id=28050\r","permalink":"https://datanexus-kr.github.io/curations/2026-04/2026-04-01-beopmang-api-korean-law-json-for-ai-agent/","summary":"법령 XML을 직접 파싱하면 표 구조가 깨지고 전처리에 반나절이 빠진다. 법망은 JSON으로 정제된 데이터를 바로 주는 API로, 그 과정을 미리 해결해놓은 서비스다.","title":"AI 에이전트 개발을 위한 한국 법령 데이터 활용법"},{"content":"대용량 엑셀 다운로드에서 메모리를 늘려도 OOM이 반복되고, Apache POI 스트리밍 코드를 매번 직접 작성하는 비용이 크다. StreamSheet는 그 반복 작업을 어노테이션 하나로 걷어낸 Spring Boot 라이브러리다.\nOOM 없이 100만 행이 나간다 데이터를 한꺼번에 메모리에 올리지 않는다. JPA Stream이나 JDBC ResultSet으로 순차적으로 읽으면서 조회와 파일 작성을 동시에 진행한다. 가비지 컬렉터 부하가 일정하게 유지되고, 부하 테스트에서도 프로세스 점유율이 안정적이었다.\n다양한 데이터베이스 환경을 지원하니 기존 프로젝트에 붙이는 데 제약이 없다.\n어노테이션 하나로 끝난다 객체에 어노테이션을 붙이면 설정이 완료된다. Spring Boot Auto-configuration이 붙어 있어서 의존성 추가만으로 바로 동작한다. 반환 타입만 지정하면 내부 변환기가 스트림을 열고 데이터를 전송한다.\n열 순서나 이름을 바꿀 때 설정값 한 줄만 수정하면 된다. 번호를 일일이 대조할 필요가 없고, Apache POI 보일러플레이트를 직접 유지보수하던 공수가 사라진다.\n핵심만 뽑으면\nJPA Stream과 JDBC ResultSet으로 순차 처리하니 100만 행 엑셀 다운로드에서 OOM이 나지 않는다 어노테이션 기반 선언적 방식이라 Apache POI 보일러플레이트를 직접 짤 필요가 없다 Spring Boot Auto-configuration을 지원하니 의존성 추가만으로 기존 프로젝트에 바로 붙는다 관련 글\nBronze 레이어 — 원본을 있는 그대로 쌓는다\r— 대용량 데이터 적재 시 고려할 수집 전략 원문: https://news.hada.io/topic?id=27997\r","permalink":"https://datanexus-kr.github.io/curations/2026-04/2026-04-01-streamsheet-excel-export-spring-boot/","summary":"100만 건 엑셀 다운로드에서 OOM 없이 스트리밍 처리하는 Spring Boot 라이브러리. 어노테이션 하나로 보일러플레이트를 걷어낸다.","title":"대용량 엑셀 다운로드를 위한 Spring Boot 최적화 기법"},{"content":"노션, 깃허브 이슈, 위키 페이지가 제각각인 팀 환경에서 RAG를 구성하려면 소스마다 커넥터를 직접 만들어야 한다. OpenDocuments는 그 연결 작업을 미리 구현해 놓은 셀프호스팅 RAG 도구다.\n설치는 한 줄이다 npm install 하나로 시작한다. API 키 자동 로딩이 붙어 있어서 수동 설정을 최소화하고, opendocuments doctor 명령어로 설정 오류를 바로 잡아낼 수 있다.\nTypeScript와 Hono 기반이라 가볍다. SQLite와 LanceDB를 쓰니 별도 DB 서버 없이도 돌아간다. Ollama를 연결하면 외부 API 호출 없이 로컬에서만 처리된다. 금융사처럼 망 분리가 엄격한 환경에서 쓸 수 있는 구조다.\n한국어-영어 교차 질의가 된다 PDF, Jupyter 노트북 등 다양한 포맷을 파싱하고 의미 단위로 쪼개서 벡터로 저장한다. 검색은 벡터 조회와 키워드 방식을 섞은 하이브리드 방식이다. 한국어로 물어봐도 영어 문서에서 답을 찾아오고, 답변에 출처 문서를 명시하니 할루시네이션을 걸러낼 수 있다.\nMCP 서버를 지원하니 코딩 에이전트에서 사내 지식 베이스를 바로 질의할 수 있다. 모노레포 구조라 파서나 커넥터를 플러그인으로 직접 추가하는 것도 어렵지 않다. 수백 개 테스트 케이스가 깔려 있어서 커스텀 로직을 얹을 때 안정성 걱정이 덜하다.\n핵심만 뽑으면\nOllama 연동으로 외부 API 없이 로컬에서만 데이터를 처리하니 망 분리 환경에 적합하다 하이브리드 검색에 다국어 지원이 붙어 있어 한국어 질의로 영어 문서를 찾아온다 MCP 서버를 지원하니 코딩 에이전트와 사내 지식 베이스를 바로 연결할 수 있다 관련 글\n왜 DataNexus를 만드는가\r— 흩어진 도메인 지식을 구조화하는 문제의식 4개의 오픈소스를 이 조합으로 결정하기까지\r— RAG 엔진 선정 시 고려한 평가 기준 참고 소스\nhttps://github.com/joungminsung/OpenDocuments\rhttps://news.hada.io/topic?id=27910\r","permalink":"https://datanexus-kr.github.io/curations/2026-04/2026-04-01-opendocuments-self-hosted-rag-platform/","summary":"노션, 깃허브, S3 등에 흩어진 팀 문서를 Ollama로 로컬 구동하는 RAG 플랫폼. 보안 환경에서도 외부 API 없이 자연어 검색이 된다.","title":"흩어진 팀 지식을 하나로 연결하는 오픈소스 RAG 플랫폼 OpenDocuments"},{"content":"클로드에게 \u0026ldquo;이 부분 고쳐줘\u0026quot;만 반복하면 이미 아는 문제 안에서만 맴돈다. 수정 지시가 쌓일수록 본인이 인식하지 못한 사각지대는 그대로 남는다. 지시 대신 평가를 요청하는 방식으로 바꾸면 보이지 않던 영역이 드러난다.\n어떤 직무를 맡기느냐가 결과를 가른다 클로드에게 시니어 UX 디자이너 역할을 주고 사용자 흐름을 평가하게 하면 버튼 하나 고치는 수준이 아니라 동선 전체를 다시 짚어준다. PM 관점을 줬을 때는 고객 피드백과 실제 지표를 대조하면서 우선순위를 정리해낸다.\n시니어 엔지니어로 코드베이스를 리뷰시키면 성능 병목과 중복 코드를 동시에 잡는다. 역할이 구체적일수록 피드백의 밀도가 달라진다.\n모든 단계를 자동화하면 비용이 튄다 에이전트를 파이프라인으로 엮어두면 그럴싸해 보인다. 전체 코드베이스를 역할별로 여러 번 읽히면 토큰 비용이 급증하고 응답 지연도 생긴다.\n꼭 필요한 시점에 딱 하나의 페르소나만 부르는 게 낫다. 배포 전에 안정성이 불안하면 QA 리드만 꺼내서 에러 처리 누락을 심각도 순으로 뽑아달라고 한다. 전체를 돌리지 않아도 충분하다.\nQA 관점으로 한 번 더 들여다보기 기능이 돌아간다는 확인만으로는 부족하다. QA 역할을 주고 제품을 보게 하면 UI 깨짐이나 예외 처리 미비가 꽤 나온다. 혼자 보면 그냥 넘어갔을 지점들이다.\n역할을 바꿔가며 같은 코드를 들여다보는 과정이 바이브코딩의 기술 부채를 줄이는 실질적인 방법이다.\n전문가 역할을 부여한 평가 요청이 단순 수정 지시보다 잠재 결함을 더 많이 잡아냄 전체 자동화 파이프라인보다 필요한 시점에 특정 페르소나 하나만 호출하는 편이 토큰 비용을 아낌 QA 리드 페르소나로 엣지 케이스를 검증하면 기능 구현 이후 쌓이는 기술 부채를 줄일 수 있음 ","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-31-claude-vibe-coding-role-based-evaluation/","summary":"고쳐달라는 말만 반복하다 보면 이미 아는 문제 안에서만 맴돈다. 전문가 페르소나를 투입하면 보이지 않던 사각지대가 드러나고, 어떤 시점에 누구를 부를지 감각이 생기면 토큰도 아낀다.","title":"코딩 지시를 멈추고 전문가의 시선을 빌리는 법"},{"content":"NotebookLM으로 PPT를 만들다 보면 20장 근처에서 생성이 끊기는 경우가 있다. 분량이 절반도 안 됐는데 멈추면 처음부터 쪼개가며 다시 작업해야 한다. 이 영상은 그 문제를 구조적으로 우회하는 방법을 다룬다.\n디자인 프롬프트부터 만들어라 Behance나 Dribbble에서 레이아웃 하나를 골라 GoFullPage로 캡처한다. 그 이미지를 Gemini나 ChatGPT에 올리면 색상 체계와 구조를 텍스트로 뽑아준다. 이때 제목형, 본문형, 데이터 시각화형, 프로세스형 네 가지로 나눠서 추출하는 게 핵심이다. 타입을 쪼개두면 나중에 슬라이드별로 골라 쓸 수 있다.\n마스터 대본을 소스로 지정하라 자료 여러 개를 한꺼번에 소스로 넣으면 AI가 맥락을 잃는다. 먼저 40장 분량의 슬라이드 번호, 제목, 핵심 데이터를 담은 대본을 채팅창에서 뽑는다. 그걸 메모에 저장해 NotebookLM 소스로 변환한다. 기존 소스는 모두 체크 해제하고 이 대본 하나만 활성화하면 AI가 다른 데로 새지 않는다.\n자연어 말고 코드형 명령어를 써라 40장을 한 번에 요청하면 중간에 끊긴다. 120장, 2140장으로 나눠서 명령한다. 이때 자연어가 아니라 시스템 관리자 방식의 구조화된 명령어를 쓴다. 20페이지에서 끝내지 말고 21페이지로 바로 이어지게 하라는 연결 규칙도 같이 넣는다. 생성된 두 PPTX 파일을 원본 서식 유지 옵션으로 합치면 끝난다.\n핵심만 뽑으면\n디자인 레퍼런스를 4가지 타입(제목, 본문, 시각화, 프로세스)으로 세분화해 프롬프트로 만들면 슬라이드별 스타일을 골라 쓸 수 있다 마스터 대본 하나만 소스로 활성화하면 AI가 다른 자료로 맥락을 잃지 않는다 자연어 대신 코드형 명령어로 구간 분할 생성하면 40장도 일관되게 뽑힌다 소스\nhttps://www.youtube.com/watch?v=rlVWuvgEftU\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-31-notebooklm-slide-limit-bypass-strategy/","summary":"NotebookLM이 20장에서 멈추면 대부분 그냥 포기한다. 마스터 대본 하나를 소스로 지정하고 코드형 명령어로 밀어붙이면 40장도 뽑힌다.","title":"NotebookLM 생성 한계를 극복하는 대본 기반 통제법"},{"content":"강의 기획을 AI로 처리하려다 보면 Claude 하나로 전부 해결하려는 시도가 자주 막힌다. 실시간 정보 검색이 필요한 순간, 여러 자료를 통합해서 깊이 있게 분석해야 하는 순간마다 병목이 생긴다. 도구를 교체하는 게 아니라 단계별로 역할을 나눠야 흐름이 유지된다.\n설계는 Claude, 조사는 Perplexity가 맡는다 Claude는 학습 목표를 분석해서 시간 단위로 커리큘럼을 구조화하는 데 쓴다. 뼈대를 먼저 잡아야 나머지 단계가 막히지 않는다. 실시간 정보가 필요할 때는 Perplexity로 넘긴다. 노트북LM은 이렇게 모인 자료를 통합해서 깊이 있는 분석을 뽑아낸다.\n각 도구가 잘하는 일이 다르다. 한 도구에 몰아넣으면 어딘가 억지가 생긴다.\n슬라이드 문구와 시각화도 역할을 나눈다 정리된 정보를 슬라이드 문구로 바꾸는 건 다시 Claude다. 행동 중심 문장으로 다듬고 나면 시각화 단계로 넘긴다. 노트북LM이 구성을 잡으면 전용 도구가 개념 다이어그램과 도식을 만든다. 마지막으로 기획 의도와 결과물 사이에 간극이 없는지 확인한다.\n도구 수보다 연결 순서가 중요하다 도구를 많이 쓰는 게 목적이 되면 안 된다. 각 기능이 특정 단계에서 독립적으로 움직이면서도 다음 단계로 이어지는 구조가 있어야 한다. 공정이 순서대로 맞물리면 중간에 사람이 개입할 일이 줄어든다.\n핵심만 뽑으면\nClaude로 뼈대를 잡고 Perplexity로 실시간 자료를 채우니까 기획 초안 속도가 빨라진다 노트북LM에 자료를 모아두면 산발적인 정보가 하나의 관점으로 정리된다 단계별로 역할을 나눠야 각 도구가 제대로 쓰인다 ","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-31-ai-lecture-planning-workflow-agent-structure/","summary":"Claude, Perplexity, 노트북LM을 강의 기획 단계별로 역할 분리해서 쓰면 어떻게 달라지는지 정리한 내용이다. 도구를 많이 쓰는 게 아니라 어떤 단계에 어떤 도구를 붙이느냐가 포인트다.","title":"기획부터 시각화까지 유기적으로 연결하는 AI 협업 흐름"},{"content":"RAG 파이프라인에 웹 데이터를 넣을 때 URL만 넘기면 Markdown으로 변환해주는 도구가 있으면 편리하다. defuddle이 그 역할을 한다. 문제는 결과가 사이트 구조에 따라 크게 달라진다는 점이다.\n시맨틱 HTML이 있느냐 없느냐 테크 블로그나 공식 문서는 괜찮다. 제목 계층이 살아있고 본문 태그가 명확하면 추출 결과도 쓸 만하다. 문제는 커머스 사이트나 레이아웃 중심으로 설계된 페이지다. 시맨틱 구조가 없으니 본문과 광고가 섞여서 나온다.\n이 상태로 RAG 인덱싱에 넣으면 검색 품질이 오염된다. 자바스크립트로 화면을 렌더링하는 동적 환경에서는 내용 자체가 빠지는 경우도 많다. 정적 위키 페이지 수준의 소스라면 충분히 쓸 수 있고, 상용 서비스의 복잡한 구조에서는 한계가 뚜렷하다.\n자동화 도구가 전처리를 대체하지는 않는다 파싱 로직을 직접 짜는 수고를 덜어줄 거라 기대했지만, 추출 결과를 검수하는 데 오히려 시간이 더 들었다. 메타데이터와 본문의 경계가 흐릿해지는 현상도 반복됐다.\n소스 도메인별로 전처리 스크립트를 따로 두는 방식이 현실적이다. 모델이 아무리 좋아도 원천 데이터 품질이 낮으면 인덱스 자체가 오염되고, 그 오염은 검색 결과 전체로 번진다.\n핵심만 뽑으면\ndefuddle 추출 성능은 대상 사이트의 시맨틱 HTML 준수 여부에 따라 크게 달라짐 SEO 최적화가 부족한 사이트에서는 본문과 노이즈(광고, 메뉴) 구분이 어려움 RAG 구축 시 자동 추출 결과를 검증하는 전처리 단계를 별도로 설계해야 함 관련 글\nSilver 레이어 — Bronze를 분석 가능한 상태로 올린다\r— 데이터 정제 원칙과 품질 게이트 GEO란 무엇인가 — SEO 너머의 AI 인용 전략\r— 시맨틱 HTML과 구조화된 데이터의 중요성 데이터 전처리가 가르는 RAG 품질과 마크다운 변환 도구 활용법\r— MarkItDown을 활용한 문서 전처리 소스\nhttps://share.google/8V29VWarTG9YMxXI7\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-31-defuddle-web-markdown-extraction-seo-limitations/","summary":"RAG 파이프라인에 쓸 웹 데이터를 defuddle로 뽑아봤더니 사이트 구조에 따라 결과가 크게 달랐다. 시맨틱 HTML이 무너진 사이트에서는 본문과 광고가 섞이고, 동적 렌더링 환경에서는 내용 자체가 날아간다.","title":"defuddle로 웹페이지를 변환하며 마주한 의외의 벽"},{"content":"에이전트에게 Planning을 통째로 맡기면 프로덕션에서 루프를 돌다 멈추는 일이 생긴다. OpenAI가 공개한 가이드는 그 문제를 정확히 짚는다. 에이전트는 지능체가 아니라 도구를 쓰는 자동화 프로그램이라는 관점에서 설계를 시작하라고 한다.\n자율성보다 예측 가능성이 먼저다 흐름 전체를 모델에 맡기지 말라. 상태 머신처럼 구조를 짜고, 각 단계에서 시스템이 판단을 내리도록 명시적으로 유도해야 한다. 제어가 없는 시스템은 운영 환경에서 사고를 낸다.\n도구 호출도 마찬가지다. API 명세만 넘기면 부족하다. 입력 데이터 형식을 엄격히 제한하고, 오류가 나면 원인을 모델에 피드백으로 돌려준다. 에러를 그냥 띄우지 않고 재시도 루프로 연결하면 성공률이 올라간다. 환각 문제는 지시문에 예시 몇 개를 추가하는 것만으로도 꽤 잡힌다.\nRAG 붙이기 전에 메모리 구조부터 설계하라 RAG를 붙인다고 성능이 바로 좋아지지 않는다. 대화가 길어질수록 자원 소모는 커지고, 유효한 데이터만 걸러내는 필터링 없이는 응답 속도만 늦춰진다. 고정 정보(사용자 프로필 등)와 가변 정보(현재 대화)를 분리해 관리하면 토큰 비용이 줄어든다.\n평가 체계를 나중으로 미루면 안 된다. 프롬프트 수정 전에 성공과 실패를 가르는 정량 지표부터 세운다. 수백 개의 테스트 케이스를 반복 통과하면서 수치가 쌓여야 병목 지점이 보인다.\n모델은 부품이다 정밀한 계산이나 엄격한 규격이 필요한 작업은 외부 코드나 전문 라이브러리에 맡긴다. 모든 규칙을 하나의 긴 지시문에 몰아넣으면 유지보수가 불가능해진다. 기능을 쪼개고, 각 모듈이 명확한 역할만 수행하도록 설계하는 것이 전부다.\n핵심만 뽑으면\n자율적인 Planning보다는 개발자가 정의한 명시적 워크플로우 제어가 프로덕션 환경에서 더 안정적이다 Tool 호출 시 발생하는 에러 피드백을 모델에게 재전달하는 루프 구조가 에이전트의 성공률을 높인다 정량적 Evaluation 지표 구축은 프롬프트 엔지니어링보다 선행되어야 할 핵심 작업이다 관련 글\n왜 DataNexus를 만드는가\r— 시맨틱 갭 문제와 온톨로지 기반 접근 4개의 오픈소스를 이 조합으로 결정하기까지\r— 에이전트 시스템의 기술 스택 선정 과정 소스\nhttps://share.google/OobJU2T2JLz7gxlim\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-31-openai-agent-building-practical-guide/","summary":"에이전트에게 Planning을 통째로 맡기면 프로덕션에서 루프를 돌다 멈추는 문제가 생긴다. OpenAI 가이드는 그 문제를 명시적 워크플로우 제어로 푼다.","title":"OpenAI 가이드로 살펴본 에이전트 설계의 실무적 해법"},{"content":"AI 에이전트에게 업무를 맡기면 초기엔 프롬프트를 아무리 다듬어도 결과물이 들쭉날쭉하다. 문제는 모델 성능이 아니라 에이전트가 일하는 환경 자체인 경우가 많다. 폴더 구조와 SOP가 정비되지 않으면 에이전트는 매번 맥락을 새로 파악해야 하고, 산출물 양식도 매번 달라진다.\n폴더 구조를 조직도랑 맞추면 뭐가 달라지나 에이전트를 투입하고 일주일 뒤, 분석 역량은 나쁘지 않았지만 결과물이 아무 폴더에나 저장됐고 지시할 때마다 산출물 양식이 달랐다. 폴더 구조를 실제 조직도와 동일하게 구성했다. 지휘부, 비서진, 각 팀으로 큰 줄기를 잡고 그 아래에 매뉴얼, 도구, 데이터, 결과물 폴더를 고정했다. 뼈대가 잡히자 비서실장이 세일즈 팀에 업무를 위임할 때마다 구구절절 설명하는 수고가 사라졌다. 각 팀 폴더에 이미 어떻게 일해야 하는지 정의된 가이드라인이 있기 때문이다.\n세일즈 현황 검토를 맡겼더니 겉만 번지르르한 리포트가 나왔다. 현장 용어와 판단 기준이 없었다. 팀 대화 기록과 내부 문서를 읽히고 조직만의 표준 생성 방식을 직접 설계하게 했다. 데이터 학습이 아니라 일하는 법 자체를 명문화하는 작업이었다.\nSOP를 어떻게 쓰느냐에 따라 결과물이 갈린다 세일즈 에이전트가 초안을 가져오면 피드백을 주고받았다. 지표 계산 로직을 수정하고, 숫자 나열 대신 안건 중심의 메시지로 다듬었다. 어떤 데이터 테이블을 참조하고 어떤 관점으로 해석할지 명확해지자 퀄리티가 일정하게 유지됐다. 이제 명령어 한 줄이면 변화율 체크와 특이점 추출까지 사람 없이 돌아간다.\n사람은 매뉴얼이 허술해도 눈치껏 모면한다. 기계는 적힌 대로만 움직인다. 매일 아침 비서실장이 보내주는 통합 브리핑으로 하루가 시작된다.\n핵심만 뽑으면\n폴더 구조를 조직도 기반으로 맞추니까 매번 설명 없이도 에이전트가 맥락을 찾아간다 프롬프트 다듬는 것보다 도메인 지식과 일하는 방식을 SOP로 써두는 게 퀄리티 차이를 만든다 참조 테이블과 해석 관점이 명시되면 결과물이 일정해진다 관련 글\n4개의 오픈소스를 이 조합으로 결정하기까지\r— 시스템 아키텍처 결정의 체계적 접근 OpenAI 가이드로 살펴본 에이전트 설계의 실무적 해법\r— 에이전트의 모듈화와 워크플로우 제어 원문: https://www.linkedin.com/posts/leekh929_ai-%EB%B9%84%EC%84%9C%EC%8B%A4%EC%9E%A5%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%97%88%EB%8B%A4%EB%8A%94-%EA%B8%80%EC%9D%84-%EC%93%B4-%EC%A7%80-%EC%9D%BC%EC%A3%BC%EC%9D%BC%EC%9D%B4-%EB%90%90%EC%8A%B5%EB%8B%88%EB%8B%A4-%EB%B9%84%EC%84%9C%EC%8B%A4%EC%9E%A5%EC%9D%84-%EB%A7%8C%EB%93%A4%EA%B3%A0-activity-7438587163157590016-je7Q?utm_source=share\u0026amp;utm_medium=member_android\u0026amp;rcm=ACoAAAGPfasB-djMifTErXTP5V7RQzL6YbO5POo\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-31-ai-agent-organization-sop-folder-structure/","summary":"AI 에이전트를 실무에 투입하면 처음엔 결과물 위치도 제각각이고 산출물 양식도 매번 달라진다. 폴더 구조를 조직도 기반으로 정비하고 SOP를 명문화하면 퀄리티가 일정해지는 이유를 정리했다.","title":"AI 조직의 성패를 가르는 디렉토리 설계와 업무 매뉴얼의 힘"},{"content":"Claude Code를 처음 세팅할 때 플러그인 목록 앞에서 멈추는 경우가 잦다. 수십 개의 플러그인 중 어떤 것을 켜야 하는지 기준이 없기 때문이다. Plugin Advisor는 그 판단 비용을 줄여주는 도구다.\nPreset Pack부터 시작하면 뭐가 달라지나 모든 기능을 켜고 시작하는 대신 프로젝트 성격에 맞는 Preset Pack을 고른다. 프리셋을 적용하면 로컬 환경에서 빠진 패키지를 바로 잡아낸다.\n복사 전에 실행되는 사전 체크리스트가 핵심이다. API 키나 환경 변수가 빠진 채 코드를 실행하다 런타임 에러를 맞는 상황을 미리 막는다. DDL 정의가 꼬이거나 엔드포인트 주소 하나만 어긋나도 파이프라인이 전체 멈추는 환경에서는 시작 단계 검증이 디버깅 시간을 크게 줄인다. 만료된 토큰을 방치한 채 배포 스크립트를 실행하는 실수도 이 단계에서 잡힌다.\nSetting Plan이 커넥터 관리에 어떻게 쓰이나 Plugin Advisor의 Setting Plan은 단계별 맞춤 가이드를 뽑아준다. 장애 지점을 기록하고 다음 설계 단계의 입력으로 넘기는 흐름이 붙어 있다. 커넥터가 늘어날수록 이 구조가 실제 작업 속도에 영향을 미친다.\nClaude Code를 에이전트로 운영하려면 설정 기반이 먼저 갖춰져야 한다. 수많은 커넥터를 관리하는 데이터 엔지니어링 환경에서 이 도구는 체크리스트 역할을 한다. 필요한 것만 남긴 설정 위에서 로직 구현에 집중할 수 있다.\n핵심만 뽑으면\nPreset Pack으로 시작하니까 플러그인 하나하나 켜고 끄는 판단 비용이 사라진다 사전 체크리스트가 환경 변수 누락이나 만료된 토큰 같은 실수를 실행 전에 잡아준다 Setting Plan이 장애 지점을 기록해서 다음 설정 단계 입력으로 자동 연결한다 소스\nhttps://plugin-advisor.vercel.app/\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-31-claude-code-plugin-advisor-efficiency/","summary":"Claude Code를 처음 세팅할 때 플러그인 목록 앞에서 막혔다면 Plugin Advisor가 그 입구를 좁혀준다. Preset Pack과 사전 체크리스트로 런타임 에러를 미리 잡는 흐름을 정리했다.","title":"Claude Code 설정의 막막함을 걷어내는 Plugin Advisor 활용법"},{"content":"보고서 한 편을 슬라이드로 만드는 데 반나절이 걸리는 건 내용 정리가 아니라 서식 맞추는 작업 때문이다. 단일 AI 모델로 구조화와 서식 제어를 동시에 잡으려 하면 어느 쪽에서든 품질이 떨어진다. NotebookLM, Claude, Gemini를 단계별로 나눠서 파이프라인으로 연결하면 이 문제가 해결된다.\n구조화는 NotebookLM, 서식은 Claude가 따로 맡는다 NotebookLM에 보고서 전문을 올리면 이슈, 시나리오, 파급 효과를 분류하고 각 장의 헤드라인을 뽑아낸다. 초안 생성을 요청할 때 제약 조건을 미리 박아두는 것이 이 단계의 포인트다.\n\u0026ldquo;7페이지를 영어로 작성해 줘. 헤드라인과 거버닝 자막을 동일하게 해 주고, 문장 끝에 마침표를 찍지 마. 흰색 배경에 블루를 액센트 컬러로, 픽토그램과 인포그래픽으로 세련되게.\u0026rdquo;\n마침표 제거, 배경색 고정 같은 세밀한 조건을 처음부터 넣으면 나중에 수작업으로 고칠 게 거의 없다. 차이가 생각보다 크다.\n사내 서식 적용은 Claude 차례다. 기존 보고서 PDF나 이미지를 넘기면 폰트, 섹션 탭 배치, 페이지 번호 위치를 스타일 시트로 뽑아준다.\n\u0026ldquo;지금 내가 만든 이 페이지가 완성본이야. 스타일 시트로 추출해서 다음번에 쓸 수 있도록 정리해 줘. 프롬프트 템플릿도 같이 만들어 줘.\u0026rdquo;\n이 스타일 시트를 Claude PPT 애드인에 붙여 넣으면 레이아웃이 자동 배치된다. 텍스트 박스 위치를 일일이 옮기던 반복 작업에서 벗어난다.\n이미지 잘라내기와 표지는 어떻게 마무리하나 NotebookLM이 생성한 이미지에서 필요한 부분만 추출할 때는 PowerPoint의 도형 병합 기능을 쓴다. [삽입] \u0026gt; [자유형 도형]으로 영역을 그린 뒤, 이미지와 도형을 Shift 클릭으로 함께 선택하고 [도형 서식] \u0026gt; [도형 병합] \u0026gt; [교차]를 적용하면 깔끔한 개체만 남는다. 별도 디자인 툴이 없어도 된다.\n표지는 Gemini가 마무리한다. 기존 시안 이미지를 참고 자료로 넘기고 폴리곤 스타일로 변환을 요청하면 전체 분위기를 살린 결과가 나온다.\n핵심만 뽑으면\nNotebookLM이 구조화와 초안을 맡고, Claude가 서식을 추출해서 PPT 애드인에 주입하니까 레이아웃 수작업이 거의 사라진다 프롬프트에 마침표 제거, 컬러 지정 같은 세밀한 조건을 처음부터 넣으면 사후 편집 시간이 크게 준다 스타일 시트를 한 번 추출해두면 다음 보고서부터는 같은 서식을 재사용할 수 있다 소스\nhttps://www.youtube.com/watch?v=q8C8IrPYulQ\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-31-ai-pipeline-business-slide-automation/","summary":"NotebookLM, Claude, Gemini를 파이프라인으로 연결해서 보고서를 슬라이드로 만드는 과정을 정리했다. 단일 모델로는 구조화와 서식 제어를 동시에 잡기 어렵고, 역할을 나눠야 각 단계에서 제대로 된 결과가 나온다.","title":"복합 AI 워크플로우로 완성하는 기업용 보고서 자동화"},{"content":"LangChain이 v1으로 넘어오면서 기존 체인 방식의 예제 코드 상당수가 구식이 됐다. 복잡한 흐름을 체인으로 제어하면 코드가 금세 엉키는 문제도 있다. baem1n이 올린 이 저장소는 수십 개의 한국어 주피터 노트북으로 그 간극을 채워준다.\nLangGraph에서 State와 Node를 어떻게 짜는가 LangGraph는 State와 Node를 중심으로 흐름을 정의한다. 단계별 데이터가 어떻게 흘러가는지 명시하고, 조건에 따라 경로를 나눈다. 말로만 들으면 추상적인데, 노트북에서 파이썬 코드로 실행해보면 구조가 빠르게 잡힌다.\nuv 패키지 매니저로 환경을 세팅하고 노드 하나씩 실행하다 보면 데이터가 노드 사이를 오가는 원리가 보인다. 질문을 다시 쓰거나 우선순위를 조정하는 고도화된 RAG 흐름도 단계별로 담겨 있어 실무 프로젝트에 바로 옮겨올 수 있다.\n프롬프트 패턴을 모듈로 관리하기 데이터 분석 예제에서는 LLM이 구문을 생성하고 격리된 환경에서 실행한 뒤 시각화까지 이어지는 흐름을 보여준다. Deep Agents 하네스로 반복되는 프롬프트 패턴을 모듈로 분리하면 팀 단위 개발에서 컨벤션을 유지하기 편해진다.\n미리 기록된 실행 결과도 포함돼 있어 대형 모델을 직접 돌리지 않아도 봇이 도구를 고르고 동작하는 기록을 살펴볼 수 있다. 내부 원리를 파악하는 데 시간을 아낄 수 있다.\n핵심만 뽑으면\nLangGraph State/Node 설계를 파이썬 코드로 단계별 실행하니 비결정적 흐름 제어 개념이 빠르게 잡힘 uv로 의존성을 관리하면 환경 세팅 시간이 줄어서 실습에 바로 진입 가능 Deep Agents 하네스로 프롬프트 패턴을 모듈화하면 팀 개발 컨벤션을 일관되게 유지할 수 있음 관련 글\n4개의 오픈소스를 이 조합으로 결정하기까지\r— 에이전트 오케스트레이션 기술 스택 선정 OpenAI 가이드로 살펴본 에이전트 설계의 실무적 해법\r— 에이전트 설계의 예측 가능성과 제어 원문: https://www.linkedin.com/posts/baem1n_github-baem1nlangchain-langgraph-deepagents-notebooks-activity-7435985777458561024-TULI?utm_source=share\u0026amp;utm_medium=member_android\u0026amp;rcm=ACoAAAGPfasB-djMifTErXTP5V7RQzL6YbO5POo\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-31-langchain-v1-langgraph-deepagents-guide/","summary":"LangChain v1으로 넘어오면서 기존 체인 코드가 구식이 됐다. State와 Node 설계로 흐름을 제어하는 LangGraph를 한국어 주피터 노트북으로 단계별로 익힐 수 있는 저장소를 정리한다.","title":"업데이트된 LangChain 환경에 맞춘 실습용 노트북 활용법"},{"content":"데이터 엔지니어링 업무를 하다 보면 시장 지표 수집이 어느 순간 하루 루틴 중 제일 귀찮은 일이 된다. Macro-Pulse는 그 반복을 없애버린 오픈소스 프로젝트다. 단순히 수치를 긁어오는 게 아니라 운영까지 버티는 구조로 짜여 있다.\n서버 없이 어떻게 매일 돌아가나 구조 자체는 단순하다. 파이썬으로 국내외 증시, 금리, 원자재 데이터를 수집하고 히트맵 스크린샷을 함께 생성한다. 결과물은 텔레그램 메시지 하나로 전달된다. GitHub Actions의 Cron 스케줄러로 정해진 시간에 자동 실행하고, GitHub Pages에 대시보드를 올린다. 별도 서버가 없다.\n패키지 관리 도구로 uv를 쓴다. pip 대비 설치 속도가 눈에 띄게 빠르고 의존성 충돌도 덜 난다. 실행 이력을 저장해서 장애가 생기면 어디서 막혔는지 바로 추적할 수 있다.\nJSON으로 받으면 뭐가 다른가 설정 파일 하나로 리포트 형식을 바꿀 수 있다. 출력 항목 추가나 섹션 순서 변경에 코드를 건드릴 필요가 없다. 시장 상황에 따라 특정 정보를 선택적으로 넣고 빼는 작업도 여기서 끝난다. API 키 같은 민감한 정보는 GitHub Secrets로 분리했다.\n테스트 구조도 실용적이다. 로직 검증과 외부 통신을 분리해서 환경 변수에 따라 테스트 범위를 조절한다. 컨테이너 내부에 브라우저를 미리 구성해두면 로컬과 서버 환경 차이로 스크린샷이 깨지는 문제를 막는다. dry-run 옵션을 켜면 메시지를 실제로 보내지 않고 기능만 점검할 수 있어서 개발 중에 유용하다.\n핵심만 뽑으면\nGitHub Actions Cron으로 돌리니까 별도 서버 없이 매일 자동 수집이 된다 JSON 설정 파일로 항목을 추가하거나 빼니까 코드를 건드릴 일이 없다 dry-run 옵션 덕분에 실제 메시지 발송 없이 파이프라인 전체를 검증할 수 있다 관련 글\n메달리온 아키텍처 — 데이터를 세 겹으로 쌓는 이유\r— 자동화 파이프라인의 레이어 설계 원칙 Bronze 레이어 — 원본을 있는 그대로 쌓는다\r— 외부 데이터 수집과 적재 전략 원문: https://github.com/yeseoLee/Macro-Pulse\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-31-automation-macro-report-telegram-bot/","summary":"증시 지수, 금리, 원자재 가격을 매일 수동으로 수집하는 반복 업무를 없앤 자동화 파이프라인이다. GitHub Actions와 Python으로 서버 없이 돌아가고, 결과는 텔레그램으로 받는다.","title":"지루한 지표 수집 업무를 자동화로 해결하는 법"},{"content":"AI 에이전트에 \u0026ldquo;발표 자료 만들어줘\u0026quot;를 던지면 매번 레이아웃이 달라진다. 외부 CDN을 당겨오다 로컬에서 깨지기도 하고, 프롬프트를 아무리 다듬어도 결과가 제각각인 문제는 해결되지 않는다. make-slide는 에이전트에게 디자인 지침서를 먼저 읽히는 방식으로 이 문제를 다룬다.\n에이전트에게 지침서를 먼저 읽힌다 make-slide는 npx 명령 하나로 초기 설정을 마치면 특정 경로에 테마 지침서 파일이 생긴다. 이후 에이전트는 백지에서 디자인을 고민하지 않고 이 파일을 읽어 정해진 규칙대로 HTML을 만든다. 레이아웃을 말로 설명할 필요가 없다. 경로만 넘기면 된다. 토큰도 줄어든다.\n입력은 가리지 않는다. 주제 한 줄이든, 텍스트 블록이든, 발표 대본 전체든 에이전트가 분석해 아웃라인을 제안한다. 중앙 정렬이나 분할 배치 같은 레이아웃 옵션 중 맥락에 맞는 걸 고르고, 승인하면 구현 단계로 넘어간다. 결과물은 단일 HTML 파일이라 브라우저 외에 아무것도 필요 없다.\n지침서 패턴은 슬라이드 밖에서도 쓸 수 있다 디자인팀 가이드라인을 지침서 파일에 넣어두면 에이전트가 그대로 따른다. 조직 전용 테마를 추가해 표준 템플릿으로 운영하는 것도 가능하다.\n.claude/skills/ 구조로 에이전트에게 도메인 지식이나 디자인 원칙을 파일로 쥐여주면 복잡한 작업이 명령 하나로 처리된다. 막대그래프나 지표 카드 같은 시각화도 별도 라이브러리 없이 된다. 파일이 가벼우니 GitHub Pages로 바로 공유할 수 있다.\n원문: https://github.com/Kuneosu/make-slide\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-30-ai-eijeonteuwa-make-slidero-guhyeonhaneun-gopumjil/","summary":"AI 에이전트에게 디자인 지침서를 먼저 읽히고 슬라이드를 만들게 하는 방식인데, 결과물 품질이 눈에 띄게 일정해진다.","title":"AI 에이전트와 make-slide로 구현하는 고품질 HTML 슬라이드 자동화"},{"content":"n8n으로 자동화 파이프라인을 처음 짤 때 가장 많이 걸리는 지점이 있다. 루프가 한 번만 돌고 끝난다거나, 에러가 나도 워크플로우가 멈추지 않는다거나. 대부분 노드 사이 데이터 흐름 구조를 파악하지 않은 채 시작해서 생기는 문제다. n8n은 자바스크립트로 세밀하게 제어할 수 있고, 직접 서버에 올리면 운영 비용도 낮출 수 있다.\n배열로 흐른다는 걸 먼저 알아야 한다 노드 사이를 흐르는 데이터는 배열 형태다. 목록으로 쏟아지는 결과를 하나씩 처리하려면 Split In Batches로 분리해야 한다. 흩어진 데이터를 다시 합칠 때는 Merge 노드를 쓴다. 이 흐름을 놓치면 반복 작업이 단발성 실행에 그친다.\n표현식 매핑은 조건문과 텍스트 가공을 즉석에서 처리할 수 있다. 정규식이 복잡하게 얽히기 시작하면 Code Node로 전환하는 게 낫다. 긴 수식보다 명시적으로 작성한 스크립트가 나중에 훨씬 읽기 편하다.\n키를 워크플로우 안에 박으면 안 된다 인증 키를 워크플로우 내부에 직접 넣는 방식은 위험하다. Credentials 메뉴에서 별도로 관리하고, 환경 변수로 개발과 운영 서버 주소를 분리해야 한다. 유통업 API를 붙이다가 테스트 데이터가 운영 환경에 섞인 적이 있다. 그때 이 분리가 얼마나 중요한지 체감했다.\n트리거 선택도 명확해야 한다. 외부 신호에 반응할 때는 Webhook, 주기 실행에는 Schedule 노드를 쓴다. 폴링 방식으로 데이터를 가져올 때는 마지막으로 처리한 지점을 반드시 저장해서 중복을 막아야 한다. 에러 전용 워크플로우를 별도로 만들고 메신저 알림까지 연결해두면 장애 대응 속도가 달라진다.\nDocker로 올릴 때 볼륨 빠뜨리면 생기는 일 재시작 시 워크플로우가 사라지는 상황은 볼륨 마운트가 빠져서 생기는 경우가 많다. 무거운 파일을 다룰 때는 메모리 할당량도 미리 점검해야 프로세스가 죽는 걸 막을 수 있다.\n이미지나 PDF를 다룰 때는 Binary Data 모듈을 쓴다. 파일 이름이 깨지거나 형식을 못 읽는 문제는 MIME 타입 헤더를 명시하면 대부분 해결된다. 금융 업종 프로젝트에서 영수증 파이프라인을 구축하다 이 지점에서 시간을 꽤 썼다. 자주 쓰이는 패턴은 Sub-workflow로 묶어 재사용한다.\n핵심만 뽑으면\n노드 사이 데이터가 배열로 흐르니까 Split In Batches 없이는 루프가 한 번만 돌고 끝난다 표현식이 복잡해지면 Code Node로 옮기는 게 나중에 훨씬 손보기 쉽다 Credentials 분리와 Error Workflow를 갖춰야 운영 단계에서 사고가 안 난다 관련 글\n메달리온 아키텍처 — 데이터를 세 겹으로 쌓는 이유\r— 데이터 파이프라인의 레이어 분리 원칙 Bronze 레이어 — 원본을 있는 그대로 쌓는다\r— 원본 데이터 수집 전략과 CDC 소스\nhttps://youtube.com/watch?v=y9u1IdDYHZQ\u0026amp;si=n_kmaaX4HH9MHwiS\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-30-n8n-weokeupeulrou-hyoyuleul-nopineun-deiteo-gujo-s/","summary":"n8n으로 자동화 파이프라인을 짜다 보면 아이템 흐름과 에러 처리에서 막히는 지점이 생긴다. 그 지점들을 실무 경험 기반으로 정리한 영상이다.","title":"n8n 워크플로우 효율을 높이는 데이터 구조 설계법"},{"content":"PDF를 그냥 읽어 들이면 표 구조가 무너지고 제목 계층이 사라진다. LLM은 망가진 입력으로도 그럴듯한 답을 만들어내니 문제가 있다는 걸 한참 뒤에야 알아채게 된다. RAG 품질은 생성 단계가 아니라 전처리 단계에서 이미 갈린다.\n텍스트 추출과 구조 보존은 다르다 Microsoft의 MarkItDown은 단순히 텍스트를 뽑아내는 게 아니다. 표와 제목 계층을 Markdown 형식으로 온전히 보존해 LLM이 읽기 좋은 상태로 만든다. Word, PowerPoint, Excel, PDF는 물론 영상 자막, 음성 파일까지 처리한다. 파일 안의 이미지는 vision 기능을 연결해 해석하고, 스캔 문서는 OCR로 정리한다.\nMarkdown이 LLM에 유리한 이유는 단순하다. HTML이나 JSON 대비 토큰 효율이 좋고, 언어 모델이 학습 데이터에서 이미 많이 접한 형식이라 추론 정확도가 올라간다. 전처리 단계에서 구조를 잡아두면 retrieval과 generation 양쪽 모두 이득이다.\n0.1.0 이후 뭐가 바뀌었나 파일 경로를 직접 입력받던 방식에서 binary stream 기반으로 바뀌었다. 임시 파일을 만들지 않고 메모리에서 바로 처리하니 서버 리소스 효율이 올라간다. 이전 방식에 익숙하다면 연동 코드를 점검해야 한다.\n전체 기능을 한 번에 설치하거나 필요한 포맷 지원만 골라 담을 수 있다. MCP 서버를 지원하니 Claude 같은 LLM 데스크톱 앱과 직접 연결도 된다. OCR이 라이브러리 자체에 내장돼 있어 별도 시스템 설치 없이 배포할 수 있다.\n관련 글\n메달리온 아키텍처 — 데이터를 세 겹으로 쌓는 이유\r— 데이터 파이프라인 레이어 설계 원칙 Silver 레이어 — Bronze를 분석 가능한 상태로 올린다\r— 데이터 정제와 품질 게이트 defuddle로 웹페이지를 변환하며 마주한 의외의 벽\r— 웹 데이터 전처리의 현실적 한계 원문: https://github.com/microsoft/markitdown\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-30-deiteo-jeonceoriga-gareuneun-rag-pumjilgwa-makeuda/","summary":"PDF나 Word 파일을 LLM에 넣기 전에 표 구조와 제목 계층을 살려서 Markdown으로 바꿔주는 도구인데, 전처리 공수가 줄어드는 게 생각보다 크다.","title":"데이터 전처리가 가르는 RAG 품질과 마크다운 변환 도구 활용법"},{"content":"법령 원문을 공공기관 API로 가져오는 것까지는 된다. 문제는 그다음이다. 시행령 위임 관계를 따라가다 보면 조문 번호 체계가 꼬이고, 경과규정에서 막힌다. 데이터를 가져오는 것과 해석하는 것은 완전히 다른 문제다.\n법제처 API 앞에서 막히는 이유 대한민국에는 1,600개가 넘는 법률과 10,000개가 넘는 행정규칙이 있다. 이 정보는 개발자 경험이 열악한 정부 API 뒤에 쌓여 있다. 접근 자체가 일이다.\nkorean-law-mcp는 이 문제를 64개의 구조화된 tool로 정리했다. 조문 텍스트 조회, 약어 자동 해석, HWPX 첨부파일의 Markdown 변환까지 된다. MCP 서버나 CLI 모드 둘 다 쓸 수 있어 Claude Desktop 같은 AI 클라이언트와 연동하기 편하다.\n원문을 가져와도 관계는 따로 잡아야 한다 파이프가 아무리 좋아도 맥락을 구조화하지 않으면 AI는 추론에 기댄다. 산업안전보건법 제38조 텍스트를 정확히 가져왔어도, 이 조문이 시행령 어느 조항으로 위임되고 다시 시행규칙으로 재위임되는지 AI 스스로 파악하기는 어렵다. 경과규정이나 관련 판례까지 포함하면 불확실성은 더 커진다.\nDataNexus의 온톨로지 레이어는 이 연결 고리를 knowledge graph 노드로 명시적으로 구축한다. \u0026ldquo;제38조 → 시행령 제○조 위임 → 시행규칙 제○조 재위임 → 관련 판례 3건 → 최근 개정으로 경과규정 적용 중\u0026quot;이라는 관계가 사전에 잡혀 있으면 AI가 추론하는 게 아니라 그래프를 탐색하면 된다.\n두 레이어를 붙이면 달라지는 것 korean-law-mcp가 데이터를 가져오는 파이프라면, DataNexus는 그 데이터를 해석하고 연결하는 레이어다. 지식 그래프 기반 접근은 법률 이외의 전문 도메인에서도 같은 방식으로 작동한다.\n관련 글\nDataHub Glossary를 온톨로지로 쓸 수 있을까\r— DataNexus 온톨로지 레이어의 설계 배경 SKOS 호환 레이어를 왜 넣었는가\r— 외부 표준 온톨로지와의 연동 전략 AI 에이전트 개발을 위한 한국 법령 데이터 활용법\r— JSON 규격화된 법령 데이터 API 소스\nhttps://github.com/chrisryugj/korean-law-mcp\r","permalink":"https://datanexus-kr.github.io/curations/2026-03/2026-03-30-beobryul-deiteo-paipeureul-neomeo-noereul-eodda-ko/","summary":"korean-law-mcp가 법제처 API를 64개 tool로 정리해 파이프 역할을 하고, DataNexus 온톨로지 레이어가 법령 간 위임 관계를 그래프로 잡아주면 AI가 추론 대신 탐색으로 법령을 해석할 수 있다.","title":"법률 데이터, 파이프를 넘어 뇌를 얻다: korean-law-mcp와 DataNexus의 시너지"},{"content":"\rGEO 최적화 Guide — 전체 시리즈\n1. GEO란 무엇인가 - SEO 너머의 AI 인용 전략\r2. AI마다 인용하는 소스가 다르다 ← 현재 글\r3. On-Site GEO 기술 구조 - 상품 DB에서 JSON-LD까지\r4. Off-Site GEO - 공식 사이트를 안 보는 AI에게 선택받는 법\r5. AEO - 코딩 에이전트가 읽는 문서는 왜 다른가\r\u0026ldquo;AI에서 잘 나온다\u0026quot;는 말을 곧이곧대로 믿으면 안 된다 GEO를 적용하겠다고 마음먹은 뒤, 가장 먼저 하는 게 ChatGPT에 자사 브랜드를 물어보는 거다. \u0026ldquo;우리 회사 나온다, 잘 되네.\u0026rdquo; 여기서 끝나는 경우가 많다.\n근데 같은 질문을 Perplexity에 던지면 다른 답이 나온다. Google AI Overview에서는 또 다르다. 어떤 플랫폼에서는 공식 사이트가 인용되고, 어떤 플랫폼에서는 블로그 리뷰가 출처로 잡힌다. 심지어 같은 ChatGPT 안에서도 웹서치 모드를 켰느냐 껐느냐에 따라 인용 소스가 달라진다.\nAI 검색을 하나의 채널처럼 취급하면 안 된다. 플랫폼마다 인용 로직이 다르고, 선호하는 소스 유형이 다르다.\n플랫폼별 인용 소스, 데이터로 보면 이렇다 세 곳의 분석 데이터를 합쳐보면 플랫폼 간 차이가 뚜렷하게 드러난다. Yext, Qwairy, GrackerAI 각각 규모는 달라도 방향은 같다.\n플랫폼 선호 소스 특징 Gemini 공식 사이트 (52%) Google 검색 인덱스 기반. 구조화 데이터가 있는 자사 도메인을 우선 ChatGPT 디렉토리/리스팅 (49%) Yelp, TripAdvisor 같은 서드파티 집계 사이트 의존도 높음 Perplexity Reddit/커뮤니티 (31%) 실사용자 토론 스레드를 적극 인용. 업종별 전문 디렉토리도 활용 Google AIO YouTube (23%) 6개월간 인용 점유율 34% 성장. 영상 콘텐츠 비중이 독보적 Gemini는 구글 검색 로직을 물려받아서 공식 사이트의 구조화된 콘텐츠를 신뢰한다. ChatGPT는 외부 검색 레이어에 의존하다 보니 디렉토리와 리스팅 사이트의 영향을 많이 받는다. Perplexity는 사람들이 실제로 의견을 나눈 커뮤니티 스레드에서 답을 끌어온다.\n재미있는 건 플랫폼 간 인용 소스의 겹침이 거의 없다는 점이다. ChatGPT와 Perplexity가 공통으로 인용하는 도메인은 전체의 11%에 불과하다. 한쪽에서 잘 보인다고 다른 쪽에서도 보이는 게 아니다.\n소스와 인용은 다른 개념이다 AI 답변을 자세히 보면 두 가지 형태의 출처 표기가 있다.\n하나는 답변 하단에 참고 링크로 깔리는 소스(Source) 로, URL이 신뢰할 만하다고 판단되면 여기에 포함된다. 다른 하나는 답변 본문 중간에 하이퍼링크로 걸리는 인용(Citation) 인데, 본문의 특정 문장을 뒷받침하는 근거로 쓰인다.\n인용까지 가려면 두 단계를 통과해야 한다. URL 수준의 도메인 신뢰도, 그리고 해당 페이지 본문의 정보 신뢰도. 도메인은 믿을 만한데 본문 구조가 파싱하기 어렵다면 소스에는 뜨지만 인용에는 안 걸린다. 반대로 본문은 잘 구조화됐는데 도메인 자체가 약하면 역시 소스 목록에만 남는다.\n이전 글\r에서 다룬 GEO 3대 원칙과 연결하면 이렇다:\nIdentity → 도메인 신뢰도의 기반. GTIN, Organization 스키마가 여기에 기여 Context → 본문의 정보 품질. 카테고리, 용도, Variant 관계가 구조화되어야 파싱 가능 Citability → 소스에서 인용으로 넘어가는 관문. JSON-LD, FAQ Schema가 이 단계를 결정 웹서치 모드에 따라 결과가 달라진다 ChatGPT에서 같은 질문을 두 번 해보면 답이 달라지는 경우가 있다. 웹서치가 켜졌을 때와 꺼졌을 때의 차이다.\n웹서치가 꺼진 상태에서는 사전 학습 데이터를 기반으로 답한다. 학습 시점에 존재했던 콘텐츠가 인용 대상이 된다. 웹서치가 켜지면 실시간 크롤링 기반으로 전환된다. 이때는 현재 시점의 구조화 상태, robots.txt 허용 여부, 콘텐츠 최신성이 인용을 좌우한다.\nGEO 모니터링을 할 때 이 구분을 안 하면 오판하기 쉽다. \u0026ldquo;우리 브랜드 ChatGPT에서 잘 나오는데요?\u0026rdquo; — 웹서치 모드를 끄고 테스트한 거라면 사전 학습 데이터에서 나온 거다. 실시간 크롤링 기반으로 테스트해야 현재 GEO 상태를 정확히 볼 수 있다.\nGoogle AIO에서 YouTube가 급부상하고 있다 Google AI Overview의 인용 패턴에서 가장 눈에 띄는 변화는 YouTube다.\nYouTube가 AI Overview 인용 1위 도메인이다 (Ahrefs Brand Radar). 반년 사이에 점유율이 34% 늘었다. 소셜 플랫폼 중에서도 Reddit 다음으로 YouTube 인용이 많다 (OtterlyAI).\n조회수가 많은 영상이 인용되는 게 아니라는 점도 재밌다. AI에 인용된 YouTube 영상의 절반 가까이가 조회수 1,000도 안 됐다. 좋아요 수십 개짜리도 수두룩하다. AI는 인기도가 아니라 정보 구조를 본다. 타임스탬프, 챕터 구분, 명확한 제목 - 이런 게 인용 여부를 가른다.\n반면 ChatGPT와 Perplexity에서는 YouTube 인용을 거의 찾아보기 어렵다. 같은 영상 콘텐츠도 플랫폼에 따라 가치가 완전히 달라진다.\nrobots.txt를 보면 경쟁사의 AI 전략이 보인다 경쟁사가 GEO를 어떻게 접근하고 있는지 가장 빠르게 파악하는 방법이 있다. https://경쟁사도메인/robots.txt를 브라우저에 입력하면 된다.\n커머스 업종은 상품 카탈로그 보호를 위해 AI 크롤러를 차단하는 경우가 많다. 가격, 재고, 상품 상세 정보가 경쟁사 AI에 노출되는 걸 막으려는 거다. B2B SaaS는 반대로 최대한 열어둔다. AI 검색에서 노출되는 게 리드 확보에 유리하니까.\n대규모 그룹사의 경우 계열사별로 robots.txt 정책이 제각각인 경우가 많다. 어떤 계열사는 GPTBot을 차단하고, 어떤 계열사는 전체 허용이다. 그룹 차원에서 일관된 정책 없이 각 사가 알아서 설정한 결과다.\n실제로 대형 유통 그룹의 계열사들을 전수 조사해보니, FAQPage Schema를 적용한 회사는 0개, AI 인용이 양호한 회사는 호텔 계열 1개뿐이었다. robots.txt를 열어놨어도 구조화 데이터가 없으면 AI 입장에서는 읽을 게 없다.\n# 차단 상태 (GEO 불가) User-agent: GPTBot Disallow: / # 허용 상태 (GEO 가능) User-agent: GPTBot Allow: / User-agent: Google-Extended Allow: / User-agent: anthropic-ai Allow: / 새 글 없이 구조만 바꿔도 인용률이 달라진다 GEO를 시작할 때 가장 흔한 오해가 \u0026ldquo;콘텐츠를 새로 만들어야 한다\u0026quot;는 거다. 물론 새 콘텐츠가 도움이 되지만, 기존 콘텐츠의 구조를 바꾸는 것만으로도 AI 인용 가능성이 달라진다.\n구조화 데이터를 적용한 페이지는 AI Overview에 뜰 확률이 36% 높고 (GrackerAI), 완전한 Schema를 적용하면 ChatGPT 노출 확률이 80% 까지 올라간다 (Search Engine Land). 기본 Schema만 있으면 20%.\n기존 블로그 글에 할 수 있는 것들:\n글 상단에 핵심 요약(TLDR) 40~60단어 추가 저자 바이오와 전문성 표기 FAQ 섹션을 추가하고 FAQPage Schema로 래핑 \u0026lt;meta name=\u0026quot;description\u0026quot;\u0026gt;을 100자 이상 구체적으로 수정 콘텐츠 최신성도 중요하다. Perplexity에서 높은 인용을 받은 페이지 4분의 3 이상이 한 달 이내에 업데이트된 것이었다. 석 달 넘게 손 안 댄 페이지는 밀린다.\nReddit이 한국어 쿼리에 뜨는 건 우연이 아니다 한국어로 질문했는데 Reddit 스레드가 인용되는 경우가 있다. 이상해 보이지만 구조적인 이유가 있다.\nReddit은 AI 번역과 hreflang 태그를 활용해서 22개 언어 버전을 운영하고 있다. 한국어 쿼리에 매칭될 수 있는 구조가 이미 갖춰져 있는 거다. Perplexity의 전체 인용 중 Reddit이 6.6%를 차지하고, Google AI Overview에서도 2.2%로 상위권이다. 특히 \u0026ldquo;best ○○\u0026rdquo;, \u0026ldquo;○○ 추천\u0026rdquo; 같은 주관적 쿼리에서 Reddit의 점유율이 높아진다.\n이건 Off-Site GEO 관점에서 시사하는 바가 크다. 자사 사이트의 구조화만으로는 커버하지 못하는 영역이 있다. AI가 \u0026ldquo;사람들의 실제 의견\u0026quot;을 우선하는 쿼리 유형에서는 커뮤니티와 리뷰 플랫폼의 영향력이 크다.\n하나의 전략으로 모든 AI를 커버할 수 없다 플랫폼마다 뭘 믿는지가 다르다:\n신뢰 기반 설명 유리한 플랫폼 자사 사이트 구조화 Schema.org, JSON-LD, FAQ Gemini, Google AIO 서드파티 리스팅 정합성 디렉토리, 리뷰 사이트 정보 일치 ChatGPT 커뮤니티 평판 Reddit, 포럼, UGC Perplexity 영상 콘텐츠 구조화 YouTube 챕터, 타임스탬프 Google AIO On-Site GEO가 기본이다. robots.txt를 열고, JSON-LD를 넣고, FAQ를 구조화하면 Gemini와 Google AIO에서 특히 효과가 크다.\nChatGPT에서 노출되려면 거기에 더해서 서드파티 리스팅의 정합성까지 챙겨야 한다. Perplexity까지 노리면 커뮤니티에서의 자연스러운 언급이 필요하다.\n이 시리즈는 On-Site GEO에 집중한다. 다만 어떤 플랫폼을 먼저 노리냐에 따라 Off-Site까지 손 대야 할 수도 있다.\n","permalink":"https://datanexus-kr.github.io/guides/geo-optimization/002-ai-citation-sources/","summary":"ChatGPT는 Wikipedia를, Perplexity는 Reddit을, Gemini는 공식 사이트를 선호한다. 하나의 전략으로 모든 AI 플랫폼에 대응하는 건 불가능하다.","title":"2. AI마다 인용하는 소스가 다르다"},{"content":"\rGEO 최적화 Guide — 전체 시리즈\n1. GEO란 무엇인가 - SEO 너머의 AI 인용 전략 ← 현재 글\r2. AI마다 인용하는 소스가 다르다\r3. On-Site GEO 기술 구조 - 상품 DB에서 JSON-LD까지\r4. Off-Site GEO - 공식 사이트를 안 보는 AI에게 선택받는 법\r5. AEO - 코딩 에이전트가 읽는 문서는 왜 다른가\r구글 1페이지 노출만으로 충분할 줄 알았다 SEO를 열심히 해서 구글 검색 1페이지에 올라갔다. 자연스럽게 검색 유입도 늘었다. 여기까지는 익숙한 시나리오다.\n요즘 주변에서 검색하는 방식이 달라졌다. ChatGPT에 \u0026ldquo;가성비 노트북 추천해줘\u0026quot;라고 치고, Perplexity에서 \u0026ldquo;서울 가족여행 호텔\u0026quot;을 찾는다. Google AI Overview가 검색 결과 위에 답을 먼저 깔아버린다.\n클릭이 사라지고 있다. AI가 대신 답해주니까.\nAI가 인용하는 URL 중 구글 검색 상위 10위에 드는 건 9% 에 불과하다 (Ahrefs). SEO 상위 노출이 AI 인용을 보장하지 않는다. 별도의 최적화 레이어가 필요하다.\n그게 GEO(Generative Engine Optimization)다.\nGEO는 무엇인가 ChatGPT, Perplexity, Gemini, Google AI Overview 같은 AI 검색엔진이 우리 콘텐츠를 답변에 인용하게 만드는 게 GEO다. 데이터 구조 자체를 AI가 읽기 좋게 바꾸는 작업이다.\nSEO는 사람이 클릭하게 만드는 거였다. GEO는 AI가 우리를 출처로 찍게 만드는 거다.\n구분 SEO GEO 목표 클릭 유도 (Traffic) AI 답변 내 인용 (Citation) 신뢰 기준 키워드 밀도, 백링크 수 식별 가능성, 구조화 데이터 인식 주체 사람 + 검색엔진 봇 생성형 AI 모델 핵심 기술 메타태그, 콘텐츠 최적화 JSON-LD, Schema.org, FAQ 구조화 KPI 노출 순위, CTR 언급률, 인용 정확도 오해하면 안 되는 게, GEO한다고 SEO를 버리는 게 아니다. SEO 기반이 탄탄해야 GEO도 먹힌다. GEO는 그 위에 얹는 거다.\n왜 지금인가 데이터를 보면 이미 흐름이 바뀌고 있다.\nChatGPT WAU가 8억을 넘었다 (OpenAI). 한국은 세계 2위 유료 구독 시장이고, 경제활동인구 셋 중 하나는 AI를 쓴다. Gartner는 2026년까지 전통 검색량이 25% 줄어들 거라고 본다 (Gartner). Capgemini 보고서를 보면 소비자 3분의 2 이상이 AI 추천 제품을 실제로 산다 (Capgemini). Google 검색의 절반 이상이 제로클릭 으로 끝나는데, AI Overview가 깔리면 이 비율은 더 올라간다.\n전환율 쪽이 더 흥미롭다. AI 검색 유입의 구매 전환율이 14.2%다. 전통 구글 검색 대비 5배 (GrackerAI). AI 유입 방문당 매출도 기존보다 2.5배 이상 높다 (Adobe).\n트래픽은 줄어드는데 AI가 골라준 결과의 전환율은 오히려 높다. 많이 노출되느냐보다, AI한테 선택되느냐가 매출을 가른다.\nGEO 3대 원칙 GEO를 적용할 때 매번 부딪히는 질문이 있다. 이 상품을 AI가 다른 것과 구분할 수 있나? 용도와 맥락을 알아채나? 그리고 읽은 다음에 출처를 달아줄 수 있는 구조인가?\nIdentity - 식별 가능성 AI가 상품이나 서비스를 명확히 구분할 수 있어야 한다.\nGS1 GTIN/GLN 같은 국제 표준 식별자가 핵심이다. \u0026ldquo;초코스틱 오리지널\u0026quot;과 \u0026ldquo;초코스틱 아몬드\u0026quot;를 AI가 별개 상품으로 인식하려면 각각 고유한 GTIN이 있어야 한다. 대표코드 하나로 묶어놓으면 AI는 둘을 구분하지 못한다.\nContext - 맥락 연결성 상품의 용도, 관계, 위치를 AI가 이해할 수 있어야 한다.\n카테고리 계층, Variant(맛/용량/색상) 관계, 브랜드-제품-SKU 구조. 이런 맥락이 구조화되어 있어야 AI가 \u0026ldquo;20대 남성에게 어울리는 운동화\u0026quot;라는 질문에 적절한 상품을 연결할 수 있다.\nCitability - 인용 가능성 AI가 콘텐츠를 읽고 출처를 밝힐 수 있는 구조여야 한다.\nJSON-LD, FAQ Schema, robots.txt 설정이 여기에 해당한다. 아무리 좋은 데이터라도 AI 크롤러가 접근할 수 없거나 파싱하기 어려운 구조면 AI는 그냥 넘겨버린다.\n표로 보면 이렇다:\n원칙 핵심 질문 핵심 기술 검증 기준 Identity AI가 이걸 다른 것과 구분하는가? GS1 GTIN/GLN 식별자 등록, Variant 구분 Context AI가 용도와 관계를 이해하는가? 카테고리, 지식그래프 메타데이터 품질, 채널 간 정합성 Citability AI가 읽고 출처를 밝힐 수 있는가? JSON-LD, FAQ, robots.txt 구조화 데이터 유효성, 크롤러 접근 허용 Invisible GEO vs Visible GEO 구현 방식은 두 갈래로 나뉜다.\nInvisible GEO 는 \u0026lt;head\u0026gt; 태그 안의 JSON-LD다. 사용자 눈에는 안 보이지만 AI와 검색엔진이 직접 파싱한다. AI 인용률을 끌어올리는 가장 강력한 방법이다. 다만 SPA로 되어 있다면 SSR(Server-Side Rendering) 전환이 선행되어야 한다.\n\u0026lt;!-- Invisible GEO: \u0026lt;head\u0026gt; 안의 JSON-LD --\u0026gt; \u0026lt;script type=\u0026#34;application/ld+json\u0026#34;\u0026gt; { \u0026#34;@context\u0026#34;: \u0026#34;https://schema.org\u0026#34;, \u0026#34;@type\u0026#34;: \u0026#34;Product\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;초코스틱 오리지널\u0026#34;, \u0026#34;gtin13\u0026#34;: \u0026#34;8801234567890\u0026#34;, \u0026#34;brand\u0026#34;: { \u0026#34;@type\u0026#34;: \u0026#34;Brand\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;K식품\u0026#34; }, \u0026#34;offers\u0026#34;: { \u0026#34;@type\u0026#34;: \u0026#34;Offer\u0026#34;, \u0026#34;price\u0026#34;: 1500, \u0026#34;priceCurrency\u0026#34;: \u0026#34;KRW\u0026#34;, \u0026#34;availability\u0026#34;: \u0026#34;https://schema.org/InStock\u0026#34; } } \u0026lt;/script\u0026gt; Visible GEO 는 \u0026lt;body\u0026gt; 안의 HTML 콘텐츠다. FAQ 페이지, 상품 상세 설명, 영양정보 표. 사람도 읽고 AI도 읽는다. 기술적 장벽이 낮아서 당장 시작할 수 있다.\n항목 Invisible GEO Visible GEO 위치 \u0026lt;head\u0026gt; JSON-LD \u0026lt;body\u0026gt; HTML SEO 효과 높음 보통 AI 인용률 높음 높음 구현 난이도 높음 (SSR 필요) 낮음 사용자 경험 없음 (기계 전용) 직접 노출 실무에서는 둘 다 쓴다. JSON-LD로 기계가 파싱하기 좋게 넣고, HTML로 사람과 AI 모두 읽을 수 있게 깔아두는 식이다.\nAI는 이미 잘 답하고 있다, 문제는 출처다 \u0026ldquo;가족 여행 호텔 추천해줘.\u0026rdquo; 이 질문을 Genspark, Perplexity, ChatGPT에 동시에 던져봤다. 세 AI 모두 비슷한 답을 내놓는다. 수영장 정보, 객실 가격, 조식까지. 답변 품질은 이미 충분하다.\n문제는 출처다. Genspark은 공식 사이트의 Schema 데이터를 직접 인용하고, Perplexity는 네이버 블로그와 여기어때를 긁어온다. ChatGPT는 공식 사이트를 참조하지만 구조화 데이터 없이는 정밀도가 떨어진다. 같은 호텔인데 AI마다 보여주는 가격이 다르다.\nSchema가 공식 인용을 보장하진 않는다. AI가 공식 사이트를 쉽게 파싱할 수 있게 만들어서, 블로그 대비 공식 출처 선택 확률을 높이는 거다. On-Site GEO가 중요한 이유가 여기 있다.\nDemo - AI 검색 비교\r연구 결과가 말해주는 것 Princeton대와 Georgia Tech 연구팀\r이 만 건의 쿼리를 분석했는데, 결과가 꽤 뚜렷하다:\n출처를 명시한 콘텐츠: AI 가시성 +40% 통계를 포함한 콘텐츠: +30% 키워드를 반복 삽입한 콘텐츠: 오히려 -10% SEO에서 통하던 키워드 반복이 GEO에서는 오히려 깎인다. AI는 같은 단어가 몇 번 나왔는지가 아니라, 정보가 얼마나 체계적이고 믿을 만한지를 본다.\n구조화 데이터 쪽 실증도 쌓이고 있다. AI Overview에 인용된 브랜드는 자연 검색 클릭률이 35% 높고, 유료 광고 클릭률은 거의 두 배까지 올랐다 (Seer Interactive). 구조화 데이터를 넣으면 AI Overview에 뜰 확률이 36% 올라간다 (GrackerAI). 완전한 Schema를 적용한 사이트가 ChatGPT에 노출될 확률은 80%, 기본 Schema만 있으면 20% (Search Engine Land).\n콘텐츠 최신성도 빼놓을 수 없다. Perplexity에서 높은 인용을 받은 페이지 4분의 3 이상이 한 달 이내에 업데이트된 것이었다. 석 달 넘게 손 안 댄 페이지는 밀린다.\nOn-Site GEO와 Off-Site GEO GEO는 크게 두 영역으로 나뉜다.\n구분 On-Site GEO Off-Site GEO 정의 자사 사이트를 AI가 읽고 답변에 사용하게 만들기 AI가 참고하는 외부 사이트에 브랜드 노출 핵심 기술 JSON-LD, Schema.org, SSR, robots.txt, FAQ Reddit, Wikipedia, 뉴스, 커뮤니티 담당 개발팀 / 기술 조직 마케팅 / PR / 브랜드 전략 이 시리즈에서는 On-Site GEO 에 집중한다. 개발자가 코드로 바로 적용할 수 있는 영역이다.\n","permalink":"https://datanexus-kr.github.io/guides/geo-optimization/001-what-is-geo/","summary":"구글 상위 10위 페이지 중 AI가 인용하는 비율은 9%에 불과하다. SEO 순위가 AI 인용을 보장하지 않는 시대, GEO의 3대 원칙과 학술 근거를 정리한다.","title":"1. GEO란 무엇인가 - SEO 너머의 AI 인용 전략"},{"content":"고객 테이블에 주민번호와 사업자번호가 같이 있다 기간계 시스템에서 흔히 보는 구조다. 고객 테이블 하나에 개인고객 속성(주민번호, 생년월일)과 법인고객 속성(사업자번호, 대표자명)이 섞여 있다. 개인고객이면 사업자번호가 NULL이고, 법인고객이면 주민번호가 NULL이다. 고객유형코드 컬럼 하나로 구분한다.\n데이터가 적을 때는 별 문제가 안 된다. 고객이 수천만 건이 되면 이야기가 달라진다. 개인고객에만 필요한 컬럼이 법인고객 행에도 자리를 차지하고, 법인고객에만 필요한 컬럼이 개인고객 행에서 NULL로 비어 있다. 컬럼이 늘어날수록 테이블이 와이드해지고 의미가 흐려진다. \u0026ldquo;이 컬럼이 어떤 고객 유형에 해당하는지\u0026quot;를 DDL만 보고 알기 어렵다.\n수퍼-서브 타입은 이 문제를 논리 모델 단계에서 정리하는 방법이다.\n공통 속성과 고유 속성을 분리한다 수퍼-서브 타입의 원리는 간단하다. 공통 속성은 수퍼 타입(고객)에 두고, 유형별 고유 속성은 서브 타입(개인고객, 법인고객)에 둔다.\n[고객] ← 수퍼 타입: 고객ID, 고객명, 연락처 ├─ [개인고객] ← 서브 타입: 주민번호, 생년월일 └─ [법인고객] ← 서브 타입: 사업자번호, 대표자명 고객ID 하나로 수퍼 타입과 서브 타입이 연결된다. 개인고객 테이블에는 개인고객에만 해당하는 속성만 들어간다. NULL 투성이의 와이드 테이블이 사라진다.\n서브 타입을 나누는 또 다른 이유가 있다. 서브 타입별로 다른 엔터티와 독립적으로 관계를 맺을 수 있다. 법인고객만 여신한도와 관계를 가진다든지, 개인고객만 멤버십 등급과 관계를 가진다든지. 수퍼 타입 하나에 모든 관계를 매달면 관계의 의미가 모호해지는데, 서브 타입으로 나누면 \u0026ldquo;이 관계가 어떤 유형에 해당하는지\u0026quot;가 모델에서 바로 읽힌다.\n배타적인가, 중복 가능한가 서브 타입을 설계할 때 반드시 먼저 따지는 게 있다. 하나의 인스턴스가 서브 타입 중 정확히 하나에만 속하는지(Exclusive), 여러 서브 타입에 동시에 속할 수 있는지(Inclusive)다.\nExclusive 가 압도적으로 많다. 고객은 개인 아니면 법인이다. 계좌는 보통예금, 적금, 정기예금 중 하나다. 상품은 실물이거나 디지털이다. 구분코드 하나로 분류가 끝난다.\nInclusive 는 드물지만 빠뜨리면 나중에 큰 수정이 필요하다. 서비스 상품 같은 경우가 해당된다. 하나의 상품이 B2B 대상이면서 동시에 B2C 대상일 수 있다. 직원 역할도 마찬가지다. 한 사람이 영업과 기술지원을 겸하는 경우, \u0026ldquo;직원역할\u0026rdquo; 서브 타입이 Inclusive가 된다.\n설계 초기에 \u0026ldquo;이 분류가 정말 배타적인가\u0026quot;를 한 번 더 따져야 한다. Exclusive로 전제하고 모델을 짰는데 겹치는 케이스가 나오면 구분코드 체계부터 관계 구조까지 뜯어고쳐야 한다.\n물리 모델로 넘어갈 때의 선택지 논리 모델에서 수퍼-서브 타입은 깔끔하다. 물리 모델로 전환할 때 선택이 갈린다.\n통합 테이블. 수퍼 타입과 서브 타입을 하나의 테이블로 합친다. 처음에 문제라고 했던 그 와이드 테이블이 되지만, 조인이 없어서 쿼리가 단순하다. 서브 타입별 고유 속성이 적으면 실용적인 선택이다.\n개별 테이블. 수퍼 타입 테이블과 서브 타입 테이블을 각각 만든다. NULL이 없고 구조가 명확하지만, 고객 정보를 온전히 보려면 수퍼 타입과 서브 타입을 조인해야 한다.\n서브 타입만. 수퍼 타입 테이블 없이 개인고객 테이블, 법인고객 테이블만 만든다. 공통 속성을 각 테이블에 중복으로 가진다. 서브 타입별로 완전히 독립적인 분석을 하는 경우에 맞지만, 고객 전체를 보려면 UNION이 필요하다.\n정답은 없다. 서브 타입 수, 고유 속성의 양, 쿼리 패턴에 따라 달라진다.\nDW 차원 설계로 이어지면 DW에서는 이 선택이 차원(Dimension) 설계와 직결된다. 2편\r에서 다뤘던 \u0026ldquo;접근 경로\u0026rdquo; 관점이 판단 기준이 된다.\n고객 차원을 설계한다고 하자. 개인고객과 법인고객을 별도 차원으로 나누면 팩트 테이블에 FK가 늘어나고, 분석할 때 어떤 차원을 조인할지 매번 선택해야 한다. 통합 차원으로 만들면 NULL이 많은 와이드 테이블이 되지만, 1편\r에서 다뤘던 것처럼 클라우드 Columnar Storage에서는 NULL 컬럼의 스캔 비용이 거의 없다.\n판단 기준은 분석 패턴이다. 개인고객 매출은 연령대와 지역으로 보고, 법인고객 매출은 산업군과 매출 규모로 본다면 차원 속성 자체가 다르니 나누는 게 낫다. 고객 전체를 하나의 축으로 놓는 분석이 대부분이면 통합이 편하다.\n실무에서 많이 보는 절충안은 통합 차원을 기본으로 두되, 서브 타입별 분석이 빈번할 때 별도 뷰나 마트를 추가하는 방식이다. 클라우드 환경에서 스토리지 비용이 낮으니 중복 저장 부담이 작다.\n다음 글에서는 Inmon 방식과 Kimball 방식을 비교한다. 1편에서 간략히 언급했던 내용을 더 구체적으로 들어간다.\n","permalink":"https://datanexus-kr.github.io/guides/dw-modeling/004-super-sub-type/","summary":"수퍼-서브 타입은 논리 모델에서 비즈니스 분류를 명확하게 만든다. 물리 모델로 넘어갈 때 세 가지 선택지가 생기고, DW에서는 그 선택이 차원 설계 전체를 바꾼다.","title":"4. 수퍼-서브 타입 - 고객이 개인이면서 법인일 수 있는가"},{"content":"툴을 바꾸면 모델이 다르게 보인다 DW 프로젝트에 투입되면 보통 기존 모델부터 받아서 훑는다. 이때 한 가지 확인하지 않으면 나중에 고생하는 게 있다. 모델을 그린 툴이 뭔지, 그 툴의 표기법이 뭔지.\nERwin으로 설계한 모델을 DA#으로 열면 관계선 해석이 달라진다. 관계선의 점선이 한쪽에서는 \u0026ldquo;비식별 관계\u0026quot;인데 다른 쪽에서는 \u0026ldquo;선택적 참여\u0026quot;다. 둘 다 정상이다. 표기법이 다를 뿐이다. 문제는 이걸 모른 채 모델 리뷰를 하면 같은 ERD를 놓고 서로 다른 이야기를 하게 된다는 것이다.\nERD는 모델러, 개발자, 현업이 공통으로 읽는 언어다. 그 언어에 방언이 여러 개 있다는 걸 모르면 소통이 꼬인다.\n같은 까마귀발인데 해석이 갈린다 ERD 표기법 중 가장 널리 쓰이는 건 까마귀발(Crow\u0026rsquo;s Foot) 계열이다. 관계선 끝에 대시(1), 원(0), 까마귀발(N) 기호를 조합해서 카디널리티를 표현한다. 여기까지는 공통이다.\n문제는 까마귀발 안에 두 계열이 있다는 점이다.\nIE(Information Engineering) 방식 은 ERwin, PowerDesigner 등에서 기본으로 쓴다. 식별 관계를 실선, 비식별 관계를 점선으로 구분한다. 식별 관계란 상위 엔터티의 PK가 하위 엔터티 PK의 일부로 포함되는 것이다.\nBarker 방식 은 Oracle 계열 툴과 DA#에서 쓴다. 겉보기엔 같은 까마귀발인데 기호의 의미가 다르다.\n기호 IE 방식 Barker 방식 점선 비식별 관계 선택적 참여 (0 or 1) 실선 식별 관계 필수 참여 (정확히 1) 대시 정확히 1 식별 관계 점선의 의미가 완전히 다르다. IE에서 점선은 \u0026ldquo;상위 PK가 하위 PK에 포함되지 않는다\u0026quot;는 구조적 정보다. Barker에서 점선은 \u0026ldquo;참여가 선택적이다\u0026quot;라는 비즈니스 규칙이다. 같은 기호가 다른 층위의 정보를 담고 있다.\nDA#으로 설계한 모델을 ERwin으로 가져와서 리뷰하면 이 차이를 모르는 사람이 점선을 전부 비식별 관계로 읽어버린다. 설계 의도가 그대로 왜곡된다.\n까마귀발을 안 쓰는 표기법도 있다 IDEF1X라는 표기법은 까마귀발 대신 원으로 카디널리티를 나타낸다. 원이 없으면 정확히 1, 빈 원은 0 또는 1, 속이 찬 원은 0 또는 N이다. 식별/비식별은 IE와 동일하게 실선/점선으로 구분한다.\n변형도 있다. 모든 원을 속이 찬 원으로 통일하면서 Z(0 or 1), P(1 or N) 같은 문자를 붙이는 방식이다. ERwin에서 IDEF1X 모드로 전환하면 이걸 쓸 수 있다.\n까마귀발 계열과 IDEF1X는 카디널리티를 표현하는 기호 체계 자체가 다르다. 까마귀발 계열 내에서의 혼동(IE vs Barker)이 같은 기호를 다르게 읽는 문제라면, IDEF1X와의 차이는 기호 자체를 모르면 읽지 못하는 문제다. 성격이 다른 혼동이다.\n프로젝트에서 부딪히는 현실 표기법을 외우라는 게 아니다. 프로젝트에서 실제로 생기는 문제를 미리 아는 게 중요하다.\n모델링 툴을 바꿀 때 표기법 전환이 자동으로 되기는 하지만 완벽하지 않다. 세부 표현이 달라지거나, 뒤에서 다룰 수퍼-서브 타입처럼 구조 자체가 바뀌는 경우도 있다. 같은 IE 방식이라도 툴에 따라 대시 2개를 하나로 표현하는 것 같은 미세한 차이가 있다.\n프로젝트 시작할 때 세 가지를 맞추면 된다.\n어떤 표기법을 쓸 건지 정한다 팀 전원이 해당 표기법의 기호 의미를 안다 툴의 도움말에서 표기법 상세를 한 번은 확인한다 이게 안 된 프로젝트에서는 모델 리뷰가 표기법 논쟁으로 빠진다. 설계를 논의해야 할 시간에 \u0026ldquo;이 선이 뭔 뜻이냐\u0026quot;를 따지게 된다.\n코드로 그리는 ERD 클라우드 DW 환경에서는 ERD를 GUI 툴로 그리지 않는 팀도 많다. dbt에서 SQL 기반으로 모델을 정의하고, Mermaid나 DBML 같은 텍스트 기반 다이어그램으로 관계를 표현한다. 코드 리뷰처럼 모델 변경 이력을 추적할 수 있다는 게 장점이다.\n도구가 바뀌어도 표현해야 하는 건 같다. 카디널리티, 식별/비식별 관계, 필수/선택 참여. 이 개념을 모르면 GUI 툴이든 텍스트 기반이든 모델을 제대로 읽을 수 없다.\n다음 글에서는 수퍼-서브 타입을 다룬다. 고객을 개인고객과 법인고객으로 나누는 구조인데, 논리 모델에서 물리 모델로 넘어갈 때 선택지가 갈리고 DW에서는 그 선택이 차원 설계로 이어진다.\n","permalink":"https://datanexus-kr.github.io/guides/dw-modeling/003-erd-notation/","summary":"같은 까마귀발인데 왜 해석이 다를까. 점선 하나가 툴마다 다른 뜻을 가진다. 프로젝트에서 모델을 공통 언어로 쓰려면 쓰는 도구의 표기법부터 맞춰야 한다.","title":"3. ERD 표기법 - 같은 그림, 다른 해석"},{"content":"\u0026ldquo;이 Unknown은 누가 넣은 건가요?\u0026rdquo; DW 모델 리뷰 자리에서 꼭 나오는 질문이다.\n상품 마스터 테이블을 열어보면 \u0026ldquo;Unknown\u0026quot;이라는 이름의 데이터가 들어 있다. 사원 테이블에도 있다. 기간계 시스템을 해온 사람이라면 당연히 의아하다. 마스터 테이블에 더미 데이터라니.\n비슷한 질문이 뒤따른다. \u0026ldquo;주문실적 테이블에 시점담당사원이라는 컬럼은 뭔가요? 기간계 주문 테이블에는 없던 건데.\u0026rdquo; DW 모델을 처음 접한 사람에게는 이것도 낯설다.\n두 모델의 차이를 키워드로 설명하는 자료는 많다. 비정규화, 스타스키마, 스노우플레이크. 검색하면 바로 나온다. 문제는 키워드만으로 \u0026ldquo;왜 이렇게 설계하는가\u0026quot;가 설명이 안 된다는 것이다. 목적부터 짚어야 한다.\n기간계 모델은 트랜잭션을 지킨다 OLTP 데이터 모델의 목표는 명확하다. 빈번한 입력과 수정 과정에서 정합성을 깨뜨리지 않는 것.\n이 목표가 모델의 생김새를 결정한다. 엔터티 사이의 관계가 엄격하다. 부서가 없으면 사원을 등록할 수 없고, 상품이 없으면 주문이 발생할 수 없다. 고객이 없는 주문도 존재하지 않는다. 모든 관계에는 선행 조건이 있고, 데이터가 발생하는 그 시점에 조건이 충족되어야 한다.\n이걸 보장하기 위해 정규화를 한다. 중복을 줄이면 수정할 곳이 한 군데로 줄고, 정합성이 깨질 여지가 작아진다. 최상위 마스터(코드 테이블 같은)부터 순서대로 등록하고, 그 위에 트랜잭션 데이터를 쌓는다. 순서가 틀어지면 안 된다.\n비유하면 이렇다. 할아버지가 있어야 아버지가 있고, 아버지가 있어야 아들이 있다. 존재 관계다. 사람이 있어야 사람의 행동이 기록된다. 행위 관계다. OLTP 모델은 이런 관계의 제약 조건을 빠짐없이 반영하는 데 집중한다.\nDW 모델은 접근 경로를 설계한다 DW 데이터 모델은 다른 문제를 푼다. 모든 데이터를 빠짐없이 적재하고, 분석 대상에 접근하는 경로를 만드는 것이다.\n\u0026ldquo;접근 경로\u0026quot;가 핵심이다. 주문실적이라는 분석 대상이 있다고 하자. 사원 기준으로도, 상품 기준으로도, 고객 기준으로도 들어갈 수 있어야 한다. 어느 경로로 가든 같은 결과가 나와야 하고, 성능도 비슷해야 한다. 스타스키마가 이 구조를 가장 직관적으로 표현한다.\n[사원] | [상품] — 주문실적 — [고객] | [직업] 주문실적을 중심에 놓고, 접근 경로가 되는 차원 테이블이 주변을 둘러싸는 형태다.\nOLTP 경험이 많은 사람이 이 모델을 보면 \u0026ldquo;비정규화한 OLTP\u0026quot;라고 오해하기 쉽다. ERD라는 도구가 같으니까 결과물도 같은 종류일 거라고 생각한다. 도구가 같을 뿐이다. 설계의 출발점이 다르다.\n시점 데이터라는 낯선 개념 OLTP 주문 테이블에는 \u0026ldquo;담당사원\u0026rdquo; 컬럼이 있다. 현재 담당사원을 가리킨다. DW의 주문실적 테이블에는 시점담당사원 이 있다. 주문이 발생한 바로 그 시점의 담당사원이다.\n왜 이런 게 필요한가. 상품 담당사원이 올해 A에서 B로 바뀌었다고 하자. OLTP에서는 현재 담당이 B다. 그걸로 끝이다. DW에서는 상황이 다르다. \u0026ldquo;작년 실적은 A 기준으로, 올해 실적은 B 기준으로 보고 싶다\u0026quot;는 요구가 자연스럽게 나온다. 상품담당사원이력이나 고객직업이력 같은 이력 데이터를 활용해서, 주문실적 적재 시점에 시점 데이터를 함께 만들어낸다.\nOLTP에서 퇴사한 사원은 마스터에서 비활성화하면 그만이다. DW에서는 과거 시점에만 존재했던 사원도, 더 이상 유효하지 않은 직업 코드도 마스터 테이블에 전부 남겨야 한다. 과거 분석에 필요한 데이터가 빠지면 안 되니까.\nUnknown이 존재하는 이유 DW 프로젝트에서 흔한 상황이 하나 있다. 과거 10년치 주문실적을 분석하려는데, 상품 마스터 관리가 부실해서 최근 상품만 남아 있다. 주문실적에는 상품ID가 찍혀 있는데 상품 테이블에는 해당 ID가 없다.\nOLTP였으면 이런 일 자체가 안 일어난다. 상품이 없으면 주문이 생길 수 없도록 설계했으니까. DW는 입장이 다르다. 이미 발생한 과거 데이터를 있는 그대로 적재해야 한다.\n이때 선택지가 몇 가지 있다.\n주문실적의 상품ID를 Unknown에 해당하는 ID로 바꾸거나 분석용 상품ID 컬럼을 하나 더 두어 이중 관리하거나 매핑 안 되는 상품ID를 적재 시점에 상품 마스터에 먼저 추가하고, 나머지 속성은 NULL이나 대체값으로 채우거나 어떤 방식이든 한 가지는 공통이다. 상품 마스터에 Unknown 이라는 기준 데이터를 미리 넣어 둔다는 것. 해당 상품의 담당사원도 알 수 없으니 사원 테이블에도 Unknown을 넣는다. 엔터티 간 관계를 형식적으로 충족시키되, 데이터가 발생한 시점이 아니라 적재하는 시점에 인위적으로 맞추는 방식이다.\nOLTP 모델러가 보면 불편할 수 있다. 인위적인 더미 데이터로 관계를 맞추다니. DW의 목적을 생각하면 합리적인 판단이다. 분석 대상 데이터를 빠뜨리지 않으면서, 어떤 접근 경로로 들어가든 일관된 구조가 유지되어야 하니까.\n관계를 맞추는 시점이 다르다 정리하면 이렇다.\nOLTP 는 데이터가 발생하는 시점에 관계 조건을 충족시킨다. 부서 없이 사원을 등록할 수 없고, 고객 없이 주문을 넣을 수 없다. 관계를 어기면 데이터 자체가 들어가지 않는다.\nDW 는 데이터를 적재하는 시점에 관계를 맞춘다. 원본에 누락이 있으면 Unknown으로 채운다. 과거 시점 데이터가 필요하면 이력에서 끌어와서 만든다. 적재 과정에서 일정한 개입을 통해 관계를 맞추는 방식이다.\nOLTP DW 목적 트랜잭션 처리, 정합성 보장 분석 데이터 적재, 접근 경로 설계 관계 충족 시점 데이터 발생 시점 데이터 적재 시점 누락 데이터 허용하지 않음 Unknown으로 처리 이력 관리 현재 상태 중심 시점 데이터 생성 설계 방향 정규화 (중복 최소화) 접근 경로 중심 (분석 편의) 이 차이를 알고 나면 DW 모델에서 \u0026ldquo;왜 이렇게 했지?\u0026ldquo;라는 의문이 상당 부분 풀린다.\n클라우드 시대에도 같은 이야기 이전 글\r에서 클라우드 DW의 물리적 제약 변화를 다뤘다. 스토리지가 싸졌고, 컬럼나 스토리지 덕에 조인 패턴이 달라졌고, ELT 패러다임으로 전환됐다.\n물리적 제약은 바뀌었지만 OLTP와 DW의 목적 차이는 여전하다. BigQuery를 쓰든 Synapse를 쓰든, 분석 데이터에 대한 접근 경로를 설계해야 하는 건 마찬가지다. Unknown 레코드가 필요한 상황도, 시점 데이터를 관리해야 하는 요건도 인프라가 바뀐다고 없어지지 않는다.\n달라진 게 있다면 이력 관리를 더 적극적으로 할 수 있게 됐다는 것 정도다. 스토리지 부담이 줄어서 SCD Type 2 방식으로 차원 이력을 쌓아도 부담이 덜하다. SCD 유형별 설계 방식은 시리즈 뒤쪽에서 다룬다.\n다음 글에서는 ERD 표기법 차이를 짚어본다. 같은 관계를 그려놓아도 Crow\u0026rsquo;s Foot이냐 IDEF1X이냐에 따라 해석이 달라진다. 표기법을 모르면 같은 모델을 보고도 서로 다른 이야기를 하게 된다.\n","permalink":"https://datanexus-kr.github.io/guides/dw-modeling/002-oltp-vs-dw-model/","summary":"ERD가 같아 보여도 설계 철학은 완전히 다르다. OLTP는 트랜잭션 정합성, DW는 분석 접근 경로. 그 차이가 Unknown 레코드와 시점 데이터 같은 낯선 것들을 만든다.","title":"2. OLTP vs DW 모델 - 목적이 다르면 설계도 다르다"},{"content":"왜 갑자기 외부 시스템 연동을 이야기하나 3편까지는 하나의 관점으로만 글을 썼다. NL2SQL 정확도. \u0026ldquo;LLM에게 비즈니스 맥락을 얼마나 잘 주입할 수 있느냐\u0026quot;가 모든 의사결정의 기준이었다.\n여기서부터 관점이 하나 더 추가된다. 플랫폼이다.\nDataNexus가 한 고객사 안에서만 돌아가는 NL2SQL 도구로 끝난다면, 외부 연동은 필요 없다. DozerDB 그래프가 잘 돌아가면 그만이다. 문제는 1편에서 이미 그보다 큰 그림을 그려놨다는 거다 -그룹사별 멀티테넌시, Data Moat, 시간축 지식그래프. 이 단어들은 전부 DataNexus가 단일 시스템을 넘어 여러 조직이 온톨로지를 교환하는 플랫폼이 되어야 한다는 전제 위에 있다.\n유통 그룹이 백화점·마트·온라인몰을 갖고 있는데, 관계사마다 \u0026ldquo;매출\u0026quot;의 정의가 다르다. 이걸 통합하려면 각 관계사의 온톨로지를 공통 포맷으로 내보내서 매핑해야 한다. DataNexus만의 독자 포맷으로는 이 작업이 안 된다.\nSKOS 호환 레이어가 NL2SQL 정확도를 직접 올려주진 않는다. 대신 다른 방식으로 도움이 된다.\n금융 도메인의 FIBO나 유통의 GPC 같은 산업 표준 온톨로지를 가져오면, 밑바닥부터 용어를 정의하는 시간이 줄어든다. 구축이 빨라지면 NL2SQL 엔진에 맥락이 주입되는 시점이 앞당겨진다. 1편에서 \u0026ldquo;범용 모델의 일반화 속도를 DataNexus의 데이터 축적 속도가 앞서야 한다\u0026quot;고 썼는데, 표준을 가져다 쓰는 것도 그 방법 중 하나다. 고객사가 이미 Collibra나 Alation을 쓰고 있는 경우, 표준 포맷으로 내보낼 수 없으면 도입 자체가 막힌다. NL2SQL 정확도가 아무리 높아도 기존 인프라와 공존 못 하면 현장에서 안 쓴다. 유통사 프로젝트에서 겪은 교훈이다 -기술보다 현장 적합성이 도입을 결정한다. 4편은 NL2SQL 엔진의 내부 성능 이야기가 아니다. DataNexus가 플랫폼으로 기능하기 위한 인터페이스 설계 이야기다. 관점이 다르니까 풀어야 할 문제도 다르다.\n외부 시스템과 연동이 안 됐다 이전 글\r에서 DataHub + DozerDB 이중 구조로 내부 온톨로지 문제를 풀었다. 내부 시스템에서만 쓰기에는 충분했다.\n문제는 외부 시스템과의 연동이었다. 금융 도메인을 탐색하다가 FIBO(Financial Industry Business Ontology)를 발견했는데, 금융업계 표준 용어 체계로 \u0026ldquo;Financial Product\u0026rdquo;, \u0026ldquo;Loan\u0026rdquo;, \u0026ldquo;Interest Rate\u0026rdquo; 같은 개념이 계층으로 정리돼 있다. 유통 쪽도 마찬가지다. GS1의 GPC(Global Product Classification)에는 \u0026ldquo;의류 → 여성복 → 원피스\u0026quot;처럼 상품 분류 체계가 표준으로 잡혀 있다. 의료엔 SNOMED CT, 제조엔 ISA-95. 도메인마다 수천 개 용어가 이미 정리돼 있는데, 이걸 가져다 쓸 수 있으면 온톨로지를 밑바닥부터 만들 필요가 없다.\nFIBO 파일을 열어봤다. OWL 포맷이었다. DozerDB 그래프에 넣으려니 구조 자체가 안 맞았다. 반대 방향도 마찬가지 -DataNexus 온톨로지를 고객사 기존 시스템(Collibra, TopBraid 같은)에 내보내고 싶어도 표준 포맷이 없으니 방법이 없었다. 내부에서는 잘 돌아가는데 밖으로 꺼내는 순간 무용지물이 되는 상황.\n외부 호환이 안 되면 생기는 문제가 한두 개가 아니다. 대기업은 이미 Collibra나 Alation 같은 메타데이터 관리 툴을 쓰고 있는 경우가 많다. DataNexus를 도입한다고 기존 용어 체계를 버리진 않는다. 표준 포맷으로 내보낼 수 있으면 공존이 가능한데, 못하면 용어 수백 개를 수작업으로 옮겨야 한다. 그것만으로 몇 달이 날아간다.\n유통 그룹처럼 백화점·마트·온라인몰이 각각 \u0026ldquo;매출\u0026quot;을 다르게 정의하는 경우, 그룹 차원에서 용어를 통합하거나 최소한 매핑하려면 공통 포맷이 있어야 한다. 없으면 관계사마다 따로 논다. 금융권은 감독 기관에 데이터 계보(lineage)나 용어 정의를 보고해야 하는 규제 요건도 있다. 거기에 벤더 종속 문제까지. DataNexus를 쓰다가 다른 플랫폼으로 바꿔야 할 수도 있는데, 표준 포맷으로 내보낼 수 있으면 옮길 수 있지만 안 되면 갇힌다. 도입을 결정하는 자리에서 이게 꽤 크게 작용한다.\n같은 그래프인데 언어가 다르다 DozerDB는 LPG(Labeled Property Graph) 방식을 쓴다.\n노드(동그라미)에 이름과 속성을 붙인다: 순매출 {definition: \u0026quot;총매출-반품-에누리\u0026quot;} 노드 사이에 화살표를 긋고, 그 화살표에도 속성을 단다: -[MANUFACTURES {since: \u0026quot;2024-01-01\u0026quot;}]-\u0026gt; 핵심은 화살표 자체에 \u0026ldquo;언제부터\u0026rdquo;, \u0026ldquo;신뢰도 얼마\u0026rdquo; 같은 정보를 달 수 있다는 점이다. 이전 글\r에서 MANUFACTURES, STOCKS 관계를 만들 때 이걸 활용했다.\nSKOS를 포함한 웹 표준들은 완전히 다른 체계를 쓴다. RDF(Resource Description Framework) -모든 정보를 세 단어짜리 문장으로 쪼갠다.\n순매출 → broader → 매출 (순매출의 상위 개념은 매출이다) 순매출 → prefLabel → \u0026quot;순매출\u0026quot;@ko (한국어 이름은 \u0026ldquo;순매출\u0026quot;이다) 주술목(주어-서술어-목적어), 이 세 단어가 하나의 단위다. 트리플(triple) 이라고 부른다.\n여기서 갈린다. LPG는 관계에 속성을 자유롭게 붙일 수 있지만, RDF는 트리플이 원자 단위라서 관계 자체에 속성을 직접 달 수 없다. 대신 URI 기반이라 전 세계 어디서든 같은 개념을 같은 주소로 가리킬 수 있다. 시스템 간 데이터 교환에는 RDF가 압도적이다.\nLPG의 표현력은 내부에서 쓰고, RDF의 호환성은 외부 연동에 쓴다. 둘 다 필요했다.\nOWL은 과하고, RDFS는 부족하고 RDF 세계에도 표준이 여러 개다.\nOWL(Web Ontology Language) 은 가장 강력하다. 클래스 상속, 제약 조건, 자동 추론까지 지원한다. 법률 문서에 비유할 수 있다 -모든 조항과 예외를 정밀하게 기술할 수 있는 대신 추론 엔진(Reasoner)을 별도로 띄워야 하고 학습 곡선이 가파르다. FIBO가 OWL인 이유도 금융 규제의 복잡성 때문이다.\nDataNexus가 하려는 건 추론이 아니다. \u0026ldquo;객단가가 뭔지, 어떤 테이블의 어떤 컬럼에 있는지\u0026quot;를 NL2SQL 엔진에 알려주는 맥락 제공이다. OWL은 과했다.\nRDFS(RDF Schema) 는 반대로 너무 가볍다. subClassOf 정도는 되는데 동의어나 용어 정의를 달 표준 속성이 없다.\nSKOS(Simple Knowledge Organization System) 가 그 사이에 있었다. 이름부터 \u0026ldquo;단순한 지식 조직 체계\u0026quot;다. 도서관 분류 체계나 시소러스(Thesaurus: 동의어·유의어·상하위어를 매핑해둔 용어 관계 사전)를 표현하려고 만든 W3C 표준인데, DataNexus가 하는 일이 결국 비즈니스 용어 사전 관리다. 이걸 쓰는 게 무리는 아니었다.\nSKOS 개념이 DataNexus 구조에 어떻게 대응되는지 정리하면:\nSKOS DataNexus (DataHub + DozerDB) 쉽게 말하면 skos:Concept Glossary Term / Entity 노드 용어 하나 skos:broader IsA 관계 (상위 개념) \u0026ldquo;객단가는 매출지표의 일종\u0026rdquo; skos:narrower IsA 역방향 (하위 개념) \u0026ldquo;매출지표의 하위에 객단가\u0026rdquo; skos:related RelatedTo 계열 \u0026ldquo;관련 있는 용어\u0026rdquo; * skos:prefLabel Term name (한국어 대표명) 공식 이름 skos:altLabel 동의어 (영문, 약어) \u0026ldquo;객단가\u0026rdquo; = \u0026ldquo;ATV\u0026rdquo;, \u0026ldquo;Average Transaction Value\u0026rdquo; skos:definition Term definition 용어 뜻풀이 skos:ConceptScheme 도메인별 용어 묶음 \u0026ldquo;유통 용어집\u0026rdquo;, \u0026ldquo;재무 용어집\u0026rdquo; * 주의할 점이 있다. skos:related는 양방향이다. \u0026ldquo;A related B\u0026quot;이면 자동으로 \u0026ldquo;B related A\u0026quot;도 성립한다. DozerDB의 SELLS나 SUPPLIED_BY 같은 관계는 방향이 있다. A매장이 B상품을 판매한다고 B상품이 A매장을 판매하진 않는다. 이 방향 정보는 SKOS로 내보낼 때 손실된다. 뒤에서 다시 다룬다.\nDozerDB 위에 SKOS를 얹다 내가 정한 원칙은 하나였다. 기존 그래프를 건드리지 않는다.\nDozerDB에 이미 MANUFACTURES, STOCKS, CALCULATED_FROM 같은 관계가 들어가 있고 쿼리가 돌고 있었다. 표준 맞추겠다고 이걸 뒤집으면 다른 프로젝트에서 여러 번 본 삽질을 반복하는 거다.\n기존 노드 위에 SKOS 메타데이터를 오버레이 했다. 투명 필름 한 장 덮는 느낌이다.\n// 기존 Entity 노드에 SKOSConcept 라벨과 SKOS 속성을 추가 MATCH (net:Entity {name: \u0026#39;순매출\u0026#39;}) SET net:SKOSConcept SET net.skos_prefLabel = \u0026#39;순매출\u0026#39; SET net.skos_altLabel = [\u0026#39;Net Sales\u0026#39;, \u0026#39;순매출액\u0026#39;] SET net.skos_definition = \u0026#39;총매출에서 반품과 에누리를 차감한 금액\u0026#39; SET net.skos_inScheme = \u0026#39;finance-terms\u0026#39; 유통 도메인도 똑같다.\n// 유통 도메인 용어 예시 MATCH (atv:Entity {name: \u0026#39;객단가\u0026#39;}) SET atv:SKOSConcept SET atv.skos_prefLabel = \u0026#39;객단가\u0026#39; SET atv.skos_altLabel = [\u0026#39;ATV\u0026#39;, \u0026#39;Average Transaction Value\u0026#39;, \u0026#39;객단\u0026#39;] SET atv.skos_definition = \u0026#39;총매출액을 구매 고객수로 나눈 값\u0026#39; SET atv.skos_inScheme = \u0026#39;retail-terms\u0026#39; 기존 Entity 노드는 그대로다. SKOSConcept이라는 라벨과 skos_ 접두사 속성이 위에 붙을 뿐. 기존 Cypher 쿼리에는 영향이 없다.\nbroader/narrower 관계는 두 가지 방법이 있었다. BROADER, NARROWER 엣지를 IsA와 나란히 미리 만들어두거나, 기존 IsA 관계를 Export 시점에 skos:broader로 바꿔 출력하거나.\n후자를 택했다. 엣지를 이중으로 만들면 IsA가 바뀔 때마다 BROADER도 동기화해야 한다. 동기화가 어긋나면 데이터가 꼬인다. 원천(Source of Truth)은 하나여야 한다. Export 시점에 한 번 변환하는 게 단순하고 안전하다.\n가져오기와 내보내기 오버레이를 넣고 나니까 전에 안 되던 두 가지가 가능해졌다.\n가져오기 -FIBO에서 금융 용어를, GS1 GPC에서 상품 분류 체계를 DataNexus로 끌어오는 경우다. FIBO는 원래 OWL로 배포되지만 SKOS로 변환된 파생 버전도 있다. GPC도 마찬가지로 SKOS 매핑이 가능하다. \u0026ldquo;의류 → 여성복 → 원피스\u0026rdquo; 같은 상품 계층을 그대로 가져와서 유통 고객사 온톨로지의 뼈대로 쓸 수 있다. OWL의 복잡한 제약 조건은 빠지지만, DataNexus에 필요한 건 용어 이름·정의·상하위 관계뿐이다. SKOS 서브셋으로 충분하다.\n내보내기 -DataNexus 용어를 고객사 시스템으로 보내는 경우. DozerDB 그래프에서 특정 도메인(예: retail-terms)의 노드와 관계를 꺼내서 SKOS Turtle 포맷으로 변환한다.\n@prefix skos: \u0026lt;http://www.w3.org/2004/02/skos/core#\u0026gt; . @prefix dnx: \u0026lt;http://datanexus.ai/ontology/\u0026gt; . dnx:atv a skos:Concept ; skos:prefLabel \u0026#34;객단가\u0026#34;@ko ; skos:altLabel \u0026#34;ATV\u0026#34;@en, \u0026#34;Average Transaction Value\u0026#34;@en ; skos:definition \u0026#34;총매출액을 구매 고객수로 나눈 값\u0026#34;@ko ; skos:broader dnx:sales-metrics ; skos:inScheme dnx:retail-terms . dnx:sales-metrics a skos:Concept ; skos:prefLabel \u0026#34;매출지표\u0026#34;@ko ; skos:narrower dnx:atv, dnx:net-sales, dnx:upt ; skos:inScheme dnx:retail-terms . 유통 현장에서 \u0026ldquo;객단가\u0026quot;라고 부르는 걸 어떤 시스템에서는 \u0026ldquo;ATV\u0026quot;로, 어떤 곳에서는 \u0026ldquo;평균구매단가\u0026quot;로 부른다. altLabel에 이 별칭들을 다 넣어두면 NL2SQL 엔진이 어떤 이름으로 질문이 들어와도 같은 테이블을 찾을 수 있다. 이 파일을 Collibra든 TopBraid이든 SKOS를 지원하는 어떤 시스템에든 넣을 수 있다.\n가져오기/내보내기가 되면 앞서 얘기한 문제들이 풀린다. 유통 그룹에서 백화점은 \u0026ldquo;매출\u0026quot;을 점포별 POS 합산으로, 온라인몰은 결제 완료 기준으로, 마트는 반품 차감 후 기준으로 각각 정의하고 있다고 하자. 각 관계사가 DataNexus에 자기 용어를 SKOS로 내보내면, 그룹 본사에서 이걸 받아 매핑 테이블을 만들 수 있다. \u0026ldquo;백화점의 매출 = 온라인몰의 확정매출 = 마트의 순매출\u0026quot;이라는 관계가 표준 포맷으로 잡히는 거다. 금융 고객사라면 감독 기관에 용어 정의와 데이터 계보를 보고해야 할 때 SKOS Turtle 파일을 그대로 제출하거나, 기관이 요구하는 포맷으로 변환할 수 있다. 표준이 없으면 이런 건 전부 수작업이다.\nSchema.org 같은 RDFS/OWL 기반 표준은 이 SKOS 레이어 범위 밖이다. 필요해지면 별도 변환기를 만들면 되지만 당장 우선순위는 아니다.\n남은 한계 SKOS로 안 되는 것들이 있다는 건 처음부터 알고 있었다.\nSKOS에는 레이블 자체에 메타데이터를 붙이는 확장(SKOS-XL)이 있다. \u0026ldquo;순매출\u0026quot;이라는 이름이 언제 등록됐는지, 누가 승인했는지를 기록할 수 있다. 다국어 레이블 관리가 복잡해지면 꺼내 써야 할 수도 있는데, 아직은 안 넣었다.\nOWL 수준의 추론도 SKOS 범위 밖이다. \u0026ldquo;A가 B의 하위이고, B가 C의 하위이면, A는 C의 하위다\u0026rdquo; 같은 자동 추론. 온톨로지 규모가 작을 땐 없어도 되는데, 수천 개 용어가 쌓이면 얘기가 달라질 수 있다.\n가장 아쉬운 건 커스텀 관계 Export다. DozerDB의 SELLS, STOCKS, SUPPLIED_BY 같은 유통 도메인 특화 관계는 SKOS 표준에 대응하는 게 없다. \u0026ldquo;A매장이 B상품을 판매한다\u0026quot;는 방향이 있는 관계인데, skos:related로 뭉뚱그리면 방향과 의미가 사라진다. dnx:sells 같은 커스텀 네임스페이스로 확장하면 정보는 보존되는데, 받는 쪽이 이 커스텀 관계를 이해할 수 있어야 한다. 정보 손실 vs 호환성 -트레이드오프다.\n커스텀 관계를 내보내는 구체적인 방법 skos:related로 뭉뚱그리면 의미가 사라진다고 했다. 그래서 어떻게 하느냐.\nDataNexus 전용 네임스페이스를 정의한다.\n@prefix dnx: \u0026lt;http://datanexus.ai/ontology/relation/\u0026gt; . dnx:atv-store a skos:Concept ; skos:prefLabel \u0026#34;객단가-매장 관계\u0026#34;@ko ; dnx:relationshipType \u0026#34;SoldBy\u0026#34; ; dnx:direction \u0026#34;outgoing\u0026#34; ; dnx:confidence 0.95 ; dnx:validFrom \u0026#34;2024-01-01\u0026#34; . dnx:relationshipType, dnx:direction, dnx:confidence 같은 커스텀 속성으로 DozerDB의 SELLS 관계가 가진 방향성과 메타데이터를 보존한다. 받는 쪽 시스템이 dnx: 네임스페이스를 이해하면 정보 손실 없이 복원할 수 있고, 이해 못 하면 skos:related로 폴백한다. 정보가 사라지는 게 아니라 읽을 수 있는 시스템에서만 보이는 거다.\n현실적으로는 이렇게 운영한다.\nExport 대상 방식 정보 보존율 SKOS 네이티브 시스템 (Collibra, TopBraid) skos: 표준 속성만 포함 ~80% (방향, 속성 손실) DataNexus 간 교환 (그룹사 ↔ 그룹사) dnx: 커스텀 네임스페이스 포함 ~95% (거의 완전 보존) 규제 보고용 skos: + skos:note에 커스텀 관계 텍스트 기록 ~85% (사람이 읽을 수 있는 수준) DataHub 쪽에서는 Export 시점에 미매핑 속성을 처리하는 규칙도 정해 뒀다.\nDozerDB 속성 SKOS Export 시 처리 confidence dnx:confidence (커스텀) 또는 skos:note에 텍스트로 기록 since / valid_until dnx:validFrom / dnx:validUntil 또는 skos:historyNote cardinality dnx:cardinality (커스텀 전용, SKOS에 대응 없음) operator (CALCULATED_FROM) dnx:calculationOperator 깔끔하지는 않다. dnx: 네임스페이스는 DataNexus 생태계 안에서만 의미가 있고, 외부 시스템이 이걸 해석하리라는 보장은 없다. 표준의 빈 곳을 커스텀 확장으로 메꾸면 결국 새로운 비표준을 만드는 셈이다. 제대로 하려면 SKOS-XL이나 별도의 Application Profile을 정의해야 하는데, 지금은 과하다. 고객사에서 실제로 요구하면 그때 넣을 생각이다.\n대략 80%는 SKOS 표준으로 커버하고, 나머지 20%는 DozerDB 커스텀 속성으로 채운다. 깨끗하진 않지만, SKOS에 안 맞는 걸 억지로 구겨넣는 것보다는 낫다.\nDataNexus를 설계하고 구축하는 과정을 기록합니다. GitHub\r| LinkedIn\r","permalink":"https://datanexus-kr.github.io/posts/datanexus/004-skos-compatibility-layer/","summary":"DataNexus 온톨로지를 외부와 연결하기 위해 SKOS를 선택한 이유. LPG와 RDF, 두 그래프 모델을 잇는 호환 레이어 설계.","title":"4. SKOS 호환 레이어를 왜 넣었는가"},{"content":"\u0026ldquo;스타스키마 안 해도 되나요?\u0026rdquo; 클라우드 DW 전환 프로젝트를 하면 꼭 나오는 질문이다.\n온프레미스에서 수년간 운영하던 DW를 BigQuery나 Azure Synapse로 옮기는 자리. 누군가가 묻는다. \u0026ldquo;거기는 Columnar Storage(컬럼기반 저장소)라 조인 비용이 다르다는데, 그러면 스타스키마 안 해도 되는 거 아닌가요?\u0026rdquo;\n상황에 따라 다르다. 그런데 이 \u0026ldquo;상황에 따라 다르다\u0026quot;가 구체적으로 뭘 따져야 하는 건지를 아는 사람은 많지 않다.\n온프레미스 시절의 공식 Kimball의 Dimensional(Star-Schema) 모델링이 사실상 표준이던 시절이 있었다.\n이유는 단순했다. 디스크 I/O가 비쌌고, 조인은 더 비쌌다. Row 기반 스토리지에서 테이블 10개를 조인하면 쿼리 응답이 분 단위로 넘어갔다. 그래서 미리 조인해 놓는 게 합리적이었다.\n모델링 의사결정이 곧 성능 의사결정이었다. 어디까지 비정규화할 것인가, 집계 테이블을 몇 단계로 쌓을 것인가, 파티션 키를 뭘로 잡을 것인가. 이 결정들이 쿼리 응답 시간을 초 단위에서 분 단위로 갈랐다.\nKimball의 방법론은 이 제약 안에서 비즈니스 가독성과 쿼리 성능을 동시에 잡으려는 시도였다. 부서마다 제각각이던 차원 테이블을 전사 공통 기준으로 통일(Conformed Dimension)해서 일관성을 보장하고, Bus Matrix로 전사 통합을 설계했다. 방법론 자체의 완성도는 지금 봐도 높다.\n문제는 이 방법론이 만들어진 전제가 바뀌었다는 거다.\n클라우드가 바꾼 전제들 클라우드 DW로 오면서 물리적 제약이 근본적으로 달라졌다.\nColumnar Storage. BigQuery, Redshift, Synapse 모두 컬럼 기반이다. SELECT에서 필요한 컬럼만 읽는다. 컬럼이 수백 개인 테이블에서 서너 개만 조회하면 그것만 스캔한다. Row 기반에서는 전부 읽어야 했다.\nCompute/Storage 분리. 스토리지가 싸졌다. 중복 저장해도 비용 부담이 작다. 온프레미스에서 디스크 용량 아끼려고 정규화를 고민하던 것과는 상황이 다르다.\nMPP 아키텍처. 대규모 병렬 처리가 기본이다. 조인 비용이 온프레미스 RDBMS 대비 상대적으로 낮아졌다. 물론 공짜는 아니다 - 셔플이 발생하면 여전히 느리다. 하지만 \u0026ldquo;조인은 무조건 피하라\u0026quot;는 원칙이 더 이상 절대적이지 않다.\nELT 패러다임. 원본을 먼저 적재하고, DW 안에서 변환한다. 변환 로직을 DW 엔진의 컴퓨팅 파워로 처리한다. ETL 시절처럼 변환 서버를 별도로 두고 \u0026ldquo;다 만들어서 넣는\u0026rdquo; 구조가 아니다.\n반정형 데이터 지원. JSON, ARRAY, STRUCT를 네이티브로 다룬다. 전통적인 관계형 모델로 풀기 어려웠던 유연한 스키마를 DW 안에서 직접 처리할 수 있다.\n이 변화들이 기존 모델링 원칙의 근거를 흔든다. 하지만 근거가 약해진 것과 원칙이 틀린 것은 다른 이야기다.\n세 가지 선택지 클라우드 DW에서 자주 비교되는 세 가지 접근법이 있다.\n1. Kimball Dimensional 모델링 스타스키마, 팩트와 디멘션, Conformed Dimension. 여전히 가장 널리 쓰인다.\n클라우드에서도 유효한 이유가 있다. 비즈니스 사용자가 이해하기 쉽다. \u0026ldquo;매출 팩트를 고객 디멘션으로 잘라본다\u0026quot;는 구조는 BI 도구와도 궁합이 좋다. Power BI, Tableau, Looker 전부 이 구조를 전제로 최적화되어 있다.\n달라진 것도 있다. 집계 테이블을 미리 쌓아둘 필요가 줄었다. 컬럼나 스토리지에서 원본 팩트를 바로 집계해도 충분히 빠르다. SCD Type 2 같은 이력 관리도 스토리지 부담이 작아져서 더 적극적으로 쓸 수 있게 됐다.\n약점은 유연성이다. 스키마가 바뀌면 팩트/디멘션 구조를 재설계해야 한다. 애자일하게 빠르게 바뀌는 요구사항에 대응하기가 구조적으로 어렵다.\n2. Data Vault 2.0 Hub(비즈니스 키), Satellite(속성), Link(관계). 원본 데이터를 있는 그대로 이력과 함께 저장하는 데 초점을 맞춘 방법론이다.\n강점은 명확하다. 감사 추적성(auditability). 원본이 언제 어떤 값이었는지를 완전하게 보존한다. 소스 시스템이 추가되거나 스키마가 변경되어도 기존 구조에 영향을 주지 않는다. 병렬 적재가 가능해서 ELT 패러다임과도 잘 맞는다.\n현실적인 걸림돌이 있다. 직접 쿼리하기가 까다롭다. Hub-Satellite 조인을 여러 번 거쳐야 하나의 비즈니스 엔터티가 나온다. 그래서 결국 프레젠테이션 레이어(보통 스타스키마)를 따로 만들어야 한다. 모델링 레이어가 하나 더 생기는 셈이다. 팀이 Data Vault 경험이 없으면 학습 곡선도 가파르다.\n금융, 의료 같은 규제 산업에서 감사 추적이 필수일 때, 또는 소스 시스템이 수시로 추가되는 환경에서 강하다.\n3. One Big Table (OBT) 팩트와 디멘션을 전부 하나의 와이드 테이블에 합친다. 극단적인 비정규화다.\n클라우드에서 이게 통하는 이유가 있다. 컬럼나 스토리지에서는 200개 컬럼이 있어도 쿼리에 쓰는 5개만 읽으니까 성능 저하가 크지 않다. 조인이 없으니 쿼리가 단순하다. 개발 속도도 빠르다. dbt 같은 도구에서 한 번의 SELECT로 만들면 끝이다.\n대가가 있다. 데이터 정합성을 보장할 구조적 장치가 없다. 고객 주소가 바뀌면 모든 OBT를 다시 빌드해야 한다. 같은 디멘션 속성이 여러 OBT에 중복되면 어디가 맞는 건지 알 수 없다. 데이터가 적고 도메인이 단순할 때는 빠르지만, 규모가 커지면 관리가 급격히 어려워진다.\n프로토타이핑이나 단일 도메인의 분석용 마트로는 좋다. 전사 DW의 기반 구조로 쓰기에는 위험하다.\n실무에서의 판단 기준 \u0026ldquo;뭐가 최고냐\u0026quot;는 의미 없는 질문이다. 아래 기준으로 따져야 한다.\n팀 역량. Data Vault를 제대로 하려면 방법론을 아는 사람이 팀에 있어야 한다. 없으면 Kimball이 현실적이다. OBT는 진입 장벽이 낮지만 규모가 커지면 경험 있는 모델러가 더 절실해진다.\n데이터 복잡도. 소스 시스템이 3개인가 30개인가. 도메인이 하나인가 여러 개인가. 복잡도가 높을수록 Kimball의 Conformed Dimension이나 Data Vault의 Hub 구조가 필요하다.\n변경 빈도. 요구사항이 자주 바뀌는 환경이면 Data Vault가 유리하다. 안정적인 환경이면 Kimball로 충분하다.\n규제 요건. 감사 추적이 법적으로 필요하면 Data Vault를 고려해야 한다. 아니라면 오버엔지니어링이 될 수 있다.\n쿼리 패턴. BI 대시보드 중심이면 Kimball 구조가 BI 도구와 궁합이 좋다. Ad-hoc 분석이 많으면 OBT의 단순함이 장점이 된다.\n실무에서 자주 보는 패턴은 레이어드 접근이다.\nRaw (원본 적재) → Staging (정제) → Integration (통합 모델) → Mart (분석용) Integration 레이어를 Data Vault로 설계하고, Mart를 스타스키마로 제공하는 조합이 대표적이다. Data Vault는 허브-링크-위성 구조로 이력 추적과 유연한 확장에 특화되어 있고, 스타스키마는 중심 팩트 테이블 주위에 차원 테이블을 별 모양으로 배치한 분석 최적화 구조다. 또는 Integration을 중복 없이 정규화한 관계형 모델(3NF)로 잡고 Mart를 Kimball 방식으로 가는 전통적인 구조도 있다. 어느 쪽이든 핵심은 레이어를 나누는 것이다.\n한 레이어에서 원본 보존과 분석 최적화를 동시에 해결하려는 순간 복잡도가 폭발한다.\n모델링은 여전히 중요하다 클라우드로 오면서 바뀐 건 \u0026ldquo;왜 이렇게 모델링하는가\u0026quot;의 판단 기준이지, \u0026ldquo;모델링이 필요한가\u0026quot;의 여부는 아니다.\n스토리지가 싸졌으니 비정규화를 더 적극적으로 해도 된다. 조인 비용이 줄었으니 집계 테이블을 덜 만들어도 된다. 하지만 비즈니스 용어의 일관성, 데이터 계보(lineage), 도메인 간 통합 - 이런 문제는 인프라가 바뀐다고 사라지지 않는다.\n오히려 클라우드에서 모델링 없이 시작한 팀이 나중에 더 고생한다. OBT로 빠르게 만들어서 처음엔 잘 돌아간다. 1년 지나면 같은 지표의 정의가 테이블마다 다르고, 어디가 원본인지 아무도 모른다. 기술 부채는 조용히 쌓인다.\n도구와 인프라가 좋아진 만큼 모델링의 무게중심이 \u0026ldquo;성능 최적화\u0026quot;에서 \u0026ldquo;의미 체계의 일관성\u0026quot;으로 옮겨가고 있다. DataNexus에서 온톨로지로 풀려는 문제와 같은 맥락이기도 하다. 기계가 읽을 수 있는 비즈니스 맥락이 없으면, 아무리 좋은 인프라 위에서도 데이터는 의미를 잃는다.\n","permalink":"https://datanexus-kr.github.io/guides/dw-modeling/001-cloud-era-dw-modeling/","summary":"Synapse, BigQuery, Redshift로 오면서 DW 모델링 관점이 어떻게 달라졌는지. Kimball, Data Vault, One Big Table - 실무에서의 판단 기준.","title":"1. 클라우드 DW에서 Kimball은 여전히 유효한가"},{"content":"\rGoogle Colab에서 실습하기\r왜 Glossary를 온톨로지로 쓰려 했나 DataNexus의 핵심 아이디어는 단순하다. 비즈니스 용어 사이의 관계를 그래프로 정의해두면, NL2SQL 엔진이 그 그래프를 참조해서 자연어를 SQL로 바꿀 수 있다. 이 그래프가 온톨로지다.\n온톨로지라고 하면 학술 논문에나 나올 것 같은데, 실체는 별거 없다. \u0026ldquo;순매출은 매출의 한 종류다(IsA)\u0026rdquo;, \u0026ldquo;매출은 총매출, 반품, 에누리를 포함한다(HasA)\u0026rdquo;. 사람 머릿속에 있는 업무 지식을 기계가 읽을 수 있게 옮긴 것뿐이다.\n이걸 어디에 저장할지가 고민이었다. 온톨로지 전용 시스템을 하나 더 띄우면 관리 포인트가 늘어난다. 이미 DataHub를 메타데이터 플랫폼으로 쓰고 있었고, 거기 Business Glossary 가 딸려 있었다. 용어 등록, 관계 설정 다 된다. 이전 글\r에서 Glossary의 관계 4종(IsA, HasA, RelatedTo, Values)이면 비즈니스 용어 계층구조를 충분히 표현할 수 있다고 봤었다.\n시스템 하나를 줄일 수 있다는 게 매력적이었다. GraphQL API로 프로그래밍 접근도 되고, 용어가 변경되면 Kafka MCL(Metadata Change Log) 이벤트가 자동으로 나간다. 나쁘지 않은 출발점이었다.\nGlossary에 용어를 넣기 시작했다 DataHub 세팅하고 제일 먼저 한 게 Glossary Term 등록이었다. \u0026ldquo;순매출 IsA 매출\u0026rdquo;, \u0026ldquo;매출 HasA 총매출, 반품, 에누리\u0026rdquo;. 이런 식으로 용어를 넣고 관계를 걸었다.\n기본 계층구조는 깔끔하게 들어갔다. 매출 → 총매출, 순매출 → 실매출.\n문제는 그 다음이었다.\n관계 4종의 한계: \u0026ldquo;공장과 제품\u0026rdquo; 실제 업무 데이터를 모델링하면서 벽에 부딪혔다.\n\u0026ldquo;A 공장에서 생산된 B 제품\u0026quot;과 \u0026ldquo;A 공장에 재고가 있는 B 제품\u0026rdquo;. 둘 다 공장과 제품 사이의 관계인데, 하나는 생산(Manufactures), 하나는 재고(Stocks)다. 의미가 완전히 다르다.\nDataHub Glossary에서 이걸 표현하면? 둘 다 RelatedTo가 된다. \u0026ldquo;공장 RelatedTo 제품\u0026quot;이 두 개 생기는데, 어느 게 생산이고 어느 게 재고인지 구분할 방법이 없다.\n이게 왜 치명적이냐면-DataNexus의 NL2SQL 엔진이 온톨로지를 보고 SQL을 만들기 때문이다. \u0026ldquo;A 공장에서 생산된 제품 목록 보여줘\u0026quot;라는 질문이 들어오면, 엔진은 공장-제품 관계를 찾아서 해당 테이블과 JOIN 경로를 결정한다.\n사용자 질문: \u0026ldquo;A 공장에서 생산된 제품은?\u0026quot;\n온톨로지 조회: 공장 → RelatedTo → 제품 (생산? 재고? 알 수 없음)\n→ LLM이 production 테이블 대신 inventory 테이블을 JOIN할 수 있음 → 잘못된 결과 반환\n관계 유형이 RelatedTo 하나뿐이니 엔진한테는 판단 근거가 없다. 잘못된 JOIN을 타면 사용자에게 엉뚱한 데이터가 나간다.\n확장하려면 재배포가 필요하다 DataHub에서 관계를 세분화하면 되지 않느냐. 안 된다.\nPDL(Persona Data Language)로 새 Aspect를 정의하고, @Relationship 어노테이션으로 관계 유형을 선언하고, DataHub를 빌드해서 재배포해야 한다. 관계 유형 하나 추가할 때마다 이 사이클을 돌려야 한다.\n비즈니스 모델링을 하다 보면 관계는 계속 늘어난다. \u0026ldquo;공급(Supplies)\u0026rdquo;, \u0026ldquo;검수(Inspects)\u0026rdquo;, \u0026ldquo;반품(Returns)\u0026rdquo;\u0026hellip; 업무 맥락에 따라 수십 가지가 필요해지는데, 하나마다 코드 고치고 재배포하는 건 현실적이지 않다.\n파고 들어가니 더 나왔다 관계 유형만 문제가 아니었다.\n동의어 충돌 \u0026ldquo;순매출\u0026quot;과 \u0026ldquo;실매출\u0026quot;을 동의어로 등록했다. 같은 개념인데 이름만 다른 경우다. 그런데 두 용어 모두 \u0026ldquo;Net Sales\u0026quot;라는 영문 동의어를 갖고 있었다. 하나의 영문명에 한글 용어 두 개가 매핑된 상황-DataHub는 이걸 그냥 넘긴다. 경고도 없다.\nNL2SQL에서 동의어 매핑이 꼬이면 엔진이 엉뚱한 용어를 참조한다. 용어가 수백 개를 넘어가면 이런 충돌을 사람 눈으로 잡을 수 없다. 커스텀 검증 로직을 따로 짜야 한다는 뜻이다.\n시각화 DataHub UI는 데이터 계보(Lineage) 탐색에 맞춰져 있다. 테이블 A → 테이블 B로 데이터가 흐르는 방향성 있는 트리.\n온톨로지는 구조가 다르다. 노드 수십~수백 개가 다대다로 엮인 그물망이다. \u0026ldquo;제품\u0026quot;이 \u0026ldquo;공장\u0026rdquo;, \u0026ldquo;창고\u0026rdquo;, \u0026ldquo;거래처\u0026rdquo;, \u0026ldquo;카테고리\u0026quot;와 전부 다른 관계로 연결되어 있고, 그 노드들끼리 또 서로 물려 있다. DataHub에는 이런 그래프를 탐색할 화면 자체가 없다.\n만들어 놓고 전체 그림을 못 보면 관리가 안 된다.\n관계에 속성을 붙일 수 없다 이게 제일 문제였다.\nDataHub Glossary에서 \u0026ldquo;A RelatedTo B\u0026quot;를 설정하면 그 관계에 아무것도 더 달 수 없다. 실무에서는 관계 자체에 정보가 필요한 경우가 많다.\n신뢰도 가 대표적이다. 자동 추출된 관계는 0.7, 전문가가 직접 정의한 관계는 0.95 -이 차이를 NL2SQL 엔진이 알아야 한다. 유효 기간 도 마찬가지다. 조직 개편으로 부서-제품 매핑이 바뀌면 그 관계가 언제부터 언제까지 유효했는지 추적해야 한다. 이게 없으면 과거 조직 구조로 현재 데이터를 조회하게 되고, 리포트 수치가 안 맞는 전형적인 원인이 된다. 카디널리티 는 JOIN 전략에 직접 영향을 준다.\n정리: 되는 것과 안 되는 것 되는 것 안 되는 것 용어 정의 (name, definition) 세분화된 관계 유형 (MANUFACTURES, STOCKS 등) 동의어 등록 (커스텀 필드) 동의어 중복/충돌 자동 감지 4종 관계 (IsA, HasA, RelatedTo, Values) 관계에 속성 부여 (신뢰도, 유효 기간) GraphQL API로 프로그래밍 접근 복잡한 그래프 탐색 UI Kafka MCL 이벤트 스트림 재배포 없는 실시간 관계 유형 확장 용어 사전으로는 쓸 만하다. 온톨로지 저장소로 쓰기엔 모자랐다.\n역할을 나눴다: DataHub + DozerDB Glossary를 통째로 버리는 건 답이 아니었다. 용어 정의의 원천(Source of Truth)으로 DataHub를 대체할 게 없다. GraphQL API, Kafka MCL 이벤트-이 인프라를 다른 도구에서 바닥부터 만드는 건 시간 낭비다.\n각자 잘하는 걸 맡겼다.\nDataHub Glossary → 용어 정의와 기본 관계의 원천 (Source of Truth) DozerDB → 세분화된 관계, 속성 달린 엣지, 그래프 추론 담당 DozerDB를 고른 건 Cypher 쿼리를 쓸 수 있어서다. 관계(엣지)에 속성을 자유롭게 붙일 수 있고, 관계 유형을 추가할 때 스키마 변경이나 재배포가 필요 없다.\n동기화 흐름은 단순하다. DataHub에서 Glossary Term이 바뀌면 Kafka MCL 이벤트가 나간다. 이벤트를 구독하는 Consumer가 DozerDB 온톨로지 그래프에 반영한다. 이름이나 정의 같은 기본 정보는 DataHub가 쥐고 있고, DozerDB는 그 위에 세분화된 관계와 속성을 얹는다.\nDozerDB에서의 관계 정의 아까 문제됐던 \u0026ldquo;공장-제품\u0026rdquo; 관계가 DozerDB에서는 이렇게 풀린다.\n// 엔티티 생성 (DataHub에서 동기화된 용어) CREATE (factory:Entity {name: \u0026#39;A공장\u0026#39;, type: \u0026#39;Factory\u0026#39;}) CREATE (product:Entity {name: \u0026#39;B제품\u0026#39;, type: \u0026#39;Product\u0026#39;}) // 생산 관계 — 시작 시점과 신뢰도를 속성으로 기록 CREATE (factory)-[:MANUFACTURES { since: \u0026#39;2024-01-01\u0026#39;, confidence: 0.95 }]-\u0026gt;(product) // 재고 관계 — 별도 엣지, 수량과 갱신 시점 CREATE (factory)-[:STOCKS { quantity: 500, last_updated: \u0026#39;2026-02-01\u0026#39; }]-\u0026gt;(product) MANUFACTURES와 STOCKS가 별개의 관계 유형이다. \u0026ldquo;A 공장에서 생산된 제품\u0026quot;이라는 질문이 오면 엔진이 MANUFACTURES를 찾아서 production 테이블로 정확히 JOIN한다. RelatedTo 하나로 퉁치던 것과는 근본적으로 다르다.\n파생 지표도 그래프에 넣었다 파생 지표 정의를 Excel로 관리하면 원본 용어가 바뀔 때 파생 시트가 안 따라간다. \u0026ldquo;순매출 = 총매출 - 반품 - 에누리\u0026quot;에서 총매출 정의가 바뀌었는데 순매출 쪽은 그대로-이런 불일치가 리포트까지 올라간다.\n이번에는 CALCULATED_FROM 관계로 계산식 자체를 그래프에 넣었다.\n// 순매출의 계산 구조를 관계로 표현 MATCH (net:Entity {name: \u0026#39;순매출\u0026#39;}) MATCH (gross:Entity {name: \u0026#39;총매출\u0026#39;}) MATCH (returns:Entity {name: \u0026#39;반품\u0026#39;}) MATCH (discounts:Entity {name: \u0026#39;에누리\u0026#39;}) CREATE (net)-[:CALCULATED_FROM {operator: \u0026#39;subtract\u0026#39;}]-\u0026gt;(gross) CREATE (net)-[:CALCULATED_FROM {operator: \u0026#39;subtract\u0026#39;}]-\u0026gt;(returns) CREATE (net)-[:CALCULATED_FROM {operator: \u0026#39;subtract\u0026#39;}]-\u0026gt;(discounts) 계산식이 바뀌면 관계를 수정한다. 변경 이력은 그래프 DB가 추적한다. Excel 시트 어딘가에 묻혀서 누가 언제 고쳤는지 모르는 것보다 낫다.\nSource of Truth가 깨진 건 아닌가 여기서 한 가지 짚고 넘어갈 게 있다.\n1편에서 메타데이터 카탈로그를 \u0026ldquo;온톨로지의 원천(Source of Truth)\u0026ldquo;이라고 썼다. 2편에서도 DataHub Glossary의 관계 4종이면 충분하다고 봤다. 그런데 3편에 와서 DozerDB를 추가했다. \u0026ldquo;그러면 Source of Truth가 두 개가 된 거 아닌가?\u0026rdquo; 당연한 질문이다.\nSoT의 대상이 달라진 거다.\nSoT라는 개념은 \u0026ldquo;모든 것을 하나의 시스템에 넣는다\u0026quot;가 아니다. \u0026ldquo;특정 데이터 카테고리에 대해 어디가 최종 권위를 가지느냐\u0026quot;를 정하는 거다. DataHub와 DozerDB는 서로 다른 질문에 답한다.\n\u0026ldquo;순매출이 뭐야?\u0026rdquo; → DataHub가 답한다. 이름, 정의, 동의어, 소유 부서. 용어의 정체성에 관한 건 DataHub가 최종 권위다. \u0026ldquo;순매출이 어떤 테이블과 어떤 경로로 연결되어 있어?\u0026rdquo; → DozerDB가 답한다. CALCULATED_FROM, MANUFACTURES 같은 세분화된 관계, 신뢰도, 유효 기간. 용어 간 연결의 의미론은 DozerDB가 쥐고 있다. 둘 사이에 충돌이 생기면? DataHub가 이긴다. DozerDB의 노드 이름이나 정의가 DataHub Glossary와 다르면 DataHub 쪽이 정답이다. Kafka MCL 이벤트가 이 방향으로만 흐른다 - DataHub → DozerDB. 역방향 동기화는 없다.\n처음부터 이 구분을 명확히 했어야 한다. 1편에서 \u0026ldquo;온톨로지의 원천\u0026quot;이라고 쓸 때, 실제로는 \u0026ldquo;용어 정의의 원천\u0026quot;이라고 써야 했다. 용어 정의와 관계 의미론을 하나의 시스템이 전부 감당할 수 있을 거라는 초기 가정이 틀렸다. 3편에서 그게 드러난 거고, DataHub + DozerDB 이중 구조는 그 결과다.\nSoT가 깨진 게 아니다. 범위가 좁아진 거다.\n남은 문제: 표준 호환 DataHub Glossary 모델은 DataHub만의 구조다. 업계에는 FIBO(금융), Schema.org(범용) 같은 표준 온톨로지가 있다. 산업 표준을 가져오거나 DataNexus 온톨로지를 밖으로 내보내려면 표준 포맷을 지원해야 하는데, 지금 구조로는 DataNexus 안에서만 통하는 독자 체계다.\n외부와 교환이 안 되는 온톨로지는 결국 내부에서만 쓰다 끝난다.\n다음 글에서 SKOS 호환 레이어를 왜 넣었는지 다룬다.\rGoogle Colab에서 실습하기\rDataNexus를 설계하고 구축하는 과정을 기록합니다. GitHub\r| LinkedIn\r","permalink":"https://datanexus-kr.github.io/posts/datanexus/003-datahub-glossary-as-ontology/","summary":"DataHub의 Business Glossary를 온톨로지 저장소로 쓰려고 했다. 되는 것과 안 되는 것, 그리고 우회한 방법.","title":"3. DataHub Glossary를 온톨로지로 쓸 수 있을까"},{"content":"비교 후보군 후보가 너무 많았다.\n메타데이터 카탈로그만 해도 DataHub, Amundsen, Apache Atlas, OpenMetadata. 상용까지 포함하면 Collibra, Alation도 있다. NL2SQL 엔진, 문서 지식엔진, 그래프 DB까지 네 축을 채워야 하는데 조합이 기하급수적으로 불어났다.\n엑셀 시트에 비교표를 만들었다. 행이 후보 도구, 열이 평가 기준. 3주쯤 지나니까 탭이 7개로 늘어나 있었다. 선택지가 많으면 안 고르는 게 문제다. 하나를 고르면 나머지와의 조합이 바뀌고, 다시 처음부터 비교해야 한다.\n네 가지 컴포넌트, 각각의 요건 이전 글\r에서 DataNexus의 네 가지 컴포넌트를 정의했다. 메타데이터 카탈로그, NL2SQL 엔진, 문서 지식엔진, 그래프 DB.\n양보할 수 없는 공통 기준이 세 가지 있었다. 오픈소스일 것. 멀티테넌시를 지원하거나 구현 가능할 것 -그룹사별 데이터 격리는 필수다. 프로덕션 레디일 것 -커뮤니티 활성도, 릴리즈 주기, 문서화 수준까지 봤다.\n컴포넌트마다 추가 요건도 달랐다. 메타데이터 카탈로그는 Business Glossary에서 용어 간 관계를 정의할 수 있어야 하고, 변경 이벤트를 실시간으로 내보낼 수 있어야 했다. NL2SQL 엔진 쪽은 사용자별 컨텍스트 분리와 Row-level Security. 문서 지식엔진은 벡터 검색만으로 안 되고 그래프 검색까지 하이브리드로 돌려야 했다. 그래프 DB는 Multi-DB와 Cypher 쿼리 지원이 전제였다.\n이 기준을 들고 후보를 걸렀다.\n메타데이터 카탈로그 DataHub, OpenMetadata, Amundsen, Apache Atlas, 상용(Collibra/Alation). 다섯을 놓고 봤다.\n상용은 먼저 빠졌다. 라이선스 비용도 문제지만 이 프로젝트에서 필요한 건 카탈로그의 Glossary를 온톨로지 저장소처럼 쓰는 거다. 상용 Glossary가 강력하긴 한데 내부 데이터 모델에 접근해서 커스터마이징하는 데 한계가 있다.\nApache Atlas는 Hadoop 생태계에 묶여 있다. HBase, Solr, Kafka를 전부 띄워야 한다. 2016년 설계 그대로인데 클라우드 네이티브 환경에서 돌리기엔 무겁다. Amundsen은 검색 중심 카탈로그로는 괜찮은데 Glossary에서 용어 간 관계를 정의하는 기능이 빈약하다. 온톨로지 저장소로 쓸 수 없었다.\n끝까지 고민한 건 OpenMetadata다. 아키텍처가 깔끔하고 데이터 품질 측정이 내장돼 있어서 단독 카탈로그로는 훌륭했다. 문제는 Glossary 관계가 Parent-Child와 RelatedTerms 위주라는 점. 상속(IsA)과 포함(HasA)을 명확히 구분해야 하는 온톨로지 표현에는 모자랐다. 실시간 이벤트 동기화도 웹훅 방식이라 대규모 스트리밍에서 Kafka 네이티브 대비 신뢰성이 떨어졌다.\nDataHub로 갔다.\nGlossary 관계가 4종이다. IsA(상속), HasA(포함), Values(값 목록), RelatedTo(일반 연관). 이 네 가지면 비즈니스 용어 간 계층을 표현할 수 있다. \u0026ldquo;순매출 IsA 매출\u0026rdquo;, \u0026ldquo;매출 HasA 총매출, 반품, 에누리\u0026rdquo; 같은 식이다.\nGraphQL API도 한몫했다. 메타데이터를 프로그래밍 방식으로 읽고 쓸 수 있어야 NL2SQL 엔진의 RAG Store에 온톨로지를 자동 동기화하는데, GraphQL이면 필요한 필드만 골라서 가져온다.\n제일 크게 작용한 건 Kafka MCL 이벤트다. Metadata Change Log를 Kafka로 내보내는 구조인데, Glossary Term이 바뀌면 이벤트가 발행된다. 이걸 구독해서 그래프 DB 온톨로지를 실시간 동기화할 수 있다. 메타데이터 변경을 수동으로 반영하는 건 규모가 커질수록 반드시 누락이 생긴다. 양보할 수 없는 요건이었다.\nNL2SQL 엔진 처음에는 직접 만들까 생각했다. 대화형 BI 솔루션을 구축하면서 NL2SQL에 GPT와 Gemini를 붙이고, 프롬프트 엔지니어링을 최적화하고, 멀티에이전트 아키텍처까지 설계하는 데까지 갔었다.\n거기서 배운 게 두 가지다. DDL만으로는 LLM이 비즈니스 맥락을 이해할 수 없다는 것. 그리고 처음부터 만들면 사용자 인증, 쿼리 로깅, 데이터 필터링, 응답 스트리밍, 쿼리 학습까지 부수 기능이 한없이 불어난다는 것. 견적을 내보니 1개월 넘게 잡아먹힐 판이었다.\n그때 Vanna가 2.0으로 올라왔다.\n1.x는 단순했다. Python 클래스 하나 상속받아서 train(), ask() 호출하는 방식. 프로토타이핑엔 괜찮은데 프로덕션에 넣기엔 부족했다. 사용자별 컨텍스트 분리도 안 되고 보안 기능도 없었다.\n2.0은 다른 물건이다. Agent 기반 아키텍처로 바뀌면서 독립적인 구성 요소를 조합하는 방식이 됐고, 모든 컴포넌트에 사용자 ID가 자동 전파되는 User-Aware 구조가 들어갔다. Row-level Security가 프레임워크 수준에서 지원된다. 성공한 쿼리를 자동 학습하는 Tool Memory도 내장. 테이블이나 차트 같은 Rich UI Component를 실시간 전송하는 Streaming까지 갖췄다.\nUser-Aware와 Row-level Security가 가장 중요했다. DataNexus는 그룹사별로 데이터를 격리해야 하는데 NL2SQL 엔진 레벨에서 이걸 지원한다는 건 직접 구현할 코드가 대폭 줄어든다는 뜻이다.\nTool Memory도 컸다. NL2SQL 정확도를 올리는 가장 확실한 방법 중 하나가 성공 쿼리를 축적해서 유사 질문에 재활용하는 건데 이게 프레임워크에 내장돼 있다. 별도로 만들면 쿼리 저장, 유사도 매칭, 버전 관리까지 만져야 하는데 그 공수가 통째로 빠진다.\n문서 지식엔진 벡터 검색만으로는 부족하다.\n사업보고서나 내부 정책문서를 검색할 때 벡터 유사도만으로 청크를 가져오면 맥락이 끊긴다. \u0026ldquo;A사업부의 매출 인식 기준\u0026quot;을 찾고 싶은데 벡터 검색은 \u0026ldquo;매출\u0026quot;이 포함된 청크를 유사도 순으로 나열할 뿐이다. A사업부와 매출 인식 기준의 관계라든가, 기준이 언제 바뀌었는지 같은 그래프 구조 정보는 벡터에 안 담긴다.\nApeRAG는 세 가지 검색을 조합해서 이 문제를 푼다. 임베딩 기반 의미 검색인 Vector Search, 고유명사나 코드명처럼 문자열 자체가 중요한 Full-text Search, 문서에서 추출한 엔티티 간 관계를 그래프로 탐색하는 GraphRAG. 이 셋을 동시에 돌린다.\n이 하이브리드가 DataNexus와 특히 잘 맞는 이유가 있다. DataHub의 Glossary Term을 ApeRAG Entity Extraction의 Taxonomy로 주입하면 문서에서 추출된 엔티티가 자동으로 비즈니스 용어와 연결된다. Exact Match → Synonym Match → Fuzzy Match(임계값 0.85) → Context Match, 4단계 Entity Resolution을 거친다.\nMinerU 통합도 있다. 엔터프라이즈 문서에는 복잡한 테이블, 수식, 다단 레이아웃이 흔한데 일반 PDF 파서로는 테이블 행/열이 깨진다. 특히 사업보고서처럼 병합 셀이 많은 문서는 파싱 결과가 처참하다. MinerU는 문서 구조를 보존하면서 파싱하기 때문에 이 문제를 정면으로 해결한다.\n그래프 DB 가장 큰 변수는 Neo4j 라이선스였다.\nCommunity Edition과 Enterprise Edition의 결정적 차이는 Multi-DB다. Community는 인스턴스 하나에 그래프 하나. Enterprise는 같은 인스턴스 안에서 여러 데이터베이스를 만들 수 있다.\nDataNexus에서 Multi-DB는 필수다. 그룹사별로 온톨로지 그래프를 격리해야 한다. groupA_ontology_db, groupB_ontology_db처럼 테넌트별 데이터베이스를 분리하고 사용자 권한으로 접근을 제어하는 구조. Community 단일 DB에 전부 넣고 라벨로 구분하는 건 보안상 말이 안 된다.\n그렇다고 Neo4j Enterprise 라이선스를 살 수는 없다. 오픈소스 프로젝트의 원칙에 어긋난다.\nDozerDB가 이 딜레마를 풀었다. Neo4j Community Edition 위에 Enterprise 기능을 얹는 오픈소스 플러그인인데 Multi-DB를 지원한다. CREATE DATABASE로 테넌트별 그래프를 만들 수 있고, Cypher 쿼리도 그대로 쓴다.\nArangoDB도 봤다. 멀티모델(문서 + 그래프 + 키밸류)이 매력적인데 Cypher를 쓸 수 없다. 자체 쿼리 언어 AQL이 그래프 탐색에는 괜찮지만 Neo4j 생태계의 라이브러리나 도구를 못 쓰게 된다. 온톨로지를 Cypher로 질의하는 패턴과 레퍼런스가 압도적으로 많기 때문에 생태계 호환성을 택했다.\nDozerDB의 한계도 안다. Fabric -크로스 DB 쿼리 -은 아직 미지원이라 서로 다른 데이터베이스를 한 번의 Cypher로 질의하는 건 불가능하다. Phase 3 이후로 미뤘다. 당장은 단일 테넌트 내 질의만으로 충분하다.\n네 개를 연결하면 네 가지를 나란히 놓기만 하면 그냥 도구 네 개다.\nDataHub에서 Glossary Term이 바뀌면 Kafka MCL 이벤트가 발행된다. 이 이벤트가 DozerDB 온톨로지 그래프에 실시간 반영되고, 동시에 Vanna의 RAG Store에도 들어간다. NL2SQL 프롬프트에 주입되는 맥락이 자동 갱신되는 셈이다. ApeRAG의 Entity Extraction은 DataHub Glossary를 Taxonomy로 참조하니까 문서 검색 결과도 최신 용어 체계에 연결된다.\n한 곳에서 용어를 고치면 네 군데가 동시에 바뀐다. 수동으로 전파하면 규모가 커질수록 반드시 어딘가 누락된다.\n다음 글 DataHub의 Business Glossary를 온톨로지로 쓸 때의 한계와 우회를 다룬다.\rDataNexus를 설계하고 구축하는 과정을 기록합니다. GitHub\r| LinkedIn\r","permalink":"https://datanexus-kr.github.io/posts/datanexus/002-architecture-decisions/","summary":"DataNexus의 기술 스택을 DataHub + Vanna + ApeRAG + DozerDB로 결정한 과정. 후보군에서 탈락한 것들과 그 이유.","title":"2. 4개의 오픈소스를 이 조합으로 결정하기까지"},{"content":"\u0026ldquo;VIP 기준이 뭐죠?\u0026rdquo; 유통사 BI Agent 프로젝트에서 있었던 일이다.\n현업 담당자가 테스트 중에 Agent에게 물었다. \u0026ldquo;지난달 VIP 고객 매출 알려줘.\u0026rdquo; 시스템이 숫자를 뱉어냈는데, 담당자 표정이 좋지 않았다. \u0026ldquo;이거 뭔가 이상한데요. VIP 기준이 우리 팀이랑 다른 것 같아요.\u0026rdquo;\n마케팅의 VIP와 CRM의 VIP가 달랐다. 매출도 마찬가지. 순매출이냐 총매출이냐에 따라 수억 단위로 차이가 난다.\n처음 겪는 문제가 아니었다. DW를 클라우드로 옮기는 프로젝트에서도 봤고, 차세대 정보계를 여러 벤더와 1년 넘게 만들 때도 똑같았다. 벤더마다 \u0026ldquo;매출\u0026rdquo;, \u0026ldquo;원가\u0026quot;의 기준이 달라서 데이터 정합성 잡느라 몇 주씩 지연됐다. 용어 하나 안 맞으면 전체 일정이 밀린다. DW/BI 프로젝트를 하면서 이 문제가 안 나온 적이 없다.\n엔터프라이즈 DW에는 테이블과 컬럼이 있다. 없는 건 맥락이다. \u0026ldquo;이 컬럼이 비즈니스에서 뭘 의미하는지\u0026quot;가 기계가 읽을 수 있는 형태로 어디에도 정의되어 있지 않다.\n그래서 직접 만들기로 했다.\nNL2SQL은 만능이 아니다 요즘 NL2SQL 도구가 많다. 자연어를 SQL로 바꿔주는 것 자체는 이미 된다.\n실환경에 붙여보면 얘기가 달라진다. 벤치마크 점수가 높은 모델을 실제 DW에 연결하니까 체감 정확도가 확 떨어졌다. 카드사·통신사·공공데이터까지 내외부 데이터를 통합해 놓은 환경이었다. LLM이 이 복잡도(테이블 구조와 질문의 난이도)를 감당하지 못했다.\nDW의 DDL을 열어보면 원인이 보인다. T_CUST_MST.CUST_GRD_CD, T_ORD_DTL.SALE_AMT 같은 약어 테이블이 수천 개다. 벤치마크 DB의 customer_name, order_date와는 차원이 다르다. 같은 회사인데도 사업부마다 테이블 네이밍 규칙이 다르고, \u0026ldquo;매출\u0026quot;이라는 단어 하나가 사업부마다 다른 테이블을 가리킨다.\n파생 지표는 더 골치다. \u0026ldquo;순매출\u0026quot;은 단일 컬럼이 아니다. SUM(SALE_AMT) - SUM(RTN_AMT) - SUM(DC_AMT) 같은 계산식인데, 이 공식은 어떤 DDL에도 안 적혀 있다. 현업 머릿속에 있거나, 잘해야 누군가의 Excel 정의서 어딘가에 묻혀 있다.\nNL2SQL의 병목은 SQL 생성 능력이 아니다. 맥락이 없다.\n온톨로지라는 선택 대화형 BI에서 프롬프트 엔지니어링을 아무리 최적화해도 DDL만으로는 한계가 뚜렷했다. 멀티에이전트 아키텍처도 설계해 봤는데, 근본 문제는 같았다. LLM에게 줄 맥락 자체가 없다.\n온톨로지를 붙이기로 했다.\n거창한 얘기가 아니다. 실용적으로 보면 이런 것이다:\n# 순매출 용어 정의 - term: 순매출 definition: 총매출에서 반품과 에누리를 차감한 금액 formula: SUM(SALE_AMT) - SUM(RTN_AMT) - SUM(DC_AMT) synonyms: [Net Sales, 순매출액, 넷세일즈] related_tables: [T_SALE_DTL, T_RTN_DTL] owner: 재무팀 이런 용어 정의를 메타데이터 카탈로그에 등록하고, NL2SQL 엔진의 RAG Store에 자동 동기화한다. LLM이 \u0026ldquo;순매출\u0026quot;이라는 단어를 봤을 때 어떤 테이블의 어떤 컬럼을 어떤 계산식으로 조합해야 하는지 알게 되는 구조다.\n처리 흐름은 이렇다:\n온톨로지 적용 전후 차이 -내부 목표치가 EX(Execution Accuracy) 기준 +15~20%p 향상이다. MVP에서 EX 80% 이상, 안정화 단계에서 90% 이상. 현실적인 수치인지는 만들어 보면서 검증한다.\nDDL 붙여넣기로는 안 되는 이유 엔터프라이즈 DW의 DDL을 다 붙여넣으면 수만수십만 토큰이다. 테이블이 수백수천 개인 환경에서는 컨텍스트 윈도우에 다 못 넣는다. 넣더라도 LLM이 그 안에서 정확한 테이블을 골라내는 건 Needle-in-a-Haystack 문제다.\n보안도 걸린다. 기업 내부 스키마를 외부 API에 통째로 보낼 수 없다. 같은 \u0026ldquo;매출\u0026quot;이라도 A그룹사 사용자와 B그룹사 사용자가 봐야 하는 범위가 다르다. Row-level Security가 필요한 환경이다.\n가장 큰 문제는 지속성이다. DDL은 바뀐다. 비즈니스 용어 정의도 바뀐다. 차세대 정보계를 오픈하고 나서도 단계별 릴리즈가 이어지면 매번 메타데이터가 바뀐다. 일회성 프롬프트가 아니라, 변경을 감지해서 자동으로 RAG Store를 갱신하는 파이프라인이 필요하다.\n따로 플랫폼을 만들어야겠다고 생각했다.\nDataNexus가 하려는 것 네 가지 컴포넌트로 구성된다.\n메타데이터 카탈로그 -비즈니스 용어 정의, 테이블 메타, 데이터 계보를 한 곳에서 관리한다. 온톨로지의 원천(Source of Truth). NL2SQL 엔진 -자연어를 SQL로 변환하되, 온톨로지에서 가져온 맥락을 프롬프트에 주입한다. DDL만 던져주는 방식과 정확도 차이가 확 난다. 문서 지식엔진 -사업보고서, 정책문서 같은 비정형 데이터를 GraphRAG + 벡터 하이브리드로 검색한다. 그래프 DB -온톨로지를 지식 그래프로 저장한다. 그룹사별 Multi-DB 격리까지. 카탈로그에서 정의한 온톨로지가 NL2SQL과 문서검색에 자동 동기화되어, 사용자 질문에 맥락이 붙는 구조다. 각 컴포넌트에 사용한 오픈소스와 선정 이유는 다음 글에서 다룬다.\n왜 지금인가 범용 모델은 빠르게 좋아지고 있다. 단순 기획이나 문서 생성은 곧 commodity가 된다. 차이를 만들려면 기업 데이터의 맥락을 구조화해서 모델에 주입하는 시스템이 있어야 한다.\n우리 회사의 \u0026ldquo;순매출\u0026rdquo; 정의가 뭔지, LLM이 아무리 똑똑해져도 모른다. 기업 내부에만 있는 지식이기 때문이다.\nLLM 연구에서는 이걸 \u0026ldquo;Non-verifiable Domain\u0026rdquo; 이라고 부른다. 수학이나 코딩은 정답을 자동 검증할 수 있는데, 기업 내부의 암묵적 지식은 외부에서 판별할 방법이 없다. 이런 데이터 위에 쌓이는 경쟁 우위가 \u0026ldquo;Data Moat\u0026rdquo; 다.\n이 우위가 영원할 거라고 보진 않는다. 범용 모델의 일반화 속도를 DataNexus의 데이터 축적 속도가 앞서야 한다.\nData Moat를 쌓는 방법은 이렇다:\n온톨로지 기반 맥락 -도메인 전문가가 용어를 정제할수록 두꺼워지는 메타데이터 카탈로그 역할별 해석 -같은 질문이라도 재무팀과 마케팅팀에 다른 응답을 주는 페르소나 최적화. 사용 패턴이 쌓일수록 개인화된다. 시간축 지식 그래프 -Temporal Knowledge Graph로 \u0026ldquo;작년 4분기 기준 VIP 정의\u0026quot;와 \u0026ldquo;올해 기준 VIP 정의\u0026quot;를 구분 비공개 데이터 자산 -그룹사별 그래프 DB 격리 + Row-level Security. 각 그룹사의 데이터가 독립 자산이 된다. 올해 상반기까지 MVP를 내고 데이터 축적 루프를 돌리는 게 목표다.\n이 블로그의 목적 DataNexus를 만들면서 부딪히는 의사결정, 삽질, 해결 과정을 기록한다.\n다룰 것들:\n기술 스택을 선정한 과정 (후보군 탈락 사유 포함) 메타데이터 카탈로그의 Business Glossary를 온톨로지로 쓸 때의 한계와 우회 SKOS 호환 레이어를 넣은 이유 NL2SQL 엔진의 User-Aware 설계와 Row-level Security CQ(Competency Questions)로 온톨로지를 사전 검증하는 법 Query Router에서 결정론적 vs 확률론적 라우팅을 나누는 기준 에이전트 태스크를 쪼개는 79% Rule 이론보다는 실제로 부딪힌 문제와 그걸 어떻게 풀었는지 (또는 아직 못 풀었는지)를 쓸 예정이다.\n다음 글 DataNexus의 기술 스택 -4개의 오픈소스를 이 조합으로 결정하기까지의 과정. 후보군에서 탈락한 것들과 그 이유를 정리한다.\rDataNexus를 설계하고 구축하는 과정을 기록합니다. GitHub\r| LinkedIn\r","permalink":"https://datanexus-kr.github.io/posts/datanexus/001-why-datanexus/","summary":"\u003ch2 id=\"vip-기준이-뭐죠\"\u003e\u0026ldquo;VIP 기준이 뭐죠?\u0026rdquo;\u003c/h2\u003e\n\u003cp\u003e유통사 BI Agent 프로젝트에서 있었던 일이다.\u003c/p\u003e\n\u003cp\u003e현업 담당자가 테스트 중에 Agent에게 물었다. \u0026ldquo;지난달 VIP 고객 매출 알려줘.\u0026rdquo; 시스템이 숫자를 뱉어냈는데, 담당자 표정이 좋지 않았다. \u0026ldquo;이거 뭔가 이상한데요. VIP 기준이 우리 팀이랑 다른 것 같아요.\u0026rdquo;\u003c/p\u003e\n\u003cp\u003e마케팅의 VIP와 CRM의 VIP가 달랐다. 매출도 마찬가지. 순매출이냐 총매출이냐에 따라 수억 단위로 차이가 난다.\u003c/p\u003e\n\u003cp\u003e처음 겪는 문제가 아니었다. DW를 클라우드로 옮기는 프로젝트에서도 봤고, 차세대 정보계를 여러 벤더와 1년 넘게 만들 때도 똑같았다. 벤더마다 \u0026ldquo;매출\u0026rdquo;, \u0026ldquo;원가\u0026quot;의 기준이 달라서 데이터 정합성 잡느라 몇 주씩 지연됐다. 용어 하나 안 맞으면 전체 일정이 밀린다. DW/BI 프로젝트를 하면서 이 문제가 안 나온 적이 없다.\u003c/p\u003e","title":"1. 왜 DataNexus를 만드는가"},{"content":"Junho Lee (이준호) Data \u0026amp; AI Platform Architect | PM\nDW/BI 현장에서 대규모 DW 클라우드 전환부터 차세대 정보계 구축까지 설계하고 리드해왔습니다. Web/ERP 개발자로 시작해서 DW/BI 엔지니어, Technical Lead, 컨설팅 본부장을 거쳤고, 지금은 온톨로지 기반 AI 데이터 플랫폼을 만들고 있습니다.\n경력 요약 커리어 전반부는 엔터프라이즈 DW/BI에 집중했습니다. 대용량 DW의 클라우드 전환을 Tech Leader로 수행했고, 차세대 정보계 프로젝트를 멀티벤더 PMO로 운영했습니다. 소매·통신·제조·건설 등 산업 도메인을 가리지 않고 프로젝트를 수행해왔습니다.\n컨설팅 조직을 빌딩한 경험도 있습니다. 소수로 시작한 팀을 20명 규모까지 키우고, 매출도 수배 이상 성장시켰습니다. 채용, 교육, 기술 조직 운영, Presales, C레벨 대상 세미나까지 - 기술만 하는 사람은 아닙니다.\n최근에는 데이터와 AI의 접점에서 일하고 있습니다. LLM 기반 BI Agent를 구축하면서 NL2SQL의 실환경 한계를 경험했고, 그 과정에서 온톨로지 접근의 필요성을 확신하게 됐습니다. 지금은 DataNexus라는 통합 데이터 에이전트 플랫폼을 설계/구축 중입니다.\nDataNexus \u0026ldquo;Everyone is an Analyst.\u0026rdquo;\n엔터프라이즈 데이터 분석의 구조적 문제를 풀기 위한 플랫폼입니다. 자연어로 사내 데이터를 탐색하고 분석하는 AI 에이전트 -말은 쉬운데, 실환경에서는 테이블명이 T_CUST_MST 같은 약어 투성이고, \u0026ldquo;순매출\u0026quot;이라는 단어 하나에도 부서마다 계산 로직이 다릅니다. DDL만으로는 LLM이 비즈니스 맥락을 이해할 수 없습니다.\nDataNexus는 온톨로지 기반 NL2SQL 엔진, GraphRAG, Data Catalog를 결합해서 이 문제를 풀어갑니다. 오픈소스를 조합한 아키텍처로, 비정형 문서와 정형 DB를 하나의 인터페이스로 다루는 구조입니다.\n이 블로그는 DataNexus를 만들어가는 과정을 기록합니다. 아키텍처 결정, 기술 선택의 이유, 삽질과 해결 과정을 있는 그대로 남깁니다.\n기술 영역 AI/ML -온톨로지 LLM RAG, NL2SQL, Langchain, MCP, 멀티에이전트 시스템 설계 DW/Data Platform -Azure Synapse, BigQuery, Redshift, PostgreSQL, Oracle, Yellowbrick, Palantir Foundry BI -Power BI, Tableau, MicroStrategy, Qlik Sense, Looker, Superset ETL/ELT -ADF, SAP Data Services, IBM DataStage, Informatica, Databricks, SSIS Cloud -Azure (Synapse, ADF, ML), AWS (Redshift, S3, Glue), GCP (BigQuery, Gemini) Graph/Catalog -DataHub, Neo4j(DozerDB), ApeRAG Contact GitHub: @datanexus-kr\rLinkedIn: linkedin.com/in/leejuno\r","permalink":"https://datanexus-kr.github.io/about/","summary":"이준호 - Data \u0026amp; AI Platform Architect","title":"About"},{"content":"","permalink":"https://datanexus-kr.github.io/dashboard/","summary":"DataNexus 엔터프라이즈 온톨로지 기반 NL2SQL 자율 데이터 에이전트 플랫폼 개발 로드맵 및 실시간 진행 현황","title":"Development Roadmap"}]