메이크파일, CloudFormation, GitHub Actions처럼 ‘데이터로 된 DSL’이 커지면 결국 한 단계 위의 코드가 필요해진다. 반복·추상화·조건·타입 안전성을 위해 애초에 코드를 쓰게 하자.
나는 Makefile을 많이 쓴다. 나는 Make를 커맨드 러너로 쓰기보다는 작은 프로젝트를 위한 임시방편 빌드 시스템으로 쓴다. 보통은 마크다운 문서와 그 의존성을 컴파일하는 데 사용한다. 예를 들면 이렇게:

그리고 위 그래프는 다음의 아주 단순한 Makefile로 생성됐다:
graph.png: graph.dot
dot -Tpng $< -o $@
clean:
rm -f graph.png
(나는 자동 변수 문법을 플래시카드로 만들기 전까지는 도저히 외울 수가 없었다.)
이건 규칙을 거의 손으로 직접 쓸 수 있는 단순한 프로젝트에는 잘 맞는다. 하지만 추상화의 천장이 매우 낮다. 거의 동일한 규칙이 잔뜩 생기면, 예를 들어:
a.png: a.csv plot.py
python plot.py $< $@
b.png: b.csv plot.py
python plot.py $< $@
c.png: c.csv plot.py
python plot.py $< $@
패턴 매칭을 써서 이를 “규칙 스키마(rule schema)”로, 공리 스키마(axiom schemata)에 비유하자면 그런 형태로 묶을 수 있다:
%.png: %.csv plot.py
python plot.py $< $@
이건 역방향으로 동작한다: 빌드 그래프 안에서 어떤 것이 다음에 매칭되는 타깃에 의존하면
plaintext%.png
Make는 이에 해당하는
plaintext.csv
파일에 대한 의존성을 갖는 규칙 인스턴스를 합성한다.
하지만 패턴 매칭 역시 매우 제한적이다. 최근 나는 파이썬 스크립트 몇 개로 나만의 플레인 텍스트 회계(plain-text accounting) 솔루션을 만들고 있다. 작업 중 하나는 2019–2024년 은행 거래 내역 CSV를 읽어서, 이후 처리를 병렬화하기 위해 연-월마다 하나씩 TOML 파일로 분리하는 것이다. 그러면 규칙은 대략 이런 식이 될 것이다:
makefileledger/2019-08.toml: inputs/checkbook_pro_export.csv uv run import_from_checkbook.py --year=2019 --month=8 ledger/2019-09.toml: inputs/checkbook_pro_export.csv uv run import_from_checkbook.py --year=2019 --month=9 # ...
나는 결국 완전한 Makefile을 생성해 주는 파이썬 스크립트를 써야 했다. Makefile은 코드처럼 보이지만 데이터다. Make 엔진이 필요할 때마다(on-demand) 실행하는 아주 작은 셸 조각들의 컨테이너 포맷일 뿐이다. 그리고 Make는 규모가 커지면 버티지 못하기 때문에, 복잡한 작업을 하려면 Makefile을 생성하기 위해 ‘진짜’ 프로그래밍 언어를 꺼내 들어야 한다.
대신 이렇게 할 수 있으면 좋겠다. 예컨대
plaintextmake.py
파일 하나에 다음 같은 걸 쓸 수 있다면:
from whatever import *
g = BuildGraph()
EXPORT: str = "inputs/checkbook_pro_export.csv"
# 내가 은행 거래 CSV를 가지고 있는 (연, 월) 쌍.
year_months: list[tuple[int, int]] = [
(y, m) for y in range(2019, 2026) for m in range(1, 13)
]
# 각 연-월의 거래를 별도의 원장(ledger)으로 가져온다.
for year, month in year_months:
ledger_path: str = f"ledger/{year}_{month:02d}.toml"
g.rule(
targets=[ledger_path],
deps=[EXPORT],
fn=lambda: import_from_checkbook(ledger_path, year, month),
)
다행히도 이런 건 이미 있다. 이름은 doit이고, 다만 널리 알려져 있지는 않다.
많은 것들이 Makefile과 비슷하다. 즉, ‘코드가 되어야 하는 데이터’가 많다—한 단계 위로 들어 올려(lift) 코드로 만들어야 한다.
CloudFormation을 생각해 보자. 아무도 그 거대한 YAML 파일을 손으로 쓰는 걸 좋아하지 않는다. 그래서 AWS는 CDK를 도입했는데, 이는 말 그대로 AWS 리소스를 표현하는 클래스들의 라이브러리1일 뿐이다. CDK 프로그램을 실행하면 CloudFormation YAML이 출력되는데, 마치 인프라를 위한 어셈블리 언어처럼 취급된다. 그 결과 타입 안전성, 모듈성, 추상화, 조건문과 루프 같은 것들을 전부 공짜로 얻게 된다.
GitHub Actions도 생각해 보자. 워크플로우-잡-스텝 트리를 손으로 쓰는 대신, 푸시 시 실행되는 파이썬 스크립트 하나를 두고 그 출력이 ‘어셈블리로서의 GitHub Actions YAML’이 된다면 우리는 얼마나 더 나아질까? 예를 들면 이렇게 쓸 수 있을 것이다:
from ga import *
from checkout_action import CheckoutAction
from rust_action import RustSetupAction
# 각 커밋마다 실행되는 워크플로우를 정의한다.
commit_workflow = Workflow(
name="commit",
test=lambda ev: isinstance(ev, CommitEvent),
jobs=[
# 린트 잡.
Job(
name="lint",
steps=[
Step(
name="check out",
run=CheckoutAction(),
),
Step(
name="set up Rust and Cargo",
run=RustSetupAction(),
),
Step(
name="run cargo fmt",
run=Shell(["cargo", "fmt", "--check"])
)
]
)
]
)
여기서 액션은 CI 스크립트가 의존하는 평범한 파이썬 라이브러리에 불과할 것이다. 다시 말해: 조건, 루프, 추상화, 타입 안전성—언어로 설계된 언어를 사용한다는 사실만으로 이 모든 것을 공짜로 얻게 된다. 천천히 자라서 결국 설계가 엉망인 DSL이 되어 버리는 데이터 교환 언어를 쓰는 대신에 말이다.
왜 우리는 반복해서 이런 결말에 도달할까? 정적 데이터는 코드보다 안전성/정적 분석 측면에서 더 나은 속성을 가진다. 하지만 사람들이 이런 시스템을 설계할 때 그게 가장 앞에 있는 동기라고는 생각하지 않는다. 게다가 CDK처럼 코드를 써서 데이터를 생성하면 그와 똑같은 속성을 얻을 수 있다. 오히려 어떤 사람들은 데이터 포맷 안에 작은 DSL을 만드는 게 귀엽고 영리하다고 생각하는 것 같다. 동적 솔루션 대신 “단순한”, 정적 솔루션으로도 어떻게든 해낼 수 있다는 사실이 자랑스러운 것이다.
새 CI 시스템/IaC 플랫폼/Make 대체제를 만들고 있다면: 제발 워크플로우/인프라/빌드 그래프를 동적으로 생성하는 코드를 내가 쓸 수 있게 해 달라.
2026년 1월 31일 게시 이전: Claude에게 텍스트 어드벤처를 하게 하기 다음: 없음
© 2014 – 2026 Fernando Borretti