개요
소프트웨어 시스템에서 복잡성은 피할 수 없다. 그러나 모든 복잡성이 동등하지는 않다. 어떤 복잡성은 해결하려는 문제에 내재된 것이고, 어떤 복잡성은 우리가 선택한 도구, 구조, 접근 방식에서 비롯된 것이다.
이 두 가지를 구분하지 못할 때, 엔지니어는 잘못된 곳에서 싸운다. 제거할 수 없는 것을 없애려 과도하게 추상화하거나, 반대로 제거해야 할 것을 방치하며 시스템을 쓸데없이 무겁게 만든다.
본질적 복잡성(Essential Complexity) 과 우발적 복잡성(Accidental Complexity) 의 구분은, 좋은 소프트웨어 설계의 가장 근본적인 판단 기준 중 하나다.
이 개념을 명시적으로 체계화한 것은 컴퓨터 과학자 프레드 브룩스(Fred Brooks) 다. 1986년 논문 「No Silver Bullet — Essence and Accident in Software Engineering」에서 그는 소프트웨어 개발의 어려움을 두 가지로 나누었다.
브룩스는 소프트웨어 공학의 “은총알(silver bullet)”, 즉 생산성을 한 번에 10배 올려줄 기술이 왜 존재할 수 없는지를 설명하기 위해 이 구분을 사용했다. 그의 논지는 다음과 같다.
소프트웨어의 어려움 대부분은 본질적인 것이다. 우발적인 어려움은 이미 도구와 언어의 발전으로 상당 부분 해소되었다. 남은 것은 문제 자체에 내재된 복잡성이므로, 이것을 제거해줄 단일 기술은 없다.
- 본질적 복잡성: 소프트웨어의 본성 자체에 내재된, 제거 불가능한 어려움
- 우발적 복잡성: 현재의 도구·환경·기술적 한계로 인해 부수적으로 발생하는 어려움으로, 원칙적으로 제거 가능
본질적 복잡성 (Essential Complexity)
정의
본질적 복잡성은 문제 도메인(problem domain) 자체에서 비롯된 복잡성이다. 어떤 기술을 쓰든, 어떤 언어로 구현하든, 어떤 팀이 작업하든 피할 수 없다. 그것은 해결하려는 현실 세계의 규칙, 관계, 제약에서 온다.
- 소프트웨어 개체(entity)의 본질은 상호 연결된 개념들의 구조체다: 데이터 집합, 데이터 항목 간 관계, 알고리즘, 함수 호출
- 이 개념적 구조체는 표현 방식과 무관하게 동일하게 유지된다
- 소프트웨어 개발의 진정으로 어려운 부분은 이 개념적 구조체를 명세(specification)하고 설계하며 테스트하는 것이지, 단순히 표현하는 노동이 아니다
- 문제를 제대로 해결하는 한 사라지지 않는다.
- 숨기거나 회피하려 할 경우, 복잡성이 없어지는 게 아니라 다른 곳으로 이동한다.
- 잘 설계된 시스템은 이 복잡성을 명확하게 드러내고 격리한다.
네 가지 본질적 속성
Brooks는 현대 소프트웨어 시스템의 본질적이고 환원 불가능한 속성으로 네 가지를 제시한다.
1. 복잡성 (Complexity)
- 소프트웨어 개체는 그 크기에 비해 인간이 만든 어떤 구조물보다 복잡하다
- 동일한 두 부분이 존재하지 않는다: 반복되는 요소가 있으면 서브루틴으로 추상화하기 때문
- 소프트웨어를 스케일업하면 동일 요소의 단순 반복이 아닌, 서로 다른 요소의 수가 증가한다
- 요소들 간의 상호작용은 비선형적이므로, 전체 복잡성은 선형 이상으로 증가한다
- 복잡성으로부터 파생되는 문제:
- 팀원 간 의사소통 어려움 → 제품 결함, 비용 초과, 일정 지연
- 가능한 모든 상태를 열거·이해하기 불가능 → 신뢰성 저하
- 기능 호출의 어려움 → 사용성 저하
- 구조적 복잡성 → 기능 확장 시 부작용 발생
- 미가시화된 상태들 → 보안 취약점
2. 순응성 (Conformity)
- 소프트웨어가 마주하는 복잡성 중 상당 부분은 임의적(arbitrary)이다
- 인터페이스를 맞춰야 하는 인간 제도와 시스템들은 논리적 필연이 아닌, 단지 서로 다른 사람들에 의해 설계되었기 때문에 제각각이다
- 소프트웨어만을 재설계하는 것으로는 이 복잡성을 단순화할 수 없다
- 물리학자는 자연에서 통일 원리를 찾을 수 있다는 믿음을 가지지만, 소프트웨어 엔지니어에게는 그러한 믿음이 주어지지 않는다
3. 가변성 (Changeability)
- 소프트웨어는 끊임없는 변경 압력에 노출된다
- 건물, 자동차, 컴퓨터 하드웨어와 달리, 소프트웨어는 출시 후에도 빈번하게 수정된다
- 이유 1: 소프트웨어는 시스템의 기능 자체를 구현하며, 기능은 변경 압력을 가장 많이 받는 부분이다
- 이유 2: 소프트웨어는 순수한 사고의 산물(pure thought-stuff)로서 무한히 가변적이므로, 변경 비용이 낮다고 인식된다
- 소프트웨어 제품은 응용 분야, 사용자, 법규, 하드웨어라는 문화적 맥락 속에 내재되어 있으며, 이 모든 것이 지속적으로 변화한다
4. 비가시성 (Invisibility)
- 소프트웨어는 본질적으로 공간에 내재되어 있지 않아 기하학적 표현이 불가능하다
- 건물의 평면도, 분자 구조 모형과 달리 소프트웨어 구조를 도식화하면 제어 흐름, 데이터 흐름, 의존성 패턴 등 서로 다른 여러 방향성 그래프가 중첩되어 나타난다
- 이 그래프들은 평면적이지도, 계층적이지도 않은 경우가 대부분이다
- 비가시성은 단일 설계자 내에서의 설계 과정을 방해할 뿐 아니라, 설계자들 간의 의사소통을 심각하게 저해한다
우발적 복잡성 (Accidental Complexity)
우발적 복잡성은 문제 자체가 아니라 우리가 문제를 해결하기 위해 선택한 방법에서 비롯된 복잡성이다. 다른 선택을 했다면 존재하지 않았을 복잡성이다.
정의
- 소프트웨어 생산에 현재는 수반되지만, 소프트웨어의 본성에 내재되어 있지 않은 어려움
- 도구, 언어, 환경, 하드웨어 제약 등 외부 조건에 의해 발생한다
- 올바른 결정으로 제거하거나 줄일 수 있다.
우발적 복잡성의 원인과 유형
우발적 복잡성은 단일 원인이 아니라 여러 패턴에서 발생한다.
잘못된 추상화
현실보다 앞서 만들어진 추상화는 곧 틀어진다. 미래를 예측해 설계한 인터페이스가 실제 요구와 맞지 않을 때, 팀은 추상화를 깨지 않기 위해 플래그, 예외 케이스, 우회 경로를 덧붙인다. 이것이 잘못된 추상화에서 오는 우발적 복잡성이다.
과잉 일반화 (Over-generalization)
한 번 쓸 코드를 “나중에 재사용할 것”이라는 가정 하에 설정 가능하고 유연하게 만드는 것. 사용되지 않는 유연성은 이해 비용만 높인다. 실제로 두 번째 사용처가 생기면, 미리 만든 구조와 맞지 않는 경우가 더 많다.
도구와 프레임워크의 마찰
도구는 문제를 해결하기 위해 선택하지만, 그 도구 자체의 학습 곡선, 버전 호환성, 설정 방식이 새로운 복잡성을 만든다. “이 라이브러리를 사용하기 위해 이 설정을 추가해야 한다”는 식의 복잡성은 도메인과 무관하다.
경계의 잘못된 배치
모듈, 서비스, 레이어의 경계가 도메인 개념이 아닌 기술적 편의로 그어질 때, 경계를 가로지르는 정보 흐름이 과도해진다. 이는 결합도(coupling)를 높이고 변경을 어렵게 만든다.
누적된 임시방편 (Patch Accumulation)
버그를 고치거나 요구사항을 빠르게 반영하기 위해 구조를 개선하지 않고 덧붙이는 패치들. 각각은 작은 결정이지만, 시간이 지날수록 코드베이스의 이해 가능성을 갉아먹는다.
암묵적 상태와 숨겨진 의존성
전역 변수, 암묵적 싱글턴, 실행 순서에 달린 초기화 로직. 코드의 동작이 보이지 않는 상태에 달려 있을 때, 추론과 테스트가 모두 어려워진다.
유행하는 패턴의 맥락 없는 적용
마이크로서비스, 이벤트 소싱, CQRS, DDD 같은 패턴들은 각각 특정 문제를 해결하기 위해 만들어졌다. 해당 문제가 없는 곳에 패턴을 적용하면 해결책보다 복잡성이 먼저 도착한다.
우발적 복잡성의 한계
$$ \text{time of task} = \sum_{i} (\text{frequency})_i \times (\text{time})_i $$
- 우발적 복잡성을 모두 제거해 그 처리 시간을 0으로 줄이더라도, 작업 전체의 9/10 이상이 우발적 요소에 소비되지 않는 이상 한 자릿수 배(order of magnitude) 개선은 불가능하다
- 개념적 요소(본질)가 작업 시간의 대부분을 차지하고 있다면, 표현(expression)에 관련된 요소들을 아무리 줄여도 큰 생산성 향상을 기대할 수 없다
두 개념의 비교
| 구분 | 본질적 복잡성 | 우발적 복잡성 |
|---|---|---|
| 원인 | 소프트웨어 본성 자체 | 현재의 도구·기술·환경 |
| 제거 가능성 | 불가능 (제거 시 문제도 사라짐) | 가능 (더 나은 선택으로 줄일 수 있음) |
| 예시 | 요구사항 명세, 개념 설계, 상태 공간 폭발 | 기계어 작성, 느린 컴파일, 불편한 개발 환경 |
| 해결 방향 | 개념적 접근(프로토타이핑, 점진적 개발, 우수 설계자 육성) | 도구 발전(고수준 언어, IDE, 통합 환경) |
복잡성에 관한 흔한 오해
오해 1 — “복잡성은 나쁜 것이다”
본질적 복잡성은 나쁜 것이 아니다. 복잡한 문제를 해결하는 시스템은 당연히 복잡하다. 나쁜 것은 우발적 복잡성이다. 이 구분 없이 “복잡성을 줄이자”고 말하면 잘못된 곳을 공략하게 된다.
오해 2 — “코드가 짧으면 복잡성이 낮다”
코드의 길이와 복잡성은 무관하다. 한 줄에 많은 것을 욱여넣은 코드는 짧지만 이해하기 어렵다. 잘 분리되고 이름이 명확한 긴 코드가 더 단순할 수 있다. 복잡성의 기준은 이해하고 변경하는 데 드는 비용이다.
오해 3 — “레거시 코드는 모두 우발적 복잡성이다”
레거시 코드에는 과거의 도메인 지식이 담겨 있는 경우가 많다. “왜 이렇게 되어 있지?“라는 코드가 알고 보면 특수한 비즈니스 규칙을 반영한 본질적 복잡성일 수 있다. 이해하기 전에 제거하면 비즈니스 규칙도 함께 사라진다.
오해 4 — “최신 기술을 쓰면 우발적 복잡성이 줄어든다”
최신 기술은 특정 우발적 복잡성을 줄여주지만, 새로운 우발적 복잡성을 만들기도 한다. 기술 선택 자체보다 그 기술이 해결하는 본질적 문제와 만들어내는 우발적 비용의 균형을 보아야 한다.
오해 5 — “아키텍처 패턴을 따르면 우발적 복잡성이 줄어든다”
패턴은 특정 맥락의 문제에 대한 해법이다. 해당 문제가 없는 곳에 패턴을 적용하면 우발적 복잡성이 늘어난다. 마이크로서비스는 특정 규모의 조직과 시스템에서 본질적 복잡성을 다루기 위한 패턴이다. 그 맥락이 없는 곳에서는 우발적 복잡성만 증가시킨다.
본질적 복잡성에 대한 유망한 접근법
Brooks는 은총알은 없지만, 본질적 복잡성의 핵심을 공략하는 몇 가지 방향을 제시한다.
- 구매 vs. 개발 (Buy vs. Build): 구축이 가능하더라도 기성 소프트웨어를 구매하는 것이 더 경제적일 수 있으며, 대중 시장의 발전이 소프트웨어 공학에서 가장 심오한 장기 트렌드다
- 요구사항 정제와 신속한 프로토타이핑: 클라이언트는 자신이 원하는 것을 정확히 알지 못한다; 프로토타입을 통한 반복적 명세화가 본질을 공략하는 접근법이다
- 점진적 개발 (Grow, not Build): 처음부터 완성된 시스템을 건축하는 대신, 실행 가능한 최소 시스템에서부터 기능을 유기적으로 성장시킨다
- 우수 설계자 육성: 기술적 탁월성은 궁극적으로 사람에게 달려 있으며, 위대한 설계자를 조기에 식별하고 체계적으로 육성하는 것이 가장 중요한 단일 노력이다
이 개념의 현대적 의의
Brooks의 구분은 1986년 당시를 기준으로 작성되었으나, 그 구조적 통찰은 이후에도 소프트웨어 공학 담론에서 지속적으로 인용된다.
- 현대의 IDE, 정적 분석 도구, CI/CD 파이프라인 등은 우발적 복잡성을 줄이는 노력의 연장선에 있다
- 요구사항 불명확성, 시스템 간 통합, 끝없는 변경 요구는 여전히 본질적 복잡성의 영역에 속한다
- Brooks의 주장에 따르면, 어떤 기술적 혁신도 본질적 복잡성을 제거하지 못하는 한 한 자릿수 배의 생산성 향상을 보장할 수 없다
- Fred Brooks, “No Silver Bullet — Essence and Accident in Software Engineering” (1986)
- Fred Brooks, The Mythical Man-Month: Essays on Software Engineering (1975)
- Rich Hickey, “Simple Made Easy” — Strange Loop Conference (2011)
- Martin Fowler, “Is Design Dead?” — XP2000 Proceedings
- David Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules” (1972)
- Michael Feathers, Working Effectively with Legacy Code (2004)
- Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (2003)
- Kent Beck, Implementation Patterns (2007)