소프트웨어 품질은 왜 "서서히" 죽는가
소프트웨어 품질 저하는 대부분 단 한 번의 치명적인 실수가 아니라, 수천 번의 작은 타협이 쌓여 일어난다. 리뷰 한 번 대충 넘기고, 테스트 한 줄 주석 처리하고, "일단 동작하니까 나중에 리팩토링하자"는 결정이 반복된다. 이 현상을 흔히 "천 번의 상처로 인한 죽음(death by a thousand cuts)"이라 부른다. 4년차 이상의 개발자라면 이 패턴이 낯설지 않을 것이다. 문제는 각각의 결정이 당시에는 합리적으로 보인다는 점이다.
품질 저하가 위험한 이유는 복리 효과로 작동하기 때문이다. 코드 한 곳의 응집도가 낮아지면 그 주변을 수정하는 비용이 올라가고, 높아진 비용은 다음 개발자가 또 다른 타협을 선택하게 만든다. 시니어 개발자가 해야 할 일은 이 패턴을 인식하고 팀 차원의 구조적 대응을 설계하는 것이다.
품질을 측정 가능하게 만들기
"코드가 더러워졌다"는 감각은 주관적이다. 팀 전체가 공유할 수 있으려면 품질을 가시화해야 한다. 실무에서 자주 쓰이는 접근은 다음과 같다.
- 순환 복잡도(Cyclomatic Complexity) 임계값 설정: 메서드 복잡도가 일정 수준을 초과하면 PR 머지를 막는 규칙을 CI에 적용한다.
- 테스트 커버리지 하한선: 커버리지 자체가 품질을 보장하진 않지만, 하락 추세를 감지하는 지표로 활용할 수 있다.
- 아키텍처 적합성 검사: ArchUnit 같은 도구로 레이어 의존성 규칙 위반을 자동으로 탐지한다.
// ArchUnit 예시: Service 레이어가 Controller를 직접 참조하지 못하도록
@ArchTest
static final ArchRule servicesShouldNotDependOnControllers =
noClasses().that().resideInAPackage("..service..")
.should().dependOnClassesThat()
.resideInAPackage("..controller..");
측정이 목적이 아니라 대화의 시작점임을 팀이 공유해야 한다. 숫자를 목표로 삼는 순간 굿하트의 법칙이 작동한다.
품질에 대한 사고방식을 바꾸는 것
기술적 도구보다 더 근본적인 문제는 품질을 바라보는 관점이다. 많은 팀에서 품질은 "기능 개발이 끝난 후 하는 것"으로 취급된다. 이 관점이 유지되는 한 품질은 항상 일정에 밀린다.
대안적 접근은 품질을 작동하는 코드의 조건으로 재정의하는 것이다. "이 코드는 기능은 하지만 품질이 낮다"는 문장 자체를 팀이 받아들이지 않는 문화가 필요하다. 실용적인 수준에서는, PR 체크리스트에 "이 변경이 기존 코드보다 이해하기 쉬운가?"라는 질문 하나를 추가하는 것만으로도 대화의 질이 달라진다.
// 리뷰 기준 예시: 동일 로직, 명확성 차이
// Before
if (u.getS() == 1 && u.getR() != null) { ... }
// After
if (user.isActive() && user.hasRole()) { ... }
시니어 개발자는 이런 기준을 코드리뷰에서 일관되게 적용함으로써 팀의 암묵적 기준선을 끌어올리는 역할을 한다.
정리
- 품질 저하는 단번에 오지 않는다. 작은 타협이 반복·누적되는 패턴을 팀 차원에서 인식하고 차단해야 한다.
- 복잡도 분석, 아키텍처 규칙 검사 등 품질을 자동화된 지표로 가시화하면 주관적 논쟁을 줄일 수 있다.
- 품질은 기능 개발의 사후 작업이 아니라, "동작한다"는 정의 자체에 포함되어야 한다는 관점 전환이 핵심이다.