DevInsight

개발의 판단과 맥락을 기록하는 곳

Frontend
조회 1약 6분 읽기

TypeScript 설계 부채를 숫자로 드러내고 CI가 대신 막게 하라

타입스크립트 프로젝트에서 CK 메트릭(WMC, DIT, NOC, CBO, RFC, LCOM)을 측정하는 CLI 도구 Artie-lens를 활용해 CI 파이프라인에 설계 품질 게이트를 구축하는 방법을 다룬다. 코드 복잡도와 결합도가 임계치를 넘으면 자동으로 빌드를 실패시켜 기술 부채 누적을 사전에 차단하는 실전 가이드.

DevInsight 편집팀 발행

AI 보조 초안과 편집 검수를 거쳐 발행했습니다.

#TypeScript#CK메트릭#설계품질#CI/CD#코드복잡도#기술부채#GitHubActions#정적분석#객체지향#코드리뷰

코드 리뷰에서 "이 컴포넌트가 좀 비대해진 것 같네요"라는 코멘트를 남긴 적이 있다면, 그건 이미 기술 부채가 형태를 갖추기 시작했다는 신호다. 문제는 그런 감각적 판단이 PR을 통과하는 순간 증발해버린다는 점이다. 리뷰어가 피곤해서, 일정이 밀려서, 혹은 그냥 "다음에 고치자"는 말과 함께 수십 줄의 클래스가 머지되고, 그 클래스는 다시 다른 클래스를 import하며 의존성 그래프를 살찌운다. 설계 품질을 감각이 아니라 숫자로 말하게 만들고, 그 숫자가 임계치를 넘으면 CI가 빌드를 깨뜨리도록 설정하는 것. 바로 그 자동화된 게이트를 구축하는 이야기다.

CK 메트릭은 1994년 Chidamber와 Kemerer가 제안한 객체지향 설계 측정 지표다. 각각 WMC(Weighted Methods per Class), DIT(Depth of Inheritance Tree), NOC(Number of Children), CBO(Coupling Between Objects), RFC(Response For a Class), LCOM(Lack of Cohesion of Methods) 여섯 가지로 구성된다. 발표된 지 30년이 지났지만, 타입스크립트 생태계에서는 이 지표를 정기적으로 측정하는 문화가 거의 자리 잡지 못했다. 린터는 코드 스타일을, 테스트 커버리지는 실행 경로를 검증하지만, 클래스 수준의 응집도나 결합도가 악화되고 있는지는 아무도 모르는 상태로 스프린트가 쌓여간다.

이 여섯 가지 숫자가 실제로 무엇을 말해주는지 간단히 짚어보자. WMC는 클래스에 정의된 메서드의 복잡도 가중 합이다. WMC가 높은 클래스는 유닛 테스트 작성이 어렵고 변경 영향도가 넓다. DIT는 상속 트리에서 루트까지의 깊이인데, 너무 깊으면 작은 부모 클래스 변경이 자식 전체를 흔든다. NOC는 직접 서브클래스의 개수로, 이 값이 높은 클래스를 수정할 때는 추상화가 충분히 잘 분리되어 있는지 의심해봐야 한다. CBO는 얼마나 많은 다른 클래스와 결합되어 있는지를 나타내며, 한 클래스가 열 개 이상의 클래스를 참조한다면 단일 책임 원칙이 깨졌을 가능성이 크다. RFC는 클래스의 퍼블릭 메서드가 호출될 때 실행될 수 있는 메서드의 총 개수로, 디버깅 비용과 직결된다. 마지막으로 LCOM은 필드와 메서드가 얼마나 서로 공유되지 않는지를 측정하는데, 1에 가까울수록 이 클래스는 사실 둘로 쪼개야 하는 상태다.

문제는 이 지표들을 수동으로 측정하는 게 현실적으로 불가능하다는 데 있다. 클래스가 수십 개만 넘어가도 엑셀이나 눈대중으로 CBO나 LCOM을 추적하는 건 무리다. Artie-lens는 바로 이 지점을 겨냥한 CLI 도구다. npx artie-lens 한 줄로 프로젝트 전체 클래스의 CK 메트릭을 계산하고, 사전에 정의한 임계치를 초과하는 항목을 빨간색으로 표시해준다. AST 기반으로 타입스크립트 소스를 직접 파싱하므로 빌드 결과물이나 번들 사이즈와 무관하게 소스 코드의 구조 자체를 측정한다는 점이 핵심이다.

실제 CI 파이프라인에 이 도구를 심는 패턴은 생각보다 단순하다. GitHub Actions를 예로 들면, 린트와 타입 체크 사이에 새로운 스텝 하나를 추가하는 것으로 끝난다. 중요한 건 --max-warnings 같은 단순 플래그가 아니라, 임계치 설정을 별도의 설정 파일로 분리한다는 점이다. 프로젝트 루트에 .artielensrc.json을 두고 각 메트릭의 허용 상한을 명시하면, 팀 내에서 "어느 정도까지 괜찮다"는 기준을 코드 리뷰에서 말싸움할 필요 없이 설정 파일 하나로 협의할 수 있다.

{ "thresholds": { "wmc": 20, "dit": 4, "noc": 5, "cbo": 8, "rfc": 40, "lcom": 0.7 }, "exclude": ["**/*.spec.ts", "**/*.test.ts"] }

여기서 진짜 판단이 필요한 건 숫자 자체보다 "우리 프로젝트에 맞는 임계치가 무엇인가"다. 20이라는 WMC 상한이 작은 유틸리티 클래스에는 과잉 제약일 수 있고, 도메인 로직을 담은 서비스 클래스에는 너무 낮을 수 있다. 그래서 Artie-lens가 제안하는 방식은 초기 릴리스 주기 동안 --baseline 플래그로 현재 코드베이스의 메트릭을 측정한 뒤, 상위 10퍼센타일을 기준선으로 삼고 점진적으로 임계치를 낮춰가는 것이다. 오늘 당장 CBO가 12인 클래스를 8로 낮추라고 강제하는 대신, "새로 작성하는 코드는 CBO 8을 넘지 않는다"는 규칙부터 적용한다. 이렇게 하면 기존 코드의 부채가 늘어나지는 않으면서, 신규 코드에만 품질 기준을 적용하는 소프트 랜딩이 가능하다.

이 지점에서 가장 흔한 실수는 메트릭을 절대적 잣대로 믿는 것이다. WMC가 19인 클래스보다 21인 클래스가 무조건 설계가 나쁘다고 말할 수는 없다. 예컨대 순수 함수의 집합으로 구성된 유틸리티 클래스는 WMC가 30을 넘어도 응집도가 높고 변경 비용이 낮을 수 있다. 반면 메서드 세 개짜리 클래스가 도메인 로직, 네트워크 호출, UI 상태 변경을 동시에 담당한다면 WMC가 10이어도 문제다. 메트릭은 거짓말을 하지 않지만 맥락을 이해하지도 못한다는 점을 항상 인지해야 한다. 이것이 바로 Artie-lens가 --exclude 패턴과 디렉터리별 임계치 오버라이드를 지원하는 이유이기도 하다. 도메인 계층과 인프라 계층에 다른 기준을 적용할 수 있어야 설계 게이트가 팀의 생산성을 억압하지 않는다.

CI에 이 게이트를 처음 도입할 때 팀원들의 반응은 대개 두 갈래로 나뉜다. "드디어 객관적 기준이 생겼다"는 안도와 "빌드가 자꾸 깨지는데 일정이 밀린다"는 불만이다. 후자는 특히 이미 코드베이스에 상당한 부채가 쌓인 상태에서 발생한다. 이때 필요한 건 빌드 실패를 알리는 것과 별개로, 실패한 항목을 정리한 마크다운 리포트를 PR 코멘트로 자동 게시하는 워크플로우다. 개발자는 빌드 로그를 뒤질 필요 없이 PR 페이지에서 바로 어떤 클래스의 어떤 메트릭이 초과했는지 확인할 수 있어야 한다. GitHub Actions의 actions/github-script나 Slack 웹훅과 조합하면, 메트릭 초과를 알림 채널로 보내는 것도 몇 줄이면 된다.

또 하나 놓치기 쉬운 지점은 메트릭의 추세 관리다. 단일 빌드 시점의 WMC가 19로 통과했다고 해서, 지난 3개월간 WMC 평균이 12에서 18로 상승 중인 추세까지 잡아내지는 못한다. 설계 부채는 한 번의 커밋으로 폭발하지 않는다. 매주 조금씩 메서드가 추가되고, 누군가 습관적으로 유틸 클래스에 정적 메서드를 추가하면서 결합도는 서서히 올라간다. Artie-lens에는 --history 플래그가 있어 이전 측정 결과와의 델타를 출력할 수 있는데, 이 데이터를 CI 아티팩트로 주기적으로 저장해두면 시간 축에서의 품질 변화를 한눈에 볼 수 있다. 스프린트 회고 때 "이번 주에 CBO가 가장 많이 증가한 상위 3개 파일" 같은 지표를 공유하는 것만으로도 리팩터링 우선순위를 논의하는 문화가 만들어진다.

실무에서 이 도구를 도입한 팀이 가장 먼저 마주치는 놀라움은 "생각보다 LCOM이 높은 클래스가 많다"는 것이다. 타입스크립트 생태계에서는 클래스 컴포넌트, 서비스 클래스, 리포지토리 패턴 등 여러 패러다임이 공존하는데, 특히 리액트 클래스 컴포넌트에서 상태 필드와 수많은 이벤트 핸들러가 LCOM을 급격히 높이는 경우가 흔하다. 이 발견 자체가 "이 컴포넌트가 너무 많은 책임을 지고 있다"는 신호를 숫자로 번역해주기 때문에, 리팩터링의 필요성을 설득하는 데 예상보다 훨씬 효과적이다. "느낌"이 아니라 "LCOM 0.92"라는 숫자를 보고 나면, 메서드 추출이나 컴포넌트 분리에 관한 논의가 감정적 교착에서 벗어난다.

한편으로 이런 자동화된 게이트를 도입할 때 함께 고려해야 할 것은 예외 처리의 투명성이다. 어떤 이유로든 특정 클래스가 임계치를 초과해야만 하는 상황은 반드시 생긴다. 외부 API 응답을 파싱하는 DTO 클래스가 CBO를 초과하거나, 레거시 마이그레이션 중인 어댑터 클래스가 WMC를 넘어서는 경우다. 이럴 때 주석에 // artie-lens:ignore 같은 억제 코멘트를 허용할 것인지, 아니면 설정 파일에 화이트리스트를 등록할 것인지는 팀의 성숙도에 따라 결정할 문제다. 억제 코멘트는 편리하지만 남용되면 게이트 자체를 무력화하고, 화이트리스트는 통제 가능하지만 PR마다 설정 파일을 수정해야 하는 오버헤드가 있다. 필자가 관찰한 바로는 초기에는 화이트리스트 방식으로 시작해서, 억제 사유가 분명하고 반복적이라면 그때 억제 코멘트 규칙을 팀 협의로 추가하는 패턴이 가장 충돌이 적었다.

기술적으로 한 가지 더 짚고 넘어가야 할 것은 타입스크립트의 구조적 타이핑과 CK 메트릭의 궁합이다. CK 메트릭은 원래 C++이나 자바 같은 명목적 타입 시스템을 염두에 두고 설계되었다. 타입스크립트에서는 인터페이스가 구조적으로 호환되기만 하면 상속 관계가 아니어도 같은 타입으로 취급되므로, DIT나 NOC 같은 상속 기반 메트릭의 해석에 약간의 주의가 필요하다. 예컨대 implements 키워드 없이 덕 타이핑으로 인터페이스를 충족하는 클래스들은 NOC에 집계되지 않지만, 의미적으로는 분명히 동일한 추상화의 구현체다. Artie-lens는 AST 레벨에서 extendsimplements를 명시적으로 추적하므로, 이 한계를 이해하고 지표를 읽는 것이 중요하다. 실무적으로는 DIT와 NOC보다 WMC, CBO, LCOM에 더 무게를 두고 설계 게이트를 운용하는 편이 타입스크립트 프로젝트에서는 더 효과적이다.

설계 품질 게이트를 구성할 때 고려해야 할 마지막 퍼즐 조각은 빌드 시간이다. 메트릭 측정 자체는 AST 파싱이므로 테스트 실행이나 번들링에 비하면 가볍지만, 모노레포에서 수백 개의 패키지를 대상으로 돌릴 때는 이야기가 달라진다. Artie-lens는 파일 단위 병렬 처리를 지원하지만, CI에서 모든 패키지를 매번 측정하는 대신 변경된 패키지만 선별해서 측정하는 전략이 필요하다. turbonx의 affected 그래프와 조합해서, 실제로 코드가 변경된 패키지에만 메트릭 게이트를 적용하는 방식이 현실적이다.

코드 리뷰의 품질을 지표화하려는 시도는 사실 꽤 오래된 아이디어다. 사이클로매틱 복잡도를 측정하는 McCabe부터 할스테드 복잡도 측정, 그리고 CK 메트릭까지, 소프트웨어의 구조적 건강을 진단하려는 학문적 노력은 계속되어 왔다. 그런데 정작 현장에서는 이런 지표들이 대학원 논문이나 소나큐브 대시보드 안에 갇혀 있었다. CI 파이프라인이 빌드와 테스트를 통제하는 것처럼, 설계 품질도 같은 수준의 자동화된 게이트를 통과하도록 만드는 것. 그게 Artie-lens라는 작은 CLI 도구가 보여주는 가능성이다. 설계는 기획서나 화이트보드 위에서만 결정되는 것이 아니라, 매일의 PR과 머지 속에서 조금씩 무너지거나 단단해진다. 그 흐름에 숫자라는 언어를 심어두면, 적어도 "어쩌다 이렇게 됐지?"라고 말해야 하는 날은 조금 늦춰진다.

댓글

댓글을 읽어오는 중입니다.

같이 읽으면 좋은 글

방금 읽은 주제와 이어지는 글을 골랐습니다.

Frontend 전체 보기

이전 글

LLM 서빙의 숨은 병목, KVCache를 무기로 바꾸다

다음 글

AI 에이전트는 서버에서 태어났지만 브라우저에서 산다

DevInsight Digest

새 글이 쌓이면, 피드에서 바로 이어 읽으세요.

과장된 알림 대신 발행한 글 전체를 RSS로 제공합니다.

RSS 구독