A-3. 복잡도·추상화·소프트웨어 엔트로피
한 줄 요약: 추상화와 리팩터링이 복잡도를 없애는지, 다른 계층으로 옮기는지, 미래의 변경 비용을 키우는지 판단할 수 있다.
학습 목표
- Gall's Law와 Second-System Effect를 통해 큰 시스템을 한 번에 설계하려는 위험을 설명할 수 있다.
- Conservation of Complexity, Leaky Abstractions, Hyrum's Law를 사용해 복잡도와 암묵적 계약의 위치를 추적할 수 있다.
- Broken Windows Theory, Scout Rule, Kernighan's Law를 코드 품질과 유지보수 행동의 관점에서 해석할 수 있다.
- React 컴포넌트, 디자인 시스템, API client, 상태 관리 추상화의 경계 조건을 판단할 수 있다.
배경: 왜 이것이 존재하는가
소프트웨어 설계에서 "복잡도를 줄였다"는 말은 자주 쓰이지만, 실제로는 복잡도가 사라진 것이 아니라 이동한 경우가 많다. 개발자에게 단순한 API가 사용자에게 복잡한 입력을 요구할 수 있고, 컴포넌트 코드는 깔끔해졌지만 빌드 설정과 런타임 디버깅이 어려워질 수 있다. 추상화는 비용을 재배치한다.
프론트엔드는 이 문제가 특히 잘 보인다. UI는 도메인 규칙, 비동기 상태, 브라우저 제약, 접근성, 성능, 디자인 시스템, API 계약이 만나는 경계다. "공통 컴포넌트로 뽑자", "상태를 전역으로 올리자", "새 디자인 시스템을 만들자" 같은 결정은 코드 줄 수보다 더 깊은 비용 이동을 만든다.
이 문서에서 다루는 원문 항목명은 Gall's Law, The Law of Conservation of Complexity, The Law of Leaky Abstractions, Hyrum's Law, The Second-System Effect, The Broken Windows Theory, The Scout Rule, Kernighan's Law다.
핵심 개념
작동하는 복잡한 시스템은 작동하는 단순한 시스템에서 진화한다
Gall's Law는 복잡한 시스템을 처음부터 완성형으로 설계하려는 충동을 경계한다. 작동하는 복잡한 시스템은 대개 작동하는 단순한 시스템에서 진화한다. 핵심은 "단순한 시스템"이 장난감이어야 한다는 뜻이 아니다. 작지만 실제 제약을 만나는 경계여야 한다.
예를 들어 디자인 시스템을 처음부터 모든 제품, 모든 테마, 모든 접근성 상태, 모든 플랫폼에 대응하도록 만들면 Second-System Effect가 나타나기 쉽다. 첫 제품에서 겪은 모든 후회를 두 번째 시스템에 반영하려고 하면서 과도한 추상화와 설정이 생긴다. 더 안전한 방식은 실제로 반복되는 컴포넌트와 변경 축을 관찰하고, 공통화의 기준을 점진적으로 세우는 것이다.
복잡도는 사라지지 않고 이동한다
The Law of Conservation of Complexity는 복잡도가 한쪽에서 줄면 다른 쪽으로 이동할 수 있음을 상기시킨다. 복잡도의 수신자는 개발자, 사용자, 운영자, 브라우저, 빌드 도구, 백엔드 팀일 수 있다.
| 결정 | 줄어드는 복잡도 | 이동하는 복잡도 |
|---|---|---|
| 공통 Form 컴포넌트 도입 | 화면별 중복 코드 | 예외 props, validation 확장, 디버깅 경로 |
| 전역 상태 저장소 도입 | 깊은 props 전달 | 상태 소유권, cache invalidation, 테스트 격리 |
| BFF 도입 | 클라이언트 조합 로직 | 서버 배포, API 소유권, 장애 경계 |
| 디자인 토큰 도입 | 색상과 간격 중복 | 토큰 이름, migration, 디자이너 협업 |
복잡도 이동 자체는 나쁘지 않다. 좋은 설계는 복잡도를 더 감당하기 쉬운 위치로 옮긴다. 문제는 이동을 제거로 착각할 때 생긴다.
모든 비자명한 추상화는 샌다
The Law of Leaky Abstractions는 추상화가 하위 계층의 세부를 완전히 숨기지 못한다는 관찰이다. fetch wrapper가 HTTP status, abort, retry, cache, credential 정책을 모두 감추려 해도 어느 순간 하위 세부가 드러난다. React 컴포넌트 추상화도 DOM focus, event propagation, layout shift, hydration mismatch를 완전히 숨기지 못한다.
누수는 실패가 아니라 비용이다. 좋은 추상화는 누수가 발생했을 때 탈출구를 제공한다. 예를 들어 공통 Modal 컴포넌트는 기본 focus trap과 aria 속성을 제공하되, 특정 화면에서 초기 focus 대상과 close 정책을 조정할 수 있어야 한다. 반대로 모든 예외를 props로 열어 두면 추상화가 아니라 설정 언어가 된다.
관찰 가능한 동작은 계약이 된다
Hyrum's Law는 충분히 많은 사용자가 있으면 API의 모든 관찰 가능한 동작이 누군가에게 의존된다는 관찰이다. 문서화하지 않은 CSS class, 에러 메시지 형식, sort 안정성, 빈 문자열 처리, debounce 시간도 외부에서 관찰 가능하면 암묵적 계약이 될 수 있다.
프론트엔드 내부 패키지에서도 이 법칙은 작동한다. 디자인 시스템의 Button이 내부적으로 button 태그를 쓰는지 a 태그를 쓰는지, class name을 어떻게 붙이는지, disabled일 때 click handler를 호출하는지 같은 세부가 소비자에게 의존될 수 있다. 따라서 public API와 private detail의 경계를 문서, lint, export 구조, 테스트로 명확히 해야 한다.
방치된 품질은 미래 행동을 바꾼다
The Broken Windows Theory는 깨진 창문이 방치되면 더 많은 훼손을 부른다는 사회적 관찰에서 왔다. 코드에서는 타입 오류를 무시하는 주석, flaky test, 아무도 설명하지 못하는 util, 접근성 누락, 린트 예외가 비슷한 역할을 한다. 작은 방치는 "여기는 그래도 된다"는 신호가 된다.
The Scout Rule은 코드를 만질 때 주변을 조금 더 낫게 만들라는 태도다. 이 원칙은 무제한 리팩터링을 의미하지 않는다. 변경과 관련된 작은 정리, 테스트 이름 개선, 죽은 코드 제거, 타입 좁히기처럼 리뷰 가능한 범위에서 품질 하락을 늦춘다.
영리한 코드는 디버깅 비용을 키운다
Kernighan's Law는 디버깅이 코드를 작성하는 것보다 어렵기 때문에, 최대한 영리하게 작성한 코드는 디버깅할 역량을 넘어설 수 있다는 경고다. 프론트엔드에서는 과도한 타입 트릭, 동적 import 조건, render props 중첩, proxy 기반 상태 추적, CSS specificity 해킹에서 자주 보인다.
영리함이 언제나 나쁜 것은 아니다. 복잡한 문제에는 복잡한 구현이 필요할 수 있다. 그러나 복잡한 구현은 관찰 가능성, 테스트, 문서, 단순한 실패 모드를 요구한다. 성능 최적화나 타입 레벨 프로그래밍이 필요하다면 왜 필요한지, 어떤 입력에서 깨지는지, 어떻게 검증하는지 남겨야 한다.
실무 관점
설계 리뷰에서는 "복잡한가"보다 "복잡도가 어디에 있는가"를 묻는다.
| 검토 축 | 질문 |
|---|---|
| API | 소비자가 알아야 하는 개념 수가 줄었는가 늘었는가 |
| 상태 | 상태 소유권과 무효화 책임이 분명한가 |
| 런타임 | 실패했을 때 stack trace와 로그로 원인을 찾을 수 있는가 |
| 성능 | 추상화가 불필요한 render, bundle, network 비용을 만드는가 |
| 운영 | 배포, rollback, 관측 지표가 단순해졌는가 |
| 사용자 | 사용자에게 더 많은 선택과 오류 가능성을 넘기지 않았는가 |
대규모 재작성은 Second-System Effect가 강하게 작동하는 영역이다. 재작성이 필요한 경우에도 "현재 시스템의 어떤 실패 모드를 제거할 것인가", "새 시스템이 같은 실패를 어떻게 방지하는가", "점진적으로 검증할 경계는 어디인가"를 명확히 해야 한다.
더 깊이
추상화의 품질은 내부 코드가 예쁜지보다 실패했을 때 드러난다. 좋은 추상화는 다음 특성을 가진다.
- 기본 경로는 단순하다.
- 예외 경로는 숨기지 않고 명시적으로 연다.
- 하위 계층으로 내려갈 디버깅 경로가 있다.
- public contract와 private detail이 구분된다.
- 성능 비용과 bundle 비용을 관찰할 수 있다.
- 변경 축이 실제로 함께 바뀌는 것들끼리 묶인다.
이 기준은 Phase 9의 패턴 학습과 연결된다. 패턴은 복잡도를 없애는 주문이 아니라, 이미 존재하는 힘을 이름 붙이는 도구다. 패턴을 적용했는데 변경이 더 넓게 퍼지고 디버깅이 어려워졌다면, 이름 있는 패턴을 썼더라도 설계는 실패했을 수 있다.
회고 질문
- 이 설계에서 복잡도는 누구에게 이동했는가?
- 추상화가 샐 때 하위 계층으로 내려갈 탈출구가 있는가?
- 문서화하지 않았지만 소비자가 의존할 수 있는 관찰 가능한 동작은 무엇인가?
- 이번 리팩터링은 Broken Windows를 줄이는가, 아니면 더 큰 공사를 숨기는가?
- 이 코드를 디버깅할 사람이 필요한 맥락을 어디서 얻는가?
정리
- 추상화는 복잡도를 제거하기보다 더 감당하기 쉬운 위치로 옮긴다.
- 작동하는 복잡한 시스템은 작동하는 단순한 경계에서 진화하는 편이 안전하다.
- 누수 없는 추상화보다 누수될 때 디버깅 가능한 추상화가 더 현실적이다.
- 관찰 가능한 동작은 암묵적 계약이 될 수 있다.
- 작은 품질 저하는 팀의 미래 행동을 바꾸므로 제한된 범위에서 꾸준히 다뤄야 한다.
확인 문제
- 공통 Table 컴포넌트가 정렬, 필터, 페이지네이션, 가상 스크롤, row selection, inline edit을 모두 지원한다. 어떤 법칙을 떠올릴 수 있는가?
정답과 해설
Second-System Effect와 Leaky Abstractions를 의심할 수 있다. 여러 요구를 한 컴포넌트에 미리 넣으면 설정 복잡도가 커지고, 화면별 예외가 하위 DOM과 성능 세부를 새어 나오게 만들 수 있다. 실제 반복되는 변경 축인지 확인해야 한다.
- 라이브러리 문서에는 없지만 사용자가 에러 메시지 문자열을 파싱하고 있다. 어떤 위험이 있는가?
정답과 해설
Hyrum's Law의 사례다. 문서화하지 않은 관찰 가능한 동작이 암묵적 API가 되었다. 문자열 변경이 breaking change가 될 수 있으므로 public error code를 제공하거나 private detail에 의존하지 못하게 경계를 정해야 한다.
- Scout Rule을 적용한다는 말이 무제한 리팩터링을 뜻하지 않는 이유는 무엇인가?
정답과 해설
Scout Rule은 변경과 관련된 주변을 조금 더 낫게 만드는 태도다. 리뷰 가능한 범위를 넘는 대규모 구조 변경은 별도 목적, 검증, rollback 계획이 필요하다.
참고 자료
- <dwmkerr/hacker-laws>: 복잡도와 추상화 관련 법칙의 원문 목록이다.
- Phase 5-6. 상태 아키텍처: 상태 소유권과 복잡도 이동을 연결해 읽는다.
- Phase 9-1. 패턴을 설계 어휘로 사용하기: 패턴을 절대 규칙이 아니라 설계 언어로 쓰는 관점을 연결한다.
- Phase 10-2. 코드 품질과 리뷰: 코드 품질을 위험 탐지 관점으로 읽는다.