Audit Event 데이터를 메달리온 아키텍처로 정제하기
파이프라인 이후의 질문
이전 글에서 감사 이벤트가 SNS → SQS → EventBridge Pipes → NSP3 Kafka까지 도달하는 구조를 만들었다. 그런데 Kafka에 이벤트가 쌓인다고 해서 바로 분석이 가능한 건 아니다.
Kafka 토픽에 들어 있는 원시 이벤트는 JSON 문자열 그대로다. 필드가 중첩되어 있고, 타임스탬프 형식이 다르고, 같은 행위가 여러 이벤트로 나뉘어 있을 수 있다. 이 상태로는 “지난 달 비밀번호 초기화가 몇 건이었나?” 같은 질문에 바로 답할 수 없다.
메달리온 아키텍처란
Databricks의 메달리온 아키텍처(Medallion Architecture)는 데이터를 Bronze → Silver → Gold 3계층으로 정제하는 패턴이다.
| 계층 | 역할 | 데이터 특성 |
|---|---|---|
| Bronze | 원시 데이터 그대로 적재 | JSON 그대로, 스키마 미적용, append-only |
| Silver | 클렌징, 정규화, 타입 변환 | 컬럼화, 중복 제거, 타임존 통일 |
| Gold | 비즈니스 의미 부여, 집계 | 분석 질문에 바로 답할 수 있는 구조 |
이 패턴의 핵심은 각 계층이 독립적으로 재처리 가능하다는 점이다. Bronze 데이터가 원본 그대로 남아 있기 때문에, Silver나 Gold의 로직이 바뀌어도 처음부터 다시 정제할 수 있다.
Bronze: 원시 이벤트 적재
Kafka에서 소비한 감사 이벤트를 Databricks Delta Table에 그대로 저장한다.
-- Bronze 테이블 개념 예시
CREATE TABLE IF NOT EXISTS audit_event_bronze (
kafka_key STRING,
kafka_offset BIGINT,
kafka_timestamp TIMESTAMP,
raw_payload STRING, -- Kafka 메시지 원본 (JSON)
ingestion_time TIMESTAMP DEFAULT current_timestamp()
)
USING DELTA
TBLPROPERTIES ('delta.enableChangeDataFeed' = 'true');Bronze 계층에서는 아무것도 해석하지 않는다. JSON 파싱도 하지 않고, 단지 원본을 안전하게 보관한다. 이렇게 하는 이유는 단순하다 — 파싱 로직이 바뀌거나, 새로운 필드를 추출해야 할 때 원본이 남아 있어야 한다.
enableChangeDataFeed를 켜둔 이유는 후속 Vector Search 인덱스의 Delta Sync에 필요하기 때문이다.Silver: 정제와 정규화
Bronze에서 JSON을 파싱하고, 비즈니스에 의미 있는 컬럼으로 변환한다.
-- Silver 테이블 개념 예시
CREATE TABLE IF NOT EXISTS audit_event_silver AS
SELECT
get_json_object(raw_payload, '$.eventId') AS event_id,
get_json_object(raw_payload, '$.actionType') AS action_type,
get_json_object(raw_payload, '$.targetType') AS target_type,
get_json_object(raw_payload, '$.actor.type') AS actor_type,
get_json_object(raw_payload, '$.actor.id') AS actor_id,
get_json_object(raw_payload, '$.target.id') AS target_id,
to_timestamp(get_json_object(raw_payload, '$.timestamp')) AS event_timestamp,
get_json_object(raw_payload, '$.context') AS context_json,
ingestion_time
FROM audit_event_bronze;Silver 계층에서 처리하는 것들:
- 타입 변환: 문자열 타임스탬프를
TIMESTAMP타입으로 - 중첩 해제:
$.actor.type같은 중첩 JSON을 flat 컬럼으로 - 정규화: 같은 행위를 나타내는 다른 표현을 통일
- 중복 제거:
event_id기반 deduplication - 타임존 통일: UTC 기준으로 정규화
Gold: 분석 가능한 테이블
Gold 계층은 비즈니스 질문에 바로 답할 수 있는 구조로 데이터를 변환한다. 단일 테이블이 아니라 분석 목적별로 여러 테이블을 만든다.
-- Gold: 일별 이벤트 집계 개념 예시
CREATE TABLE IF NOT EXISTS audit_events_daily_summary AS
SELECT
date_trunc('DAY', event_timestamp) AS event_date,
action_type,
target_type,
actor_type,
COUNT(*) AS event_count,
COUNT(DISTINCT actor_id) AS unique_actors,
COUNT(DISTINCT target_id) AS unique_targets
FROM audit_event_silver
GROUP BY 1, 2, 3, 4;
-- Gold: 사용자별 활동 이력 개념 예시
CREATE TABLE IF NOT EXISTS user_activity_timeline AS
SELECT
target_id AS user_id,
action_type,
actor_id AS performed_by,
actor_type,
event_timestamp,
context_json
FROM audit_event_silver
WHERE target_type = 'ACCOUNT'
ORDER BY target_id, event_timestamp;실제로 운영한 Gold 테이블은 약 8개였다. 등록, 로그인, 비밀번호 초기화, 승인, 권한 변경, 세션 등 이벤트 유형별로 분리했다.
| Gold 테이블 | 용도 |
|---|---|
| 이벤트 유형별 집계 | ”이번 달 비밀번호 초기화 몇 건?” |
| 일별/주별 트렌드 | ”등록 추이가 어떻게 변하고 있나?” |
| 사용자별 활동 이력 | ”이 사용자가 최근 어떤 작업을 했나?” |
| 관리자별 활동 요약 | ”어떤 관리자가 가장 많은 변경을 했나?” |
| 이상 탐지용 | ”비정상적으로 많은 권한 변경이 있었나?” |
다른 팀과의 데이터 공유
메달리온 아키텍처의 또 다른 가치는 데이터를 다른 팀과 공유하기 쉬운 구조라는 점이다.
Gold 테이블은 이미 정제되고 의미가 부여된 상태이기 때문에, 다른 팀이 우리 파이프라인의 내부 구현을 모르더라도 Gold 테이블의 스키마만 보면 데이터를 이해하고 사용할 수 있다.
Databricks Unity Catalog를 통해 테이블 단위로 접근 권한을 관리할 수 있어서, 특정 팀에게 필요한 Gold 테이블만 공유하는 것도 가능했다.
Bronze를 남겨둬야 하는 이유
메달리온 아키텍처에서 가장 흔한 실수는 Bronze를 건너뛰고 Silver부터 시작하는 것이다.
- Silver나 Gold의 변환 로직에 버그가 있으면? Bronze에서 재처리하면 된다
- 새로운 분석 관점이 필요하면? Bronze 원본에서 새로운 Gold 테이블을 만들 수 있다
- 데이터 설계(ActionType 등)가 바뀌면? 기존 데이터 해석 방식을 업데이트하고 재처리할 수 있다
Bronze 테이블의 스토리지 비용은 분석 가능성에 비하면 매우 작다. 그리고 Bronze를 append-only로 유지하면 감사 데이터의 원본 무결성을 보장하는 역할도 겸한다.
파이프라인에서 데이터 자산으로
감사 이벤트가 Kafka 토픽의 메시지에서 분석 가능한 Gold 테이블이 되기까지, 기술적으로 어려운 부분은 변환 로직보다 어떤 질문에 답해야 하는지 정의하는 일이었다. Gold 테이블의 스키마는 결국 비즈니스 질문의 구조를 반영한다.
이전 글에서 정의한 데이터 설계(ActionType, TargetType, ActorType)가 여기서 다시 중요해진다. Silver → Gold 변환에서 이 필드들이 GROUP BY, FILTER, JOIN의 핵심 키가 되기 때문이다. 처음에 필드 표준을 신중하게 정한 것이 메달리온 아키텍처 전체의 품질을 결정한 셈이다.
실제로 이 구조 위에 자연어 인터페이스를 올린 뒤, Product Manager가 직접 “이번 달 등록 수가 얼마야?”, “이 기능의 아키텍처가 어떻게 되어 있어?” 같은 질문을 던져서 지표를 확인하거나 시스템 구조를 파악할 수 있게 되었다. 데이터가 정제된 형태로 준비되어 있었기 때문에 가능한 일이었다.
다음 글에서는 이 Gold 테이블 위에 Text-to-SQL과 RAG를 올려서, 자연어로 데이터에 질문할 수 있는 구조를 어떻게 만들었는지 이어서 본다.