워크플로우 표현식 엔진을 직접 만든 이유 — 그리고 후회한 적 없는 결정
GitHub Actions 표현식의 "거의 비슷한" 80% 를 받아들이지 못해서 PEG 파서를 직접 짠 6개월. 무엇을 깎아냈고, 무엇을 추가했고, 후속 비용이 어떻게 됐는지.
CollabOps 베타 첫 주, 한 고객사 엔지니어가 슬랙으로 한 줄을 보냈다.
"왜
${{ steps.x.outputs.y }}가 안 되나요? GitHub 에선 됐는데."
그 한 줄이 그날 내 일정의 절반을 잡아먹었고, 그 주의 모든 결정 을 바꿨다. 우리는 그날부터 표현식 엔진을 직접 짜기 시작했다.
왜 빌려 쓰지 않았나
처음엔 빌려 쓸 생각이었다. 후보 셋:
- GitHub Actions 의 expression 라이브러리 포팅 — 라이선스(MIT) 는 OK 인데, Go 로 짠 걸 우리 Rust 런타임에 들고 들어오는 게 무거웠다.
- Tengo / Starlark / CEL 같은 임베더블 언어 — 너무 많다. 우리는 표현식 80개 함수 를 원하는 게 아니라, 6개 면 충분했다.
- JSON-e / yq — DSL 에 가깝지 산술/조건이 약했다.
그래서 PEG (Parsing Expression Grammar) 로 직접 짜는 결정. 첫 prototype 이 3일 만에 굴러갔다. 그게 함정이었다.
첫 함정 — "거의 비슷한" 80%
GitHub Actions 표현식 문법을 완벽 복제 할 생각은 없었다. 하지만 대부분의 기존 워크플로우가 그대로 동작 해야 한다고 생각했다. 이게 함정이었다.
# 우리 첫 버전에서 안 됐던 것들
steps.build.outputs['package-name'] # bracket access
toJson(github.event) # nested function call with object literal
contains(fromJson(steps.x.outputs), 'a') # higher-order각각이 다른 파서 결정 을 요구했다. bracket access 는 lexer rule, nested function 은 grammar 우선순위, higher-order 는 type system. 우리가 80% 호환을 광고 하지 않았는데도, 고객은 기존 워크플로우 70개 중 8개가 안 된다 는 보고서를 보냈다.
8/70 = 11.4%. 그게 우리가 자처한 마이그레이션 비용 이었다.
그래서 뭘 했나
세 가지 결정.
첫째, 호환성을 목표가 아닌 부산물 로 다시 정의했다. 우리는 우리 표현식 언어 를 만들고 있다. GitHub 호환은 우리 언어가 그것의 superset 이 되도록 짜면 자연히 따라온다. 이 한 줄의 멘탈 모델 변경이 6개월의 작업 방향을 정렬시켰다.
둘째, 함수 화이트리스트를 공개 했다. v1 에서는 6 개 (contains, startsWith, endsWith, format, join, toJson). v2 에서 8 개 (fromJson, hashFiles 추가). 함수 추가 는 토론을 거치고, 함수 제거 는 deprecation 6개월 후. 이게 조용히 깨지는 것을 막는다.
셋째, 타입 시스템을 가볍게 박았다. GitHub 표현식은 동적 이다. 런타임에 깨진다. 우리는 워크플로우 정의 시점에 타입 검사 를 강제했다. 이게 가장 큰 차별점이 됐다 — 빌드를 돌려보기 전에 표현식 오류를 안다.
사용자가 작성: ${{ contains(steps.x.outputs.foo, 42) }}
↑
타입 검사 출력: contains() 의 두 번째 인자는 string. 42 (number) 가 들어왔음.
steps.x.outputs.foo 가 항상 string 인지 확인 필요.6개월 후의 비용 표
직접 짠 결정의 진짜 비용을 표로:
| 항목 | 직접 짤 때 | 빌려 썼을 때 (가정) |
|---|---|---|
| 초기 6주 | 100% (PEG, lexer, eval) | 30% (적응 코드) |
| 첫 고객 호환성 이슈 | 우리 책임 — 빠르게 수정 | 외부 라이브러리 책임 — 우회 또는 PR |
| 차별 기능 (타입 검사) | 추가 100시간 | 불가능에 가까움 |
| 6개월 누적 LOC | 2,800 (parser 포함) | 약 800 (어댑터) |
| 6개월 누적 버그 보고 | 11건 | 측정 불가 (외부 라이브러리 버그도 우리에게 옴) |
빌려 썼을 때보다 3배 코드 를 짰다. 하지만 그 3배가 우리가 통제 가능한 코드 다. 외부 라이브러리의 우리가 못 고치는 버그 가 7개월차에 한 번이라도 터지면 그 결정은 정당화된다.
안 했으면 어땠을까
이 글을 쓰는 시점, 6개월간 단 한 번 도 그 결정을 후회하지 않았다. 후회의 단서가 있었다면 — 예를 들어 외부 라이브러리에 우리가 못 박는 기능을 매번 사람이 우회 하는 패턴이 보였다면 — 다시 검토했을 거다. 그런 단서는 없었다.
다만 한 가지는 인정한다 — 처음 3일짜리 prototype 의 빠른 동작 이 우리에게 "빨리 끝낸다" 라는 잘못된 인상을 준 시점이 있었다. 진짜 작업은 그 이후 6주 였다.
누구에게 이 결정이 의미 있는가
- 워크플로우 도메인 을 직접 만드는 팀에게 — 표현식 엔진은 거의 항상 비싸 보이지만 사실 가장 싼 결정이다. 외부 라이브러리의 가정과 우리 도메인의 가정 이 어긋나는 순간 발생하는 비용이 누적적으로 더 크다.
- 호환성을 영업 포인트 로 쓰려는 팀에게 — 100% 호환은 기술 결정이 아니라 마케팅 결정 이다. 그 결정의 비용을 알고 들어가야 한다.
다음 글에선 타입 시스템을 어떻게 가볍게 짰는지, 그리고 MCP 의 도구 스키마와 어떻게 정렬 시켰는지 얘기한다.
비슷한 글
기획자가 코드를 배포하면, 장애는 누가 책임질까
AI로 제품 담당자가 직접 코드를 만들고 여러 에이전트가 동시에 작업하는 조직에서, 운영에 나가는 변경을 누가 무엇으로 확인해야 하는지. NIST SSDF, SLSA provenance, Google SRE 포스트모템 문화를 근거로 작성자와 분리된 검증·승인·복구의 역할 설계를 정리했다.
백재민
에이전트 시대의 Eclipse 플러그인 — 작업 컨텍스트는 20년 전에도 문제였다
Mylyn 이 2000년대에 풀려던 문제와 지금 에이전트 컨텍스트 문제는 같습니다. 달라진 건 그 컨텍스트를 읽는 쪽에 사람만 있는 게 아니라는 점입니다.
김영상
에이전트 시대의 VS Code 확장 — 개발자는 에디터에서 무엇을 보는가
코드 작성 비용이 무너진 뒤 개발자의 시간은 판단과 승인으로 옮겨갔습니다. 그 일들이 왜 에디터 밖에 있으면 안 되는지, VS Code 확장을 만들며 내린 선택들.
이승백