Horizon Context를 활용한 메타데이터 관리: App Runtime을 데이터 리니지(Data Lineage)에 통합하기
요약
Snowflake의 확장된 Horizon Context 기능이 프리뷰로 공개되어, 데이터 제품 및 BI 툴을 포함한 광범위한 컨텍스트 관리가 가능해졌습니다. 이 기능을 통해 외부 서비스와 App Runtime에 배포된 API를 데이터 리니지(Data Lineage)에 연결하여, 데이터 모델링의 영향 범위를 명확히 파악할 수 있습니다.
핵심 포인트
- Horizon Context로 광범위한 컨텍스트 관리 가능
- 외부 메타데이터 커넥터로 외부 서비스 연동 용이
- App Runtime API를 리니지에 통합하여 영향 범위 추적
최근 Snowflake의 Horizon Catalog가 확장된 Horizon Context가 프리뷰로 공개되었습니다.
이 기능은 Snowflake 주변의 데이터 제품이나 BI 툴을 포함하는 컨텍스트 관리가 가능하여 큰 주목을 받고 있습니다.
먼저 사용해 보기 위해, 본문에서는 다음 내용을 시도합니다.
- 데이터와 App Runtime에 배포된 API를 리니지(Lineage)로 연결하기
(이 기능은 이미 GA가 된 기능입니다.) - (보너스) dbt core의 정보를 Snowflake로 전달하기
사용해 보기
외부 리니지(External Lineage) 사용하기
Horizon Context 기능으로 메타데이터 커넥터가 등장했습니다.
이를 통해 외부 서비스에서 메타데이터를 Snowflake로 가져와 컨텍스트로 관리할 수 있게 되었습니다.
BI 툴이나 외부 DWH가 Snowflake 데이터의 상류(Upstream)에 있는지, 하류(Downstream)에 있는지가 사람과 AI 모두에게 명확한 데이터 리니지를 제공합니다.
데이터 리니지에 한정해서 말하자면, 등록할 수 있는 정보는 커넥터가 존재하는 것만은 아닙니다.
OpenLineage 포맷을 사용함으로써 자유롭게 정보를 추가할 수 있습니다.
Snowflake에는 App Runtime이라는 기능이 있어 Next.js를 사용한 앱 개발이 간편하게 할 수 있는데,
App Runtime 자체가 Snowflake의 메타데이터 그룹으로부터 종종 고립되는 경우가 많아, 데이터 모델링의 영향 범위를 파악하기 위해 앱의 소스 코드를 읽을 필요가 있었습니다.
테이블과 같은 데이터 하류에 앱의 API가 연결되어 있으면 인지 부하(Cognitive Load)를 낮출 수 있을 것 같아 시도해 봅니다.
여기서는 dbt의 공식 샘플 프로젝트인 jaffle_shop을 사용합니다.
데이터 상세 및 준비 과정은 생략하겠습니다.
Snowflake에서 쿼리를 실행하면, 그 내용으로부터 리니지 정보를 자동으로 추출해 줍니다.
이는 Horizon Catalog 기능으로 원래 구현되어 있었습니다.

기본 리니지
앱 생성 부분은 생략하지만, Jaffle Shop의 데이터를 사용하여 분석이나 데이터 수정을 할 수 있는 앱을 만들었습니다.

mart 데이터를 사용한 그래프 표시

Snowflake 마스터 데이터를 수정하는 툴
디렉터리 구성은 Next.js App Router의 규약에 따릅니다.
app/api/
배치 아래 Snowflake로 연결되는 SQL을 포함한 API 라우트와, app/ 및 components/에 화면을 배치했습니다.
jaffle-app/
├── app/
│ ├── page.tsx ← 대시보드 화면 (/)
...
이 앱의 정보를 리니지에 등록하기 위해, 이 디렉터리에서 자동으로 다음 정보들을 추출합니다.
| 정보 | 추출 방법 | 예시 |
|---|---|---|
| API 엔드포인트명 | app/api/ 하위 파일 경로 | app/api/customers/route.ts → /app/api/customers |
| HTTP 메서드 | 파일 내의 `export function GET | PUT` |
| 참조 테이블 | SQL 템플릿 리터럴을 sqlglot으로 분석 | JAFFLE_SHOP.MARTS.CUSTOMERS |
| 화면 노드명 | 페이지 파일 경로 + 앱 이름 | app/customers/page.tsx → jaffle-app/customers |
| 화면→API 대응 | 컴포넌트 내의 `fetch( |
- 추출(Extract) — TypeScript의 API 라우트 파일에서 SQL 템플릿 리터럴을 정규표현식으로 추출
- 분석(Analyze) — sqlglot으로 SQL 분석. 테이블 FQN 추출 + 컬럼 레벨 소스 추적
- 전송(Send) — OpenLineage COMPLETE 이벤트를 구성하여 Snowflake REST API로 전송
Step 1: TypeScript에서 SQL을 추출하기
App Runtime에 배포하는 앱의 API 라우트는 Next.js App Router 규약에 따라 app/api/<name>/route.ts에 배치합니다.
각 파일 내의 SQL은 템플릿 리터럴로 정의되어 있습니다.
// app/api/customers/route.ts
const LIST_SQL = (db: string, schema: string) => `
SELECT CUSTOMER_ID, CUSTOMER_NAME,
...
이를 정규표현식으로 추출합니다:
_TEMPLATE_LITERAL = re.compile(
r'(?:const|let|var)\s+(\w+)\s*=\s*'
r'(?:\([^)]*\)\s*(?::\s*\w+)?\s*=>\s*)?' # 화살표 함수의 인자 부분
...
const NAME = ``...``뿐만 아니라 const NAME = (db, schema) => `...`` 패턴에도 대응합니다.
HTTP 메서드(GET/PUT/POST/DELETE)는 파일 내의 export 함수 이름과 변수명으로부터 추정합니다.
Step 2: sqlglot으로 SQL 분석 + 컬럼 리니지
추출한 쿼리를 sqlglot을 사용하여 분석합니다.
쿼리에는 ${db}와 같은 템플릿 변수나 Snowflake 바인드 변수(:name 또는 ?)가 포함되어 있으므로, sqlglot에 전달하기 전에 전처리가 필요합니다.
def preprocess(sql: str) -> str:
# ${VAR} → 실제 값으로 치환
sql = re.sub(r'\$\{(\w+)\}', replacer, sql)
...
전처리된 SQL을 sqlglot으로 파싱하고, AST에서 모든 테이블 참조를 가져옵니다.
또한, sqlglot의 lineage() API를 사용하면
アプリ名を含めるようにします。
フロントエンドコンポーネントから fetch("/api/...")
의 호출을 정규표현식으로 추출하고, 페이지 파일(page.tsx)과의 대응 관계를 통해 화면→API 매핑을 자동 생성했습니다.
OpenLineage는 데이터 리니지(Data Lineage)의 오픈 포맷이며, 작업(Job)의 입출력을 COMPLETE 이벤트로 기록합니다.
Snowflake는 /api/v2/lineage/external-lineage 엔드포인트에서 이 포맷을 수신하여, 리니지 그래프에 정보를 통합해 줍니다.
Layer 1의 read 이벤트(테이블 → API)에는 OpenLineage의 columnLineage facet을 부여합니다.
OpenLineage의 facet은 리니지에 추가 정보를 제공하는 메커니즘이지만, Snowflake에서 지원하는 것은 datasetType (노드의 종류 표시)와 columnLineage (컬럼 레벨 리니지) 두 가지뿐입니다.
schema나 documentation, ownership 등의 facet은 전송해도 오류는 발생하지 않지만 무시됩니다.
documentation 같은 것은 사용할 수 있으면 편리하니까, 향후 확장에 기대가 됩니다……!
추출한 정보를 다음과 같이 JSON 형식으로 집약합니다.
# event JSON
{
"eventType": "COMPLETE",
...
나머지는 REST API를 사용하여 Snowflake로 전송하면 완료입니다. Snowflake Python 커넥터로 확립된 세션의 토큰을 사용했습니다.
rest = connection._rest
token = rest.token
url = f"{rest._protocol}://{rest._host}:{rest._port}/api/v2/lineage/external-lineage"
...
실행 결과
성공하자, 테이블이나 뷰의 리니지에 앱의 API가 표시되게 되었습니다.

두 개의 노드가 CUSTOMERS 테이블에 연결됨
OpenLineage를 사용해 등록한 리니지 정보는 'External'로 표시됩니다.
하나는 api이고, 다른 하나는 그 api를 사용하는 화면입니다.
또한, 마스터 데이터 업데이트 같은 PUT이나 PATCH를 사용하는 API는 데이터의 상류(Upstream)에 연결되어 있습니다.

데이터 등록 API가 테이블의 상류에 표시
API가 어느 화면에서 호출되고 있는지까지 표현할 수 있다는 점이 좋습니다.
리니지 정보 활용하기
예를 들어, CUSTOMERS 테이블의 CUSTOMER_NAME 정의를 수정하고 싶을 경우, 내장 함수인 GET_LINEAGE를 사용하면 영향 범위를 특정하는 것이 가능합니다.
SELECT SOURCE_OBJECT_NAME, SOURCE_COLUMN_NAME,
TARGET_OBJECT_NAME, TARGET_COLUMN_NAME, DISTANCE
FROM TABLE(
...
| 소스 | 소스 컬럼 | 타겟 | 타겟 컬럼 | 거리 |
|---|---|---|---|---|
| CUSTOMERS | CUSTOMER_NAME | /app/api/customers | CUSTOMER_NAME | 1 |
| CUSTOMERS | CUSTOMER_NAME | /app/api/orders | CUSTOMER_NAME | 1 |
CUSTOMER_NAME을 변경하면, /api/customers와 /api/orders 두 개의 엔드포인트가 영향을 받는 것을 한눈에 알 수 있습니다.
또한, CoCo를 사용할 경우에도 마찬가지로 테이블에서 앱 정보까지 도달하는 것이 가능합니다.

(덤) dbt core의 메타데이터도 연계하기
덤으로, 이번에 사용한 dbt core의 정보도 Snowflake로 전달해 보겠습니다.
이번에 프리뷰가 된 Horizon Context 기능을 사용하면 dbt의 메타데이터를 Snowflake와 연계할 수 있습니다.
dbt platform을 이용하는 경우에는 자동 연계가 가능하지만, dbt core는 GUI 조작으로 필요한 파일을 업로드해야 합니다.
사이드 메뉴의 Catalog → Connections에서 dbt를 선택합니다.
dbt core로부터 정보를 전달하려면 총 세 개의 파일이 필요합니다.
- manifest.json
- catalog.json
- run_results.json

각각 드래그 앤 드롭으로 파일을 업로드했습니다.
커넥터 생성이 완료되면 Explorer에 dbt 디렉토리가 나타납니다.

dbt 커넥터를 사용하면 dbt 관리의 모델 목록을 파악할 수 있을 뿐만 아니라, dbt test 결과도 확인할 수 있습니다.

dbt 프로젝트의 리니지(Lineage)를 표시할 수 있음

Snowsight에서 테스트 결과를 일괄 표시
이번에는 Snowflake에서 Jaffle Shop을 다루고 있기 때문에 dbt 커넥터의 장점을 충분히 누리고 있지는 못하지만,
d큐멘테이션에 따르면 BigQuery나 DataBricks 등 다른 DWH의 모델을 다루는 dbt 프로젝트와도 리니지를 연결할 수 있는 것 같습니다.
마무리하며
Horizon Context의 메타데이터 커넥터를 사용하면 외부 시스템으로부터 메타데이터를 가져올 수 있습니다.
또한, OpenLineage를 사용하면 커넥터가 존재하지 않는 서비스에서도 리니지 정보를 부여하는 것이 가능합니다.
이 글에서 소개한 내용은 Horizon Context 기능군 중 일부에 불과하며, 다른 강력한 기능들도 많이 있습니다.
AI 활용에 있어 컨텍스트(Context)를 정돈한다는 것은 그 무엇보다 결과로 나타납니다.
쉽게 시작할 수 있으니 꼭 사용해 보세요!
토론
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기