티스토리 뷰

들어가며

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()

위 한줄에서:

  1. S3에 새 parquet 씀
  2. S3에 새 metadata.json 씀 (이 parquet 포함한 새 버전)
  3. Glue의 ad_clicks 포인터를 이전 → 새 metadata.json 으로 교체 ← 원자적 커밋

여기서 세번째 핵심이다.

parquet 데이터는 S3에 쓰지만, 그게 테이블의 일부라 됐다고 확정하는 건 Glue 포인터 교체다.

이게 성공해야 비로소 쿼리에 보인다.


6. 정리

metadata.json은 "테이블이 무엇인지"는 안다.
하지만 "그 수많은 버전 중 지금 최신이 어느 것인지"는 모른다.
카탈로그가 그 포인터를 들고, 원자적으로 교체해 커밋을 안전하게 만든다.
덤으로 여러 도구가 같은 테이블을 공유하는 중앙 명부가 된다.

내 Bronze에서:

  1. SparkSession에 glue 카탈로그 연결
  2. CREATE TABLE → Glue 등록 + S3 첫 metadata
  3. append → parquet/metadata 쓰고 → Glue 포인터 교체(커밋)
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/06   »
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
글 보관함