티스토리 뷰
[개념] Glue Catalog는 왜 필요한가 - 내 Bronze 레이어가 카탈로그와 소통하는 전 과정
코딩하는 제리코 2026. 6. 16. 15:07들어가며
Bronze 레이어를 만들면서 설정에 이런 줄이 있었다.
.config("spark.sql.catalog.glue.catalog-impl",
"org.apache.iceberg.aws.glue.GlueCatalog")
데이터는 S3에 parquet로 저장되고,
Iceberg는 `metadata.json`에 스키마, 파티션, 파일목록을 다 가지고 있다.
그래서 자연스러운 의문이 들었다.
Iceberg가 metadata에 테이블 정보를 다 들고 있는데, Glue Catalog가 왜 또 필요하지?
이 의문이 사실 카탈로그의 본질을 찌른다고 한다. (**와우 정말 핵심을**)
결론부터 말하면 - 카탈로그가 아는 것과 metadata.json이 아는 것은 다르다. 이 글은 그 차이를 푼 기록이다.
솔직히 말하면 카탈로그에 대한 명확한 이해가 없었으니 이 부분에서 헷갈림이 발생한거라고 봐도 무방하다
1. 카탈로그가 없으면 생기는 문제
S3에 parquet 파일만 잔뜩 있다고 해보자.
s3://bucket/warehouse/bronze.db/ad_clicks/data/
dt=2026-06-14/hour=5/00001.parquet
dt=2026-06-14/hour=5/00002.parquet
dt=2026-06-14/hour=6/00003.parquet
... 수백 개
여기서 누군가 `SELECT * FRO ad_clicks`를 하려면 이걸 다 알아야 한다.
- ad_clicks라는 테이블이 존재하는가?
- 컬럼이 뭐가 있고 타입은 뭔가?
- 어떤 파일들이 이 테이블에 속하는가?
- 파티션은 어떻게 나뉘어 있는가?
S3는 그냥 파일 저장소라 이걸 모른다.
"파일 목록"은 주지만, "이게 무슨 테이블인지"는 모른다.
누군가 이 메타정보를 따로 기록해둬야한다.
그 "따로 기록하는 곳"이 카탈로그(catalog/metastore)"다.
s3 = 데이터가 어디 있나 (파일)
카탈로그 = 어떤 테이블이 있고, 스키마/위치가 뭔가 (메타데이터)
2. metastore.json은 무엇을 아는가
Iceberg의 metadata.json은 테이블에 대해 거의 다 안다.
metadata.json (특정 버전)
├ 스키마 (컬럼, 타입)
├ 파티션 명세 (dt, hour)
├ 어떤 manifest / parquet가 속하는지
└ 스냅샷 이력
→ 이 테이블의 내용은 metadata.json이 전부 안다.
이론적으론 카탈로그 없이 metadata.json 경로만 알면 그 테이블을 읽을 수 있다.
그래서 내가 여기서 왜 카탈로그가 필요한지 궁금했다.
3. 진짜 문제 - 지금 최신은 어느 metadata인가?
metadata.json은 커밋마다 새로 생긴다.
metadata/
00000-xxx.metadata.json (테이블 생성)
00001-xxx.metadata.json (1번째 커밋)
00002-xxx.metadata.json (2번째 커밋)
00003-xxx.metadata.json (방금 또 커밋)
...
그래서 지금 이 순간, 최신은 어느 파일인가? 라고 묻는다면
metadata.json 자기 자신은 이 답을 모른다.
각 파일은 그 시점의 완성된 상태일 뿐, 내가 지금 최신이다를 스스로 보장하지 못한다.
누군가 바깥에서 현재 유효한건 00003이라고 가리켜줘야한다.
그 포인터를 드는게 카탈로그다.
- metadata.json = 테이블이 "무엇인지"안다 (내용)
- 카탈로그 = 그 수많은 버전 중 지금 어느게 진짜인지 안다 (포인터)
비유-책의 판본과 도서 목록
- 책장: 초판, 2판, 3판, 4판 ... (각 판은 그 시점의 완성본)
각 판본은 자기 내용을 다 안다.
하지만 지금 정본이 몇판인가?는 판본 자신이 못 정한다.
도서관 목록(카탈로그)이 현재 정본 = 4판이라고 기록한다.
→ metadata.json = 판본(내용), 카탈로그 = 현재 정본은 4판 포인터
3. 카탈로그가 푸는 세 가지
① 최신 포인터 - 원자적 커밋의 기준
포인터를 카탈로그가 들고 있으면 커밋이 안전해진다.
새 metadata.json(00003) 씀
↓
카탈로그 포인터를 00002 → 00003 으로 "원자적으로" 교체
↓
이 교체가 성공해야 커밋 완료
(실패하면 포인터는 00002 그대로 = 커밋 안 된 셈)
이렇게 한방에 교체되는 포인터가 있어야
- 쓰다 만 커밋이 테이블에 안 보인다.
- 동시 커밋이 충돌 없이 처리된다.
② 카탈로그 없이 하면? (대안과 한계)
카탈로그 없이도 최신을 찾는 방법은 있지만 한계가 있다.
방법 A - version-hint 파일
- S3에 최신은 3번이라고 적은 파일을 둠(Hadoop 카탈로그 방식)
- → 결국 포인터 역할 하는 무언가가 필요하다는 뜻이고 카탈로그를 파일로 대체한 것 뿐이다.
방법 B - S3 디렉토리 스캔
- metadata 폴더를 다 뒤져 최근 걸 찾음
- → 느리고, 동시 커밋 시 누가 최신인지 race condition (원자성 깨짐)
그래서 제대로 된 환경은 별도 카탈로그(Glue 등) 포인터를 관리한다.
③ 도구 간 공유 - 중앙 명부
실용적 이유. 카탈로그가 있어야 여러 도구가 같은 테이블을 공유한다.
- Glue Catalog에 등록 → Athena가 ad_clicks 가 있다고 즉시 인식
- 카탈로그 없으면 → Athena한테 매번 S3 metadata 경로를 직접 알려줘야한다.
Spark가 적재만 했는데 Athena가 코드 한 줄 없이 쿼리할 수 있는 건 둘이 같은 Glue Catalog를 공유하기 때문이다.
4. Glue Catalog란
AWS Glue Data Catalog는 AWS가 관리하는 카탈로그이다.
전통적 Hadoop 생태계의 Hive Metastore를 AWS 매니지드로 대체한 것이다.
저장하는것:
데이터베이스 (bronze, silver, gold)
└ 테이블 (ad_clicks ...)
├ 스키마 / 파티션 명세
├ 데이터 위치 (s3://.../ad_clicks/)
└ 현재 최신 metadata.json 포인터 ← 가장 중요
AWS 생태계(Athena, EMR, Redshift Spectrum)가 전부 이 명부를 공유한다.
5. 내 Bronze 레이어가 Glue와 소통하는 절차
실제 `bronze_stream.py` 코드에서 무슨 일이 일어나는지.
절차 1 - glue 카탈로그 연결 (SparkSession)
.config("spark.sql.catalog.glue", "org.apache.iceberg.spark.SparkCatalog")
.config("spark.sql.catalog.glue.catalog-impl", "...glue.GlueCatalog") # 메타 → Glue
.config("spark.sql.catalog.glue.warehouse", "s3://.../warehouse") # 데이터 → S3
.config("spark.sql.catalog.glue.io-impl", "...s3.S3FileIO") # S3 입출력
→ `glue`라는 카탈로그를 정의한다. 메타는 Glue에, 데이터는 S3에.
이후 `glue.bronze.ad_clicks`는 Glue에 등록된, S3의 Iceberg 테이블로 해석된다.
절차 2 - DB, 테이블 생성(Glue에 등록)
spark.sql("CREATE DATABASE IF NOT EXISTS glue.bronze")
spark.sql("""
CREATE TABLE IF NOT EXISTS glue.bronze.ad_clicks (
key string, value string, topic string,
kafka_partition int, kafka_offset bigint,
kafka_timestamp timestamp, ingested_at timestamp,
dt string, hour int
)
USING iceberg
PARTITIONED BY (dt, hour)
""")
실제 일어나는 일:
`CREATE DATABASE` → Glue에 'bronze' DB 생성
`CREATE TABLE`
→ ① S3에 첫 metadata.json 씀
→ ② Glue에 'ad_clicks' 등록 (스키마 + 파티션 + metadata.json 포인터)
`IF NOT EXISTS`라 이미 있으면 건너뜀 → 재시작해도 안전(멱등)
절차 3 - append (포인터 갱신 = 커밋)
batch_df.filter(col("topic") == topic) \
.writeTo("glue.bronze.ad_clicks").append()
위 한줄에서:
- S3에 새 parquet 씀
- S3에 새 metadata.json 씀 (이 parquet 포함한 새 버전)
- Glue의 ad_clicks 포인터를 이전 → 새 metadata.json 으로 교체 ← 원자적 커밋
여기서 세번째 핵심이다.
parquet 데이터는 S3에 쓰지만, 그게 테이블의 일부라 됐다고 확정하는 건 Glue 포인터 교체다.
이게 성공해야 비로소 쿼리에 보인다.
6. 정리
metadata.json은 "테이블이 무엇인지"는 안다.
하지만 "그 수많은 버전 중 지금 최신이 어느 것인지"는 모른다.
카탈로그가 그 포인터를 들고, 원자적으로 교체해 커밋을 안전하게 만든다.
덤으로 여러 도구가 같은 테이블을 공유하는 중앙 명부가 된다.
내 Bronze에서:
- SparkSession에 glue 카탈로그 연결
- CREATE TABLE → Glue 등록 + S3 첫 metadata
- append → parquet/metadata 쓰고 → Glue 포인터 교체(커밋)
'Projects > 광고 플랫폼 Lakehouse 실전 설계 with Iceberg' 카테고리의 다른 글
| 광고 이벤트 레이크하우스 구축기 (6-1) — Silver 레이어: 설계와 전체 틀 (0) | 2026.06.20 |
|---|---|
| 광고 이벤트 레이크하우스 구축기 (5) — Bronze 레이어: Kafka에서 S3 Iceberg로, 그리고 운영의 현실 (0) | 2026.06.16 |
| 광고 이벤트 레이크하우스 구축기 (4) — Producer 컨테이너화 와 토픽 자동 생성 (0) | 2026.06.14 |
| [개념] Spark Structured Streaming vs AWS Glue Streaming (0) | 2026.06.11 |
| 광고 이벤트 레이크하우스 구축기 (3) — 로컬 Kafka 환경 구성 및 Producer 연결 테스트 (0) | 2026.06.11 |
- Total
- Today
- Yesterday
- Data engineering
- docker
- 데이터파이프라인
- spark
- Spark structured streaming
- de
- catchup
- Data Pipeline
- iceberg
- Backfill
- airflow
- AWS
- lake house
- Unity Catalog
- Consumer DAG
- elasticip
- Databricks
- kafka
- Data Engineerring
- lakehouse
- DAG
- Prodcuder DAG
- s3
- DataSet
- AWS Glue Catalog
- Glue
- RDD
- Daynamic Task
- Glue ETL
- Data Dngineering
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
