코드가 커질수록 “이 기능이 왜 이렇게 동작하지?“를 알려면 파일 5개를 열어봐야 한다. 설계 문서는 3개월 전에 작성된 채로 방치되어 있고, 실제 코드와 일치하는지 아무도 모른다. 주석은 낡았고, 슬랙 스레드는 지워졌으며, 원래 기획자는 퇴사했다.

만약 설계 문서 자체가 실행 가능하고, 코드 대신 그 문서를 유지보수한다면?

CanonCode라는 사이드 프로젝트에서 이 아이디어를 실험해봤다. 실제 프로덕션 수준 프로젝트에 적용한 결과와, 그 과정에서 마주친 현실적인 문제들을 기록한다.


아이디어: 법률 체계로 소프트웨어를 거버넌스한다

법률 시스템에서 영감을 받았다:

법률소프트웨어
헌법프로젝트 원칙 (모바일 퍼스트, 오프라인 지원 등)
법률(Acts)기능 아키텍처 (QA 공고 생성, 결제 플로우)
규칙(Rules)상호작용 로직 (유효성 검사, 상태 전환)
부칙(Appendices)데이터 스키마, API 명세
판례(Case Law)예외 처리 (잔액 부족, 동시 결제 충돌)

하위 법률은 상위 법률과 모순될 수 없다. CanonCode의 린터가 자동으로 위반을 감지한다.

이 아이디어가 참신하게 보이는 이유는 단순하다. 소프트웨어 개발에서 설계와 구현의 괴리는 거의 모든 팀이 겪는 문제이지만, 대부분의 해결책은 “문서를 잘 쓰자"는 수준에 머문다. CanonCode는 문서와 코드를 연결하는 것이 아니라, 문서를 코드보다 상위에 두는 구조를 시도한다.


실험 대상: LaunchCrew

LaunchCrew는 내가 만들고 있는 C2C QA 매칭 플랫폼이다:

  • 개발자(Maker)가 QA 공고를 올림
  • 테스터(Hunter)가 지원 → 수락 → 매일 테스팅 증빙 제출
  • 완료 시 에스크로된 포인트가 자동 지급

기술 스택: Rails 8 + Inertia.js + Svelte 5 + Flutter

이 프로젝트를 선택한 이유는 비즈니스 로직의 복잡도가 실제로 꽤 높기 때문이다. 에스크로 결제, 상태 머신, 증빙 검증, 포인트 정산, 예외 처리까지 다양한 도메인이 얽혀 있다. 핵심 비즈니스 로직이 모델, 컨트롤러, 서비스, UI 컴포넌트에 걸쳐 40개 이상의 파일, 2,800줄 이상에 분산되어 있다.

새로운 개발자가 합류한다면 어디서부터 읽어야 할까? qa_posts_controller.rb? wallet.rb? escrow_service.rb? 어떤 파일이 “진실"인지조차 불명확하다.


변환 결과

전체 비교

구분.lex 명세실제 코드비율
헌법 (프로젝트 원칙)30줄~450줄15x
법률 (기능 로직)50줄~1,230줄24.6x
규칙 (유효성 검사)12줄~145줄12x
부칙 (참조 데이터)40줄~200줄5x
판례 (예외 처리)25줄~150줄6x
합계~160줄~2,800줄17.5x

17.5배 압축이라는 숫자 자체보다 중요한 것은, 160줄이 2,800줄의 “의도"를 담고 있다는 점이다. 코드는 구현 방식을 담지만, .lex는 왜 그렇게 구현되어야 하는지를 담는다.

에스크로 결제 예시: 명세 vs 코드

이 차이를 가장 극명하게 보여주는 예시가 에스크로 결제 로직이다.

.lex 명세 (8줄):

{
  "id": "CL-001-2",
  "content": "point 타입 공고 시 points_per_person × recruits_count 만큼 즉시 에스크로한다."
},
{
  "id": "CL-001-3",
  "content": "에스크로 실패 시 공고 생성을 롤백한다."
}

실제 코드 (4개 파일에 걸쳐 ~200줄):

# qa_posts_controller.rb
def create
  ActiveRecord::Base.transaction do
    @post = current_user.qa_posts.build(qa_post_params)
    escrow_amount = @post.points_per_person * @post.recruits_count
    wallet = current_user.wallet.lock!
    raise InsufficientBalanceError if wallet.balance < escrow_amount
    wallet.update!(balance: wallet.balance - escrow_amount,
                   escrowed: wallet.escrowed + escrow_amount)
    WalletTransaction.create!(wallet: wallet, transaction_type: :escrow,
                              amount: escrow_amount, ...)
    @post.save!
    QaProject.create!(qa_post: @post, developer: current_user, ...)
  end
rescue InsufficientBalanceError
  render json: { error: "포인트 충전이 필요합니다" }, status: 422
end

2줄의 명세가 컨트롤러, 서비스, 모델, 마이그레이션에 걸친 200줄을 거버넌스한다. 코드를 읽으면 wallet.lock!, ActiveRecord::Base.transaction 같은 구현 디테일이 보이지만, “왜 에스크로가 필요한가"는 코드 어디에도 명시되어 있지 않다. 그 이유는 .lex에만 있다.


어떤 점이 좋았나

1. 새 팀원 온보딩 속도

.lex 파일 하나를 읽으면 LaunchCrew의 전체 비즈니스 로직을 10분 안에 파악할 수 있다. 코드베이스를 읽으려면 며칠이 걸린다.

실제로 이 파일을 읽고 난 뒤 “에스크로 정산 로직은 어디서 처리해요?“라는 질문이 사라진다. .lexCL-001-2가 있고, 그게 ACT-003을 참조하며, ACT-003의 섹션 5가 정산 흐름을 정의한다. 계층이 명확하다.

2. 예외 처리의 추적성

코드에서 예외 처리는 catch 블록에 숨어 있다. “왜 이 로직이 있지?” → git blame → 슬랙 히스토리 → 원래 기획서…라는 고고학적 탐험이 필요하다.

.lex에서는 모든 예외가 **판례(Case Law)**로 기록되고, 어떤 조항과 연결되는지 명시한다:

CASE-002: 테스터 중도 포기
  상황: 테스터가 테스팅 도중 포기
  판결: 해당 테스터 에스크로만 개발자에게 반환
  관련조항: ACT-003 CL-005-3

이 포맷의 핵심은 “판결(Ruling)“이 있다는 것이다. 단순히 어떻게 처리하는지가 아니라, 왜 그렇게 처리하기로 결정했는지가 기록된다. 6개월 뒤에 코드를 보는 나 자신에게 보내는 편지다.

3. 아키텍처 위반 감지

헌법 제3조에 “balance >= 0"이 명시되어 있다면, 이를 위반하는 코드 변경은 린터가 잡을 수 있다. 코드 리뷰에서 “이거 잔액 마이너스 될 수 있는데요?“를 사람이 잡을 필요가 없다.

현재 Rust로 작성된 lex_engine이 이 역할을 담당한다. 조항 간 의존성 그래프를 빌드하고, 새 조항이 기존 헌법 원칙과 충돌하는지 정적 분석을 수행한다. 아직 완전하지는 않지만, 방향성은 명확하다.

4. AI 코드 생성과의 시너지

이 부분은 실험 중에 예상치 못하게 발견한 장점이다. LLM에게 “에스크로 결제 구현해줘"보다 “CL-001-2, CL-001-3 조항을 Rails로 구현해줘"가 훨씬 정확한 결과를 낸다. 컨텍스트가 명세로 정리되어 있으니, AI가 추측할 필요가 없다.


디버깅 실전: 동시 결제 충돌 문제

CanonCode를 도입한 직후 실제로 마주친 버그가 있다. 동일한 공고에 두 테스터가 동시에 지원할 때 에스크로가 두 번 차감되는 문제였다.

코드만 있었다면 추적 경로가 이랬을 것이다:

  1. wallet.rbdeduct_escrow 메서드 확인
  2. 호출 지점 검색 (grep -r "deduct_escrow")
  3. 트랜잭션 경계 확인
  4. 락 전략 확인

.lex가 있었기 때문에 먼저 명세를 확인했다:

CASE-005: 동시 지원 충돌
  상황: 모집 인원 마감 직전 복수의 지원 동시 도달
  판결: 선착순, 나중 지원은 422 반환
  관련조항: ACT-003 CL-007-1

판례에 CL-007-1이 참조되어 있었고, 해당 조항에는 optimistic locking이 명시되어 있었다. 코드에서 lock_version이 누락된 것이 즉시 확인됐다. 명세가 디버깅의 지도가 된 것이다.

이 경험이 CanonCode를 계속 발전시키게 된 계기다. 단순한 문서화 도구가 아니라, 런타임 버그를 설계 수준에서 예방하는 도구가 될 수 있다는 가능성을 봤다.


솔직한 한계

  1. 코드를 대체하지 않는다: .lex는 “무엇을"을 정의하지, “어떻게"를 정의하지 않는다. 여전히 코드를 작성해야 한다. “명세만 있으면 코드가 자동으로 나온다"는 건 아직 먼 이야기다.
  2. JSON이 장황하다: 마크다운이나 YAML이 더 간결할 수 있다. 현재 포맷은 파싱 편의성을 위해 선택했지만, 사람이 직접 읽고 쓰기엔 불편하다.
  3. 자동 코드 생성은 아직: CodeSpeak처럼 명세에서 코드를 자동 생성하는 기능은 아직 계획 단계다. LLM 연동 실험은 하고 있지만 신뢰할 만한 수준은 아니다.
  4. 작은 프로젝트에는 오버킬: 프로토타입이나 해커톤 프로젝트에는 불필요하다. 비즈니스 로직이 단순하면 명세의 이점이 거의 없다.
  5. 팀 전체의 동의가 필요하다: 혼자 쓰면 그냥 또 다른 문서다. 코드 리뷰 프로세스에 .lex 변경을 포함시키지 않으면, 명세는 곧 코드와 괴리된다.

누구에게 유용한가

  • 규제 산업 (금융, 의료): 감사 추적이 필요한 곳. 모든 설계 결정이 번호 매겨진 조항으로 추적 가능.
  • 5인 이상 팀: 설계 문서가 코드와 괴리되는 문제를 해결.
  • SI 프로젝트: 요구사항 → 구현 매핑이 필수인 곳.
  • 장기 프로덕트: 아키텍처가 시간이 지나면서 침식되는 것을 방지.

반대로, MVP를 빠르게 검증하는 단계나 팀이 2-3명인 경우에는 투자 대비 효과가 낮다. 도구의 이점은 시간과 복잡도에 비례한다.


직접 해보기

git clone https://github.com/seunghan91/canoncode.git
cd canoncode

# LaunchCrew 예제 확인
cat examples/launchcrew-qa-matching.lex | python3 -m json.tool | head -50

# Rust 엔진 빌드 후 검증
cd lib/lex_engine && cargo build --release
./target/release/lex_cli info -f ../../examples/launchcrew-qa-matching.lex

전체 코드: github.com/seunghan91/canoncode


다음 단계

  1. .lex → 코드 자동 생성 (LLM 연동)
  2. 코드 → .lex 역공학 자동화
  3. 웹 UI에서 코드와 명세 나란히 비교 뷰
  4. npm 패키지 배포 (npx canoncode init my-project)

Key Takeaways

  • 2,800줄의 코드와 160줄의 명세는 같은 시스템을 기술하지만, 담고 있는 정보의 종류가 다르다. 코드는 “어떻게”, 명세는 “왜"를 담는다.
  • 판례(Case Law) 포맷은 예외 처리 로직의 맥락을 보존하는 가장 효과적인 방법 중 하나다.
  • 명세 기반 개발의 가장 즉각적인 이점은 온보딩과 디버깅 속도다. 아키텍처 위반 감지는 그 다음이다.
  • 도구의 가치는 팀 전체가 프로세스에 포함시킬 때 발현된다. 혼자 쓰는 명세는 결국 또 하나의 방치된 문서가 된다.

코드를 유지보수하는 게 아니라, 법률을 유지보수하는 개발. 아직 실험 단계지만, 가능성은 느껴진다.