왜 Athena/Iceberg는 코드가 곧 명세(Spec)가 되게 만드는 경향이 있는가
요약
Athena와 Iceberg 환경에서 '코드가 곧 명세'가 되는 원리를 분석하며, 시스템의 선언된 상태와 실현된 상태 사이의 간극을 다룹니다. 진정한 선언적 설정을 위해서는 엔진의 역할보다 개발자의 규율이 더 중요함을 강조합니다.
핵심 포인트
- 코드가 명세 역할을 하는 것은 엔진의 기능보다 개발자의 규율(90%)에 달려 있음
- 명세는 선언된 복사본(코드)과 실현된 복사본(엔진 상태) 두 가지로 존재함
- 선언된 코드와 실제 시스템 상태 사이의 간극(Drift)을 줄이는 것이 핵심 과제임
- 에이전트가 읽고 실행할 수 있는 최선의 선언적 설정 모델을 탐구 중임
이 주제가 나올 때마다 사람들은 동일한 설정을 언급합니다. Athena on Iceberg는 "코드가 곧 명세 (the code is the spec)"가 되는 환경이라는 것입니다. 즉, 아무것도 로그인하지 않고도 Git을 열어 카탈로그(catalog)부터 변환(transforms), 스키마(schema)에 이르기까지 시스템 전체를 읽을 수 있는 환경 말입니다. 제 경험상 그들의 말이 틀린 것은 아닙니다.
제가 주장하고 싶은 것은, 그들이 맞긴 하지만 그 이유는 대부분 틀렸다는 점입니다.
사람들이 이 조합을 찾는 이유는 엔진 때문입니다. 서버리스(serverless), 오픈 포맷(open format), 프로비저닝(provision)할 것이 없다는 점 때문이죠. 하지만 실제로 코드가 명세 역할을 유지하게 만드는 요소는 거의 어떤 엔진에도 적용할 수 있는 것입니다. 그리고 이 구조가 깨질 때, 그 원인은 Iceberg와 전혀 상관이 없습니다.
그래서 저는 현재 다음과 같은 결론을 내렸습니다. "코드가 곧 명세"라는 원칙이 유지되느냐는 90%의 규율(discipline)과 10%의 엔진에 달려 있습니다. Athena/Iceberg는 그 10%의 공로를 정직하게 인정받고 있지만, 그들이 얻는 것은 오직 10%뿐입니다. 그런데 저는 사람들이 이 10%를 전체인 것처럼 착각하는 것을 계속 목격하고 있습니다. (이는 제 개인적인 시도 끝에 얻은 결론일 뿐, 조언은 아닙니다.)
미리 말씀드리자면, 저는 여전히 이 과정의 중간에 있습니다. 에이전트(agent)를 위한 최선의 선언적(declarative) 설정이 실제로 어떤 모습인지 — 즉, 추측 없이 시스템을 에이전트가 읽고 실행할 수 있는 명세로 어떻게 전환할 것인지 — 는 제가 현재 활발히 테스트 중인 단계이며, 결론을 내린 상태가 아닙니다. 다음 내용을 특정 시점에서의 작업 중인 아이디어로 읽어주시기 바랍니다. 저는 이 글을 적음으로써 제 생각이 어디서 틀렸는지 찾아내고자 합니다.
그 10%가 실제로 어디에 존재하는지 확인하려면, 상태 저장 시스템(stateful system)은 항상 두 개의 복사본을 유지한다는 점을 주목하는 것이 도움이 됩니다.
명세에는 두 개의 복사본이 있다
하나의 복사본은 선언된(declared) 것입니다. 즉, 당신이 작성한 코드, 커밋한 스키마(schema), dbt의 변환(transforms), Terraform의 카탈로그(catalog)입니다. 다른 하나는 실현된(realized) 것입니다. 엔진이 실행되는 동안 축적하는 상태, 즉 통계(statistics), 물리적 레이아웃(physical layout), 캐시(caches), 유지보수 이력(maintenance history), 누군가 새벽 2시에 설정해둔 튜닝 노브(tuning knobs) 등이 이에 해당합니다.
"코드가 곧 명세"라는 말은 사실 이 두 복사본 사이의 거리에 대한 주장입니다. 선언된 복사본이 실현된 복사본의 거의 모든 것을 설명할 수 있을 때, 당신은 Git을 읽는 것만으로 시스템을 추론할 수 있습니다. 그렇지 않다면, 당신은 추론할 수 없습니다.
그 상태 중 일부는 선언 가능합니다. DDL, dbt, 또는 Terraform에서 데이터 분포(distribution)나 정렬 전략(sort strategy)을 고정할 수 있습니다. 하지만 상당 부분은 선언이 불가능하거나, 당신이 선언한 내용으로부터 벗어나(drift) 버립니다. 즉, 자동 최적화(automatic optimization)가 작동할 때 엔진이 실제로 어떤 전략을 사용하는지, 마지막 Vacuum이 언제 끝났는지, 통계 정보(statistics)가 최신인지, 어떤 구체화된 뷰(materialized view)가 오래되었는지, 해당 파티션(partition)이 정말로 프루닝(pruning)되고 있는지 등은 알 수 없습니다. 이를 확인하려면 시스템에 로그인하여 실행 중인 시스템에 직접 물어봐야 합니다. 이제 당신의 명세(spec)는 절반은 저장소(repo)에 있고, 나머지 절반은 누군가의 머릿속에 있게 됩니다.
따라서 진짜 질문은 "내 코드가 읽기 쉬운가"가 아닙니다. 질문은 이것입니다: 진실의 얼마나 많은 부분이 코드를 벗어나 내가 볼 수 없는 런타임 상태(runtime state)로 빠져나갔는가? 나는 이것을 _숨겨진 상태 표면적(hidden-state surface area)_이라고 부르기 시작했습니다. 이를 줄이면 코드가 명세로 유지됩니다. 이를 방치하면 코드는 그저 희망 섞인 스케치(sketch)가 되어버립니다.
나는 얼마 전 엄격함이 압축을 가져온다(rigor compresses)고 주장한 바 있습니다. 사실을 더 정확하게 기술할 수 있을수록, 이를 전달하는 데 필요한 토큰(token)의 수는 줄어듭니다. 이는 함수가 아닌 플랫폼을 향한 동일한 아이디어입니다. 숨겨진 상태의 표면적이 작을수록, 인간이든 에이전트(agent)든 누구에게나 전달할 수 있는 시스템에 대한 설명이 더 짧고 정확해집니다.
이것이 주로 규율(discipline)의 문제인 이유
먼저 솔직하게 말하고 싶은 부분이 있습니다. 이것이 90%를 차지하기 때문입니다. 엔진이 당신의 시스템을 읽기 쉽게 만들어주는 것이 아닙니다. 선언적 엄격함(Declarative rigor)이 그렇게 만드는 것입니다.
데이터 계약(data contracts)으로 표현된 카탈로그(catalog), 단일 dbt DAG로 표현된 변환(transform), Terraform으로 표현된 인프라(infra) — 이것이 설계 의도(design intent)를 코드에서 추적 가능하게 만드는 요소입니다. 그리고 이것은 거의 전적으로 엔진에 무관(engine-agnostic)합니다. 이 모든 것을 Redshift로 향하게 해도 신규 입사자가 처음부터 끝까지 읽을 수 있는 결과물을 얻을 수 있습니다. 반대로, 수동으로 실행된 ALTER 문과 아무도 건드리지 못하는 고립된 파티션(orphaned partitions)이 뒤섞인 문서화되지 않은 늪과 같은 Athena-on-Iceberg 설정을 만들 수도 있습니다.
저는 두 가지를 모두 경험해 보았습니다. 읽기 쉬운 설정은 스토리지 포맷(storage format) 덕분이 아니라, 규칙들이 한 곳에 선언되고 강제되었기 때문에 읽기 쉬웠던 것입니다. 만약 다른 사람이 "Iceberg이기에 가독성이 좋다"라고 주장했다면 저는 반박했겠지만, 저 자신에게도 동일한 기준을 적용해야 합니다.
다른 것은 하지 않더라도, 이 부분만큼은 반드시 실천하십시오. 바로 여기에 핵심적인 영향력(leverage)이 있습니다.
하지만 엔진이 난이도를 결정합니다
이제 나머지 10%에 대해 이야기해 보겠습니다. 작지만 결코 무시할 수 없는 부분입니다. 동일한 규율을 갖추었다고 가정할 때, 엔진들은 자신들의 내부에 얼마나 많은 상태(state)를 유지하려고 고집하는지, 혹은 얼마나 많은 부분을 사용자가 볼 수 있도록 외부로 노출하는지에 따라 차이가 납니다. 이것이 코드를 명세(spec)로 만들 수 있는지 여부를 결정하는 것은 아닙니다. 다만, 그 상태를 유지하기 위해 얼마나 많은 규율(discipline)이 비용으로 발생하는지를 결정합니다.
Iceberg 기반의 Athena는 이례적일 정도로 많은 것을 외부로 노출합니다. 보통 독점적인 엔진 내부에 갇혀 있는 요소들—스냅샷 히스토리(snapshot history), 존재하는 파일들의 매니페스트(manifest), 현재 스키마(schema), 파티션 명세(partition spec)—이 특정 벤더의 내부 시스템 테이블에 국한되지 않는 오픈 메타데이터(open metadata)로 존재합니다. 여러분은 표준 메타데이터 테이블($snapshots, $manifests, $history)이나 카탈로그(catalog), 또는 객체 스토리지(object storage)에서 직접 이를 읽을 수 있습니다. 이것은 여전히 '상태(state)'이며 런타임(runtime)에 변경되지만, 포맷이 개방적이고 이식 가능(portable)하기 때문에 동일한 사실 정보에 대해 여러 도구에서 접근할 수 있습니다.
Redshift는 그러한 진실의 더 많은 부분을 자체 인터페이스 뒤에 숨겨둡니다. 구체화된 뷰(Materialized-view)의 최신성, VACUUM/ANALYZE 히스토리, 통계(statistics), 워크로드 관리(workload-management) 튜닝, 그리고 자동 테이블 최적화(automatic table optimization)나 AUTO 분산(distribution)을 통한 '실질적인' 물리적 레이아웃(physical layout)—이는 여러분이 선언한 내용과 상관없이 데이터 볼륨에 따라 변할 수 있습니다. 이 모든 것은 관찰 가능하며, 단지 시스템 뷰(system views)를 쿼리하여 확인할 수 있을 뿐입니다.
| Athena / Iceberg | Redshift | |
|---|---|---|
| Git / 코드에서 확인 가능 | Terraform, dbt, contracts, SQL/YAML | Terraform, dbt, DDL, contracts |
| ... |
솔직한 프레임링(framing)을 하자면, 어느 한쪽은 읽기 쉽고 다른 쪽은 그렇지 않다는 뜻이 아닙니다. 둘 다 자신의 상태를 노출합니다. 차이점은 그 _방식(how)_에 있습니다. 한쪽은 여러 호환 가능한 도구들이 검사할 수 있는 개방형 포맷(open format)으로 사실(facts)을 전달하는 반면, 다른 한쪽은 자체 인터페이스를 통해 사실을 다시 전달합니다. 가독성 수준은 동일하지만, 이를 유지하기 위해 필요한 엔진 특정적(engine-specific) 규율이 적게 들 뿐입니다. 이것이 전체 운영 작업량이 반드시 줄어든다는 의미는 아닙니다. 이것이 바로 그 10%의 핵심입니다. (이는 제가 계속 설명하고 있는 배치 지향적, 테넌트별 워크로드에 해당합니다. 거대하고 변화가 심한 Iceberg 자산은 자체적으로 거대한 상태 표면(state surface)—매니페스트(manifests), 작은 파일들, 스냅샷 확산(snapshot sprawl), 카탈로그 커밋, 엔진 간 호환성—을 형성하며, 그 격차는 점점 줄어듭니다.)
내가 실제로 신경 쓰는 이유: 다운스트림의 추측기(guesser)
여기서부터는 저에게 있어 미학적인 논쟁이 아니게 됩니다.
진정한 경계선은 S3 대 데이터베이스의 대결이 아닙니다. 그것은 바로 이것입니다: 에이전트(agent)가 실제로 도달할 수 있는 검사 가능한 인터페이스를 통해 노출된 사실은 검증된(verified) 것이며, 관찰 가능한 컨텍스트 밖에 있는 사실은 추정된(estimated) 것이어야 합니다. 에이전트가 쿼리할 수 있도록 허용된 Redshift의 VACUUM 이력은 검증된 것이지만, 에이전트가 카탈로그 접근 권한이나 도구가 없는 Iceberg 스냅샷은 추측일 뿐입니다. 엔진이 이를 결정하는 것이 아니라, 도달 가능한 증거가 결정합니다. Iceberg가 지향하는 바는 그 증거를 안전하고 휴대 가능하게 노출하는 것을 더 쉽게 만드는 것입니다. 즉, 개방형 메타데이터는 라이브 웨어하우스(live warehouse)의 열쇠를 통째로 넘겨주지 않고도 읽기 권한을 부여하기가 매우 간단합니다.
그리고 이러한 사실(facts)을 소비하는 주체는 점점 더 에이전트(agent)가 되어가고 있습니다. 요즘 저는 스키마(schema)를 가장 먼저 읽는 사람이 거의 없습니다. 대신 레포지토리(repo) 내에서 실행되며 마이그레이션(migration)을 편집하고 변환(transform) 로직을 다시 쓰는 Claude Code나 Codex가 그 역할을 합니다. 에이전트가 '볼 수 있는' 것은 선언된 복사본과 제가 연결해 둔 인터페이스(interface)뿐이며, 그 외의 모든 것은 에이전트가 추론합니다. 숨겨진 상태(hidden-state)의 표면적이 넓을수록, 혹은 그 증거에 도달하기 어려울수록, 에이전트는 제 시스템의 더 많은 부분을 조용히 추측하게 됩니다. 그러면 저는 프롬프트(prompt)에 더 많은 방어적인 지침을 추가하게 됩니다: "먼저 유효한 정렬 전략(effective sort strategy)을 확인하세요", "MV(Materialized View)가 최신이라고 가정하지 마세요", "마이그레이션하기 전에 검증하세요"와 같은 것들 말입니다. 사실(fact)에 접근할 수 있다면 에이전트는 이를 단정적으로 말하고 다음 단계로 넘어갈 수 있습니다. 하지만 접근할 수 없다면 에이전트가 할 수 있는 최선은 확답을 피하는 것(hedging)뿐이며, 이러한 모호함은 토큰(token)과 신뢰를 모두 소모합니다.
그리고 이것은 OLAP에만 국한된 문제가 아닙니다. 에이전트는 두 계층을 모두 다룹니다. OLTP에서는 스키마를 편집하고, 마이그레이션을 작성하며, tenant_id 가드(guard)에 대해 추론합니다. OLAP에서는 dbt 모델을 변경하고, 파티션 명세(partition spec)를 읽으며, 스냅샷(snapshot)이 앞으로 진행(roll forward)되었는지 확인합니다. 어떤 경우든 동일한 질문이 결정적인 역할을 합니다. '에이전트가 수행하려는 작업에 대한 증거에 도달할 수 있는가?' 에이전트를 위한 가독성(Legibility-for-agents)은 웨어하우스의 기능이 아니라, 플랫폼 전체에 걸쳐 있는 하나의 속성입니다.
제가 다른 모든 곳에서도 취하는 것과 같은 방식입니다
"숨겨진 상태의 표면적을 줄이는 것"을 목표로 삼게 되자, 제가 단순히 테이블 수준뿐만 아니라 아키텍처(architecture) 수준에서도 이미 이 작업을 수행하고 있다는 사실을 깨달았습니다.
제가 작업하는 플랫폼은 세 가지 종류의 상태(state)를 의도적으로 분리하여 유지합니다. Postgres의 영구적인 관계형 진실(durable relational truth), DynamoDB의 휘발성 상태(hot disposable state), 그리고 사용자별 SQLite의 개인적인 로컬 작업(private local work)이 그것입니다. 그리고 제가 가장 주의를 기울이는 것은 특정 저장소 자체가 아니라, 바로 그들 사이의 교차점(crossings) 입니다.
OLTP (write / update) OLAP (read / aggregate)
┌───────────────────────────┐ ┌──────────────────────┐
│ control-db (Postgres) │ │ Athena / Iceberg │
...
여러 교차점을 셀 수 있는 몇 개로 좁히고 Postgres를 통해 분석 레이크에 도달하는 경로를 단일 경로로 만드는 목적은 오픈 메타데이터를 선호하는 것과 같은 지점입니다. 계산되지 않은 모든 경로는 숨겨진 상태(hidden state)입니다. 즉, 시스템이 코드에서 결코 설명하지 않은 형태로 프로덕션 환경에서 참일 수 있는 방식이죠. 제가 머릿속으로 기억할 수 있는 세 개의 명명된 브리지와, 아무리 에이전트라도 할 수 없는 스무 개의 우발적인 경로가 있습니다.
두 가지 규모에서 동일한 원칙이 적용됩니다: 교차점과 상태를 가시적으로 만들어(legible), 코드가 시스템을 계속 설명하도록 하는 것입니다. 이 패턴이 단일 테이블부터 전체 컨트롤-플레인-투-레이크 경계까지 재현된다는 것은, 솔직히 말해 제가 신뢰하는 가장 큰 이유 중 하나인데—물론 이것은 죽을힘을 다해 변호할 논문이라기보다는 느슨하게 붙잡고 있는 직감에 가깝습니다.
그리고 바로 이 지점에서 저는 의도적으로 실제 예산을 쓰기 시작했습니다. OLTP와 OLAP를 에이전트가 이해할 수 있도록(legible to an agent) 만드는 것—선언된 스키마, 도달 가능한 메타데이터, 계산된 교차점, 에이전트가 실제로 쿼리할 수 있도록 허용된 시스템 뷰—은 예전에는 시간이 있을 때 하는 깔끔함, 즉 '있으면 좋은(nice-to-have)' 기능으로 여겨졌습니다. Claude Code와 Codex 같은 도구들이 실제 편집 작업을 더 많이 수행하면서, 이것은 깔끔함처럼 느껴지기보다는 팀의 가장 생산적인 작업자에게 건네주는 인터페이스처럼 느껴지기 시작했습니다. 하나의 팩트가 접근 가능하도록 만드는 데 들인 한 시간은 제가 반복적으로 프롬프트에 '조심하세요, 이것을 확인하세요'라고 쓰는 시간을 아껴줍니다—그리고 그 보상은 에이전트가 실행될 때마다 복리 효과를 냅니다. 그래서 저는 에이전트의 가시성(agent-legibility)을 테스트나 타입처럼 구축할 만한 인프라로 취급하며 투자합니다. 마지막에 적용하는 광택 작업이 아니라 말이죠. 이것이 완전히 사라질지는 에이전트가 실제로 얼마나 많은 작업을 하는지에 달려 있습니다만, 제 경우에 이미 그럴 가치가 있습니다.
제가 직접 제기할 반론들
- "그러니까 Iceberg가 Redshift보다 가독성이 더 좋다는 말씀이시군요." 아닙니다. 제 말은 Iceberg가 더 많은 상태(state)를 외부화(externalize)하며, 이것이 가독성을 유지하기 위한 규율 비용(discipline cost)을 낮춰준다는 뜻입니다. 규율이 잡힌 Redshift 환경은 언제나 부주의한 Iceberg 환경을 이깁니다. 90%의 요소가 지배적이며, 엔진은 단지 환율을 설정할 뿐입니다.
- "오픈 메타데이터(Open metadata)라고 해서 숨겨진 상태(hidden state)가 아예 없는 것은 아닙니다." 맞습니다. 컴팩션 (Compaction), 작은 파일 정리 (small-file cleanup), 유지보수 작업 (maintenance jobs) 등 Iceberg에도 운영상의 상태 (operational state)가 존재합니다. 다만 그 규모가 더 작고 외부에서 관찰 가능할 뿐, 존재하지 않는 것이 아닙니다. 만약 제가 "숨겨진 상태가 없다"고 암시했다면, 그것은 과한 주장입니다.
- "Athena/Iceberg도 나름의 비용(tax)이 따릅니다." 그렇습니다. 그리고 그것은 S3 일관성(consistency)에 관한 문제가 아닙니다 (현재 오브젝트 스토리지(object storage)는 강력한 일관성을 제공합니다). 그것은 동시 쓰기 충돌 (concurrent-write conflicts), 카탈로그 커밋 의미론 (catalog commit semantics), 작은 파일 (small files), 컴팩션 (compaction), 스냅샷 만료 (snapshot expiration), 그리고 고아 파일 정리 (orphan-file cleanup)에 관한 문제입니다. Athena 역시 Iceberg 테이블에 대해
OPTIMIZE/VACUUM작업이 필요합니다. 가독성은 공짜가 아닙니다. 저는 한 세트의 런타임(runtime) 관련 우려 사항을 다른 세트로 교환하고 있는 것이며, 저는 그 교환이 몰래 숨겨지는 것이 아니라 명확하게 명시되기를 원합니다. - "이것은 그저 데이터 웨어하우스(warehouse) 대 레이크하우스(lakehouse) 구도를 가장한 의견일 뿐입니다." 부분적으로는 그렇습니다. 하지만 제가 관심을 두는 축은 성능이나 비용이 아닙니다. 그것은 선언된 것(declared)과 실현된 것(realized) 사이의 간극의 크기입니다. 이 둘은 별개의 논점이며, 저는 제 주장을 뒷받침하기 위해 대중적인 논리에 기대고 싶지 않습니다.
결론적으로 제가 도달한 지점
제가 계속해서 되풀이하는 설계 목표는 작습니다. 코드가 말하는 것과 런타임(runtime)에서 실제로 일어나는 일 사이의 거리를 좁히는 것입니다. 왜냐하면 그 거리야말로 인간과 에이전트(agents)가 추측을 강요받게 되는 바로 그 지점이기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기