전체 검색

글, 태그와 카테고리를 검색하고 바로 이동해요

컨벤션은 문서가 아니라 실패하는 빌드다 — 규칙을 기계로 강제하는 4개 층

규칙 문서 32개, 4,500줄. 아무도 다 기억 못 합니다. 그래서 기억을 포기하고 기계에 맡겼어요. 훅·정적 검사·아키텍처 테스트·산출물 검증으로 쌓은 네 개 층과, 각 층이 무엇을 잡고 무엇을 못 잡는지 적었습니다.

2026. 07. 14. 18:03:447분 읽기프로젝트/데이터 플랫폼
#정적 분석#CI#코드 컨벤션
조회 39
규칙 검사 게이트 썸네일

정성껏 쓴 규칙 문서가 있어요. 그리고 그 규칙이 깨진 코드가 있죠. 문서를 쓴 사람도 저고, 규칙을 어긴 사람도 저였습니다.

사람은 원래 그래요. 그래서 이 시리즈의 제목이 "규칙은 기계가 지킨다"입니다.

저희 규칙 문서는 32개 파일에 4,500줄쯤 됩니다. 아키텍처 경계, 패키지 의존 방향, 에러 처리, 빌드와 배포 계약, 작업 유형별 절차까지요.

누가 그걸 다 기억하냐고요? 아무도 못 합니다. 저도 못 해요. 그래서 기억을 포기하고 기계에 맡겼습니다.

왜 문서가 안 통하는지부터

문서가 나쁘다는 게 아니에요. 문서는 "왜"를 설명하는 데는 최고입니다. 그런데 "지금 이 코드가 규칙을 어겼는가"를 판정하지는 못해요.

상황문서가 하는 일실제로 필요한 것
새 코드를 짤 때참고할 수 있음대부분 안 읽음
리뷰할 때근거로 인용 가능리뷰어가 규칙을 기억해야 함
규칙이 바뀔 때고칠 수 있음기존 코드가 어긴 걸 못 찾음
규칙을 어겼을 때아무 일도 안 일어남막혀야 함

마지막 줄이 핵심이에요. 어겨도 아무 일이 안 일어나는 규칙은 규칙이 아니라 권고입니다. 그리고 권고는 일정이 급해지면 제일 먼저 버려져요.

(그리고 그 판단이 대체로 합리적이라는 게 문제예요. 급할 때 문서를 지키는 건 비합리적이거든요.)

그래서 네 개 층을 쌓았습니다

한 가지 검사로 다 잡으려고 하지 않았어요. 잡을 수 있는 시점이 다르거든요.

언제무엇을 잡나
1. 정적 검사저장할 때문법·패턴·금지된 표현
2. 훅push할 때올리기 전 마지막 방어
3. 아키텍처 테스트CI에서의존 방향·경계 위반
4. 산출물 검증빌드 뒤결과물이 계약과 맞는가

아래로 갈수록 늦게 잡히지만 더 깊은 걸 잡아요. 위로 갈수록 빠르지만 표면만 봅니다.

1층 — 정적 검사

제일 흔한 층이죠. 다만 여기서 잡을 수 있는 것과 없는 것을 구분하는 게 중요했어요.

잡을 수 있음:
  금지된 import 경로
  금지된 API 사용
  네이밍 규칙
  파일 위치 규칙

못 잡음:
  "이 패키지가 저 패키지를 알면 안 된다" 같은 그래프 규칙
  런타임에만 드러나는 결합

그래서 정적 검사는 "빠르게 잡히는 것들"만 맡겼습니다. 여기에 그래프 규칙을 욱여넣으면 규칙 파일이 괴물이 돼요.

2층 — push 훅

여기서 결정 하나가 있었어요. 커밋 훅이냐 push 훅이냐.

커밋마다 검사를 돌리면 확실한데, 커밋이 느려집니다. 그리고 커밋은 자주 하는 행위라 느려지면 사람들이 커밋을 덜 쪼개요. 그건 더 나쁜 결과고요.

그래서 push 시점으로 뒀습니다. 커밋은 마음껏 쪼개되, 남에게 보내기 전에 한 번 걸러지는 거죠.

"prepare": "node build/scripts/setup-git-hooks.js"

그리고 훅 설치를 의존성 설치 단계에 걸었어요. 새로 합류한 사람이 별도 설정을 안 해도 자동으로 걸립니다. 훅은 설치를 잊으면 없는 것과 같으니까요.

3층 — 아키텍처 테스트

여기가 이 체계의 핵심이에요. 의존 방향 같은 건 파일 하나만 봐서는 알 수 없거든요.

"audit:package-dependencies:check"  # 패키지 간 의존 방향 검사
"manifest:check"                    # 선언과 실제가 일치하는가
"shared-runtime:check"              # 공유 런타임 계약이 최신인가
"test:architecture"                 # 조립 구조 회귀 테스트

두 번째 줄을 설명할게요. 저희는 각 패키지가 자기 기여를 선언하고, 그 선언을 모아 코드를 생성합니다. 그런데 선언을 고치고 생성을 안 돌리면 어떻게 될까요?

생성된 파일이 낡은 채로 남아요. 로컬에서는 동작하는데 클린 체크아웃에서는 다른 결과가 나옵니다. 제일 찾기 어려운 종류의 버그죠.

그래서 "다시 생성했을 때 결과가 지금과 같은가"를 검사합니다. 다르면 실패예요. 생성물을 커밋하되 낡지 않게 강제하는 방법입니다.

4층 — 산출물 검증

마지막 층은 빌드가 끝난 뒤에 돕니다. 소스가 아니라 결과물을 봐요.

"artifact:validate"           # 조립된 결과물이 계약과 맞는가
"artifact:evidence:validate"  # 빌드 근거까지 포함해 검증

여기서 잡는 건 앞의 세 층이 원리적으로 못 잡는 것들이에요. 빌드 도구의 동작, 청크 분할 결과, 버전 불일치 같은 것들요.

소스가 다 맞아도 결과물이 틀릴 수 있거든요. 그래서 결과물을 따로 봅니다.

그리고 규칙을 하나 더 뒀어요

층을 여럿 쌓으면 자연히 생기는 문제가 있습니다. 같은 검사가 여러 층에서 중복으로 돌아요.

안전해 보이지만 실제로는 CI 시간만 먹고 아무도 안 읽는 로그를 만듭니다. 그리고 CI가 느려지면 사람들이 CI를 안 기다려요.

그래서 문서에 못 박았습니다. 같은 검사를 여러 단계에서 반복하지 않는다.

개발자가 빠르게 돌려보는 명령이 따로 있는데, 그걸 전체 검증 뒤에 또 돌리지 않아요. 각 층은 자기만 잡을 수 있는 것을 잡습니다.

(중복 검사는 성실함처럼 보이지만, 실제로는 설계를 안 한 흔적이에요.)

예외를 어떻게 다루나

검사를 촘촘히 걸면 반드시 만나는 순간이 있어요. 정당한데 막히는 경우요.

여기서 어떻게 하느냐가 이 체계의 수명을 결정합니다. 잘못 다루면 여러 방향으로 망가져요.

나쁜 대응결과
검사를 끈다한 번 끄면 다시 안 켬
예외 주석을 자유롭게 허용주석이 늘어나며 검사가 무력화
규칙을 느슨하게 바꾼다원래 막으려던 것도 통과
우회 코드를 짠다규칙은 지켜지고 코드는 이상해짐

저희가 고른 방식은 이거예요. 예외를 허용하되 예외 목록을 한 곳에 모으고, 그 목록이 줄어드는 방향으로만 움직이게 하는 것.

그러니까 지금 있는 예외는 인정하되 새 예외는 추가를 어렵게 만듭니다. 그러면 시간이 지날수록 예외가 줄어요.

(예외를 0으로 만드는 건 비현실적이에요. 늘지 않게 만드는 게 현실적인 목표입니다.)

그리고 검사에도 테스트가 필요했어요

의외로 놓치기 쉬운 부분인데, 검사 스크립트 자체가 버그를 가질 수 있습니다.

제일 위험한 건 아무것도 안 잡는 검사예요. 통과하니까 잘 동작하는 줄 알거든요. 실제로는 검사 대상 경로를 잘못 잡아서 파일을 하나도 안 보고 있을 수 있습니다.

"test:scripts": "node --test build/scripts/*.test.js ..."

그래서 검사 스크립트에도 테스트를 붙였어요. 위반 사례를 일부러 만들어 넣고 제대로 실패하는지를 확인합니다.

통과하는 케이스만 테스트하면 반쪽이에요. 검사는 실패시키는 게 일이니까, 실패시키는지를 테스트해야 합니다.

층 사이의 순서도 규칙이에요

마지막으로 순서 이야기를 적을게요. CI에서 검사를 어떤 순서로 돌리느냐도 설계입니다.

1. 의존성 설치 (고정된 버전으로)
2. 정적 검사 · 테스트 · 타입 검사
3. 브라우저 기반 격리 검사
4. 산출물 빌드
5. 산출물 계약 검증

빠르고 싼 것을 앞에, 느리고 비싼 것을 뒤에 뒀어요. 타입 오류가 있는데 브라우저를 띄울 이유가 없거든요.

그리고 1번의 "고정된 버전"이 은근히 중요합니다. 의존성이 매번 조금씩 다르면 검사 결과도 흔들려요. 그러면 사람들이 빨간불을 안 믿기 시작하고, 안 믿는 순간 이 체계 전체가 무의미해집니다.

검사는 정확한 것보다 일관된 게 먼저예요. 가끔 틀리는 검사는 없는 검사보다 나쁩니다.

이 체계의 진짜 이득

처음엔 "실수를 막는 장치"라고 생각했는데, 쓰다 보니 다른 이득이 더 컸어요.

1. 리뷰가 내용에 집중합니다

규칙 위반을 기계가 잡으니까 리뷰에서 그 얘기를 안 해요. "여기 import 방향이 틀렸어요" 대신 "이 로직이 이 위치에 있는 게 맞나요"를 얘기하게 됩니다.

2. 큰 변경이 무섭지 않아요

구조를 갈아엎을 때 무서운 건 "내가 뭘 깼는지 모른다"는 거잖아요. 검사가 촘촘하면 깬 것이 즉시 빨간불로 돌아옵니다.

이 코드베이스가 지금도 큰 구조 변경을 두려움 없이 하는 이유가 여기 있어요.

3. 규칙을 고칠 수 있게 됩니다

의외의 이득이었어요. 규칙이 코드로 있으면 규칙 자체를 리팩터링할 수 있습니다.

문서로만 있으면 규칙을 바꿔도 기존 코드가 따라오는지 알 수 없어요. 검사로 있으면 규칙을 바꾸고 돌려보면 됩니다.

치른 값

비용실제로 겪은 일
CI 시간검증 단계가 늘어남 — 중복 제거로 관리
새 규칙 추가가 무거움문서 한 줄이 아니라 검사 하나를 만들어야 함
오탐 대응정당한 예외가 막힐 때 예외 경로를 설계해야 함
검사 자체의 유지보수검사 코드에도 테스트가 필요함

두 번째 줄이 실제로는 이득이기도 했어요. 검사를 만들 수 없는 규칙은 대개 규칙이 아직 덜 정의된 것이거든요. "적절히 나눈다" 같은 건 검사를 못 만듭니다. 만들려고 하면 무엇이 적절한지 정의하게 돼요.

그래서 결론은

개인 프로젝트든 회사 플랫폼이든 같은 결론에 도달했어요.

컨벤션 문서는 언젠가 잊히고, 잊힌 규칙은 없는 규칙입니다.

규칙을 지키게 만드는 건 의지가 아니라 구조예요. 어기면 빨간불이 들어오는 구조요.

그리고 이 원칙은 이 시리즈의 다음 편에서 극단까지 갑니다. 화면을 1px도 못 바꾸게 만든 안전망 이야기요.


다음 편은 이 철학의 끝판왕이에요. 구조를 통째로 갈아엎으면서 화면은 1px도 못 바꾸게 만든 안전망 — SHA-256 해시가 UI를 지키는 이야기입니다.

시리즈 이어읽기

규칙은 기계가 지킨다

1 / 4

이야기 나누기

반응을 남기고 새 글 소식을 받아보세요.

댓글0
서로를 존중하는 대화를 남겨주세요.

첫 댓글을 남겨 대화를 시작해보세요.

새 글 구독
새 글이 공개되면 이메일로 알려드려요.