본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Computer Science

소프트웨어 복잡성: 본질적 복잡성과 우발적 복잡성

작성 2026. 10. 21.추가
목차15개 항목
출처와 참고

Fred Brooks의 「No Silver Bullet: Essence and Accidents of Software Engineering」(1986), Ben Moseley·Peter Marks의 「Out of the Tar Pit」(2006), Rich Hickey의 「Simple Made Easy」(Strange Loop, 2011) 강연과 녹취록을 바탕으로 재구성한 노트다.

복잡성이란

여기서 말하는 복잡성은 시스템을 이해하기 어렵게 만드는 성질이다. 알고리즘의 시간·공간 복잡도(계산 복잡도 이론)와는 관계없다. 몇 줄짜리 프로그램이 이해하기는 아주 쉬운데 계산 복잡도는 최상위일 수 있다.

이해가 모든 품질의 전제다. 신뢰성 부족, 일정 지연, 보안 결함, 큰 시스템의 성능 문제는 대부분 「시스템을 이해할 수 없다」에서 나온다. 복잡성은 그 이해를 깨뜨린다.

시스템을 이해하는 방법은 둘이다.

방법하는 일한계
테스트밖에서 블랙박스로 보고, 특정 입력에 대한 동작을 관찰한다한 입력의 결과는 다른 입력에 대해 아무것도 알려 주지 않는다. 버그가 있다는 것은 보일 수 있어도 없다는 것은 보일 수 없다
비형식적 추론안을 들여다보고 「이 변수가 이 값이면 이렇게 된다」를 머릿속으로 따라간다경우의 수가 늘면 금방 무너진다

추론이 더 중요하다. 추론이 나아지면 버그가 덜 생기고, 테스트가 나아지면 버그를 더 많이 찾을 뿐이다. 그리고 둘 다 한계가 있으므로 단순함이 둘보다 우선이다. 테스트 투자와 단순함 투자 중 하나를 골라야 한다면 단순함이 앞으로의 모든 이해 시도를 돕는다.

본질적 복잡성과 우발적 복잡성

구분뜻예
본질적 복잡성(essential complexity)사용자가 보는 문제 자체에 들어 있는 복잡성. 이상적인 언어와 인프라가 있어도 팀이 다뤄야 하는 것세법의 예외 규칙, 정산 기준이 여러 개인 결제
우발적 복잡성(accidental complexity)그 밖의 모든 것. 성능, 부족한 언어·인프라, 설계 실수가 덧붙인 복잡성캐시 무효화, 스레드 풀, 루프 카운터, 수동 메모리 관리, 같은 데이터를 여러 곳에 복사해 두기

판정 기준은 「사용자가 그것이 무엇인지 아는가」다. 사용자가 모르는 것(스레드 풀, 루프 카운터)은 본질일 수 없다. 사용자가 옳다고 여길 시스템을 그 복잡성 없이 만들 방법이 하나라도 있으면 본질이 아니다.

은 탄환은 없다

1986년의 논증은 이렇다. 소프트웨어의 어려움에는 본질과 부수(accident)가 있고, 고급 언어·시분할·통합 개발 환경 같은 지난 혁신은 부수적 어려움을 없앤 것이었다. 남은 것이 대부분 본질이라면, 어떤 단일 기술도 10년 안에 생산성·신뢰성·단순성을 한 자릿수(10배) 끌어올릴 수 없다.

소프트웨어를 본질적으로 어렵게 만드는 네 성질은 다음과 같다.

성질내용
복잡성(complexity)같은 부품이 반복되지 않는다. 상태 수가 컴퓨터보다도 몇 자릿수 많다. 규모가 커지면 서로 다른 요소와 상호작용이 비선형으로 는다
순응성(conformity)물리 법칙이 아니라 사람이 만든 제도·인터페이스·기존 시스템에 맞춰야 하고, 그 임의성은 단순화로 없앨 수 없다
가변성(changeability)쓸모 있는 소프트웨어일수록 새 용도와 새 하드웨어에 맞춰 계속 바뀌라는 압력을 받는다
비가시성(invisibility)공간 속 형태가 없다. 제어 흐름·데이터 흐름·의존성 그래프가 겹쳐 있어 하나의 그림으로 그릴 수 없다

본질을 공략할 수 있다고 본 길은 넷이다. 사지 말고 살 수 있으면 산다(buy vs build), 요구사항을 빠른 프로토타입으로 다듬는다, 프로그램을 짓지 말고 키운다(먼저 돌아가는 뼈대를 만들고 점진적으로 살을 붙인다), 뛰어난 설계자를 기른다.

반론: 남은 복잡성 대부분은 우발적이다

2006년의 반론은 네 성질 가운데 복잡성만이 진짜 문제이고 나머지는 복잡성의 한 형태라고 본다. 그리고 「소프트웨어의 복잡성은 본질적 속성」이라는 전제를 거부한다. 단순한 소프트웨어도 얼마든지 가능하며, 현대 시스템의 복잡성 대부분은 문제가 아니라 상태와 제어를 다루는 방식에서 온 우발적인 것이라는 주장이다. 그러니 우발적 복잡성은 최대한 없애고, 본질적 복잡성은 다루기 쉽게 돕는 것이 소프트웨어 공학의 목표가 된다.

두 관점은 「본질적 복잡성은 줄일 수 없다」는 데 동의하고, 지금 시스템에서 어느 쪽이 많은가에서 갈린다. 실무적으로는 후자가 쓸모 있다. 코드에서 마주친 복잡성을 일단 우발적이라고 의심하고 없앨 방법부터 찾는다. 본질이라고 결론 나도 끝이 아니다. 규칙 자체를 사업 쪽과 함께 줄이는 방법은 Simple Design에 있다.

복잡성의 원인

상태

가변 상태(mutable state)가 복잡성의 가장 큰 원인이다.

  • 테스트를 무력하게 만든다. 한 상태에서 통과한 테스트는 다른 상태에 대해 아무것도 말해 주지 않는다. 입력 수보다 상태 수가 훨씬 많고, 둘이 곱해진다. 테스트는 보통 「깨끗한 초기 상태」에서 돌리고 숨은 상태가 결과에 영향이 없기를 바랄 뿐이다. 「다시 해 보세요」, 「재시작하세요」, 「재설치하세요」가 자주 통하는 이유는 시스템을 좋은 내부 상태로 되돌리기 때문이다.
  • 추론을 무너뜨린다. 상태가 하나 늘 때마다 머릿속으로 따라갈 경우의 수가 곱으로 는다.
  • 오염된다. 순수한 함수라도 상태를 가진 무언가를 부르면 그 함수도 상태에 오염된다.

제어 흐름

일이 어떤 순서로 일어나는지를 코드에 적는 것 자체가 복잡성이다. 대부분의 언어는 순서를 반드시 정하게 만든다. 그 순서가 결과에 상관없는 경우에도 읽는 사람은 「이 순서에 의미가 있나」를 따져야 한다. 동시성은 여기에 실행 순서의 경우의 수를 더한다.

코드 양과 그 밖의 원인

코드 양은 다른 원인의 결과이면서 그 자체로도 원인이다. 복잡성은 크기에 비례하지 않고 그보다 빠르게 는다. 중복 코드, 죽은 코드, 불필요한 추상화, 놓친 추상화, 나쁜 모듈화, 빈약한 문서도 우발적 복잡성을 만든다.

얽힘: complect

「Simple Made Easy」는 복잡성의 정체를 얽힘이라고 본다. 옛 단어 complect(꼬다, 엮다)를 빌려, 독립일 수 있는 것들을 함께 꼬아 놓는 것을 복잡성이라고 정의한다.

단순함(simple)쉬움(easy)
어원sim-plex, 한 번 접힌 것, 한 가닥가까이 있는 것(adjacent)
무엇의 성질인가대상의 객관적 성질. 엮인 것이 없다나와의 상대적 관계. 익숙하다, 설치돼 있다, 내 능력 안에 있다
반대말복잡함(complex, 꼬여 있음)어려움(hard)

쉬움을 고르면 단순함을 잃기 쉽다. 익숙한 도구는 쉽지만, 그것이 만든 결과물(artifact)은 꼬여 있을 수 있다. 사람은 구조(construct)를 쓰는 순간의 경험, 「16글자만 쳤다」에 끌린다. 하지만 오래 남아 고치고 디버깅하는 대상은 결과물이다. 테스트와 리팩터링은 꼬인 결과물 위에서 안전망 역할을 할 뿐 꼬임을 풀지 않는다.

  • 단순함은 개수 문제가 아니다. 단순하게 만들면 오히려 부품이 많아질 때가 흔하다. 몇 개가 매듭으로 묶인 것보다 여러 가닥이 곧게 늘어진 편이 낫다.
  • 모듈로 나눴다고 단순한 것이 아니다. 서로 호출하지 않는 모듈도 같은 데이터 모양이나 실행 순서에 기대면 완전히 꼬여 있다. 분할과 계층화는 단순함이 있어야 효과가 난다.
  • 엮음(complect)과 조합(compose)은 다르다. 조합은 독립된 것을 나란히 놓는 것이다.

자주 쓰는 구조가 무엇을 엮는지와 더 단순한 대안을 표로 보면 이렇다.

엮는 구조무엇을 엮나더 단순한 대안
상태, 변수값과 시간불변 값, 시점을 명시한 관리형 참조
객체상태, 정체성, 값값
메서드함수와 상태(언어에 따라 네임스페이스까지)함수, 네임스페이스
상속타입끼리필요한 다형성만 골라 쓰기(프로토콜, 타입 클래스)
switch·패턴 매칭「누가」와 「무엇을」의 짝을 한곳에 닫힌 형태로다형성
구문의미와 순서데이터
반복문, fold무엇을 할지와 어떻게(순서대로) 할지집합 함수(map, filter)
액터무엇을 할지와 누가 할지큐
ORM로직과 표현 방식선언적 데이터 조작
흩어진 조건문규칙과 프로그램 구조규칙 엔진·선언적 규칙

A가 B를 직접 부르면 「언제」와 「어디서」가 엮인다. A는 B가 어디 있는지 알아야 하고, B는 A가 부를 때 실행된다. 사이에 큐를 두면 둘이 풀린다. 데이터는 데이터로 다룬다. 정보 하나가 새로 생길 때마다 클래스를 만들지 말고 맵과 집합을 그대로 쓴다.

처방: 피하고, 분리한다

이상적인 세계에서 무엇이 남나

성능을 신경 쓸 필요가 없고 언어·인프라가 무엇이든 지원하는 이상적인 세계를 가정하면, 비형식 요구사항을 모호함 없는 형식 요구사항으로 옮기고 그것을 바로 실행하면 된다. 이것이 선언형 프로그래밍의 이상이다. 이때 상태와 제어가 어디까지 남는지 따지면 다음 표가 나온다.

데이터종류분류
사용자가 준 입력 데이터, 나중에 다시 참조할 가능성이 있음본질본질적 상태
입력에서 계산한 파생 데이터(표시용, 불변)본질우발적 상태. 필요할 때 다시 계산하면 된다
파생 데이터인데 사용자가 고칠 수 있음본질우발적 상태. 역함수가 있다면 수정을 입력의 수정으로 바꿔 처리한다
요구사항에 없는 파생 데이터(성능용 캐시 등)우발우발적 상태

결론은 상태 대부분은 우발적이고, 제어는 전부 우발적이라는 것이다. 사용자는 계산이 순서대로 일어나든 병렬로 일어나든 관심이 없다.

네 가지 복잡성과 처방

복잡성종류처방
본질적 로직문제의 규칙분리
본질적 상태사용자 입력분리
쓸모 있는 우발적 상태·제어성능용 캐시, 실행 순서 힌트분리
쓸모없는 우발적 상태·제어그 밖의 전부피한다

분리는 두 겹이다. 상태·제어를 순수한 로직에서 떼어 내고(로직/상태 분리), 남은 것을 본질과 우발로 다시 나눈다(본질/우발 분리). 「알고리즘 = 로직 + 제어」라는 1979년 Kowalski의 공식과 같은 방향이다.

flowchart TB
  a["우발적 상태와 제어<br/>(선언만 하고 인프라가 관리)<br/>성능 힌트: 무엇을 캐시할지, 어떤 순서로 돌릴지"] -->|"참조"| b["본질적 로직<br/>파생 관계, 무결성 제약 (순수 함수, 관계 연산)"]
  b -->|"참조"| c["본질적 상태<br/>사용자 입력만 (관계형 테이블)"]
  • 우발적 부분을 다 지워도 시스템은 올바르게 돌아야 한다. 느려질 수는 있지만 결과는 같아야 한다.
  • 우발적 상태는 직접 관리하지 않고 선언만 한다. 「이 파생 값은 캐시한다」고 적으면 인프라가 로직을 보고 일관되게 유지한다. 캐시 불일치 버그가 원천적으로 사라진다. 데이터베이스의 인덱스·구체화 뷰(materialized view), 프런트엔드의 파생 상태 계산(selector, memo)이 같은 생각이다.
  • 각 부분을 표현력이 제한된 서로 다른 언어로 쓴다. 상태를 기술하는 언어에 제어 구문이 있을 이유가 없다. 언어가 약할수록 추론이 쉽다.

이 구조를 구체화한 제안이 함수형 관계형 프로그래밍(FRP: Functional Relational Programming)이다. 본질적 상태는 관계(테이블)로, 로직은 관계 대수와 순수 함수로, 외부 세계와의 연결은 입력을 넣는 피더(feeder)와 결과를 내보내는 옵저버(observer)로 둔다. 같은 이름의 함수형 반응형 프로그래밍(Functional Reactive Programming)과는 다르다.

고전 패러다임은 어떻게 대응하나

패러다임상태제어남는 문제
객체 지향객체 안에 캡슐화명시적 순서상태가 객체마다 흩어지고, 같은 값의 두 객체를 구별하려 정체성(identity)이라는 우발적 개념이 붙는다
함수형(순수)피한다. 참조 투명성으로 같은 입력에 같은 출력대체로 명시적본질적 상태도 인자로 실어 날라야 해서, 상태를 흉내 내는 코드가 다시 생길 수 있다
논리형순수형은 없다실행 순서를 언어가 정한다. 로직과 제어를 분리한다실제로는 성능 때문에 제어(cut 등)를 섞게 된다

상태와 개념의 관계는 프로그래밍 패러다임에서 다룬다.

한계

  • 본질과 우발의 경계는 흐릿하다. 형식 명세 가운데 실행할 수 없는 표현이 있고, 성능 요구가 사실상 본질인 시스템(실시간 제어, 게임 엔진)도 있다.
  • 피하고 분리하라는 처방은 언어·인프라의 도움을 전제한다. 범용 언어 하나로 쓸 때는 규율로 지켜야 한다.
  • 「단순함」은 구조의 성질이라 측정이 어렵다. 코드 줄 수나 클래스 수로 재면 오히려 거꾸로 판단할 수 있다.

정리

  • 복잡성은 이해하기 어려움이고, 거의 모든 소프트웨어 문제의 뿌리다. 테스트와 추론에는 한계가 있어 단순함이 먼저다.
  • 본질적 복잡성은 문제에, 우발적 복잡성은 해법에 있다. 사용자가 모르는 개념은 본질이 아니다.
  • 우발적 복잡성의 주범은 가변 상태와 명시적 제어 흐름이다. 상태 대부분은 다시 계산할 수 있는 파생 데이터다.
  • 단순함(엮이지 않음)과 쉬움(익숙함)을 구별한다. 쉬운 도구가 꼬인 결과물을 남길 수 있다.
  • 쓸모없는 우발적 복잡성은 피하고, 나머지는 본질적 상태·본질적 로직·우발적 성능 힌트로 분리한다.

관련 노트: Simple Design, 프로그래밍 패러다임, 기술 부채, 소프트웨어 비대화, 함수형 프로그래밍, 소프트웨어 아키텍처 패턴

원본 기록4

댓글

GitHub 계정으로 댓글을 남길 수 있어요.