서론, 복잡성이 왜 문제인가
Fred Brooks는 1986년 ”No Silver Bullet“에서 소프트웨어를 어렵게 만드는 성질로 복잡성(Complexity), 순응성(Conformity), 변경 용이성(Changeability), 비가시성(Invisibility) 네 가지를 꼽았다. 그런데 2006년 벤 모즐리와 피터 마크스는 논문 ”Out of the Tar Pit“에서 이 네 가지 중 진짜 근본 원인은 복잡성 하나뿐이라고 정리한다. 순응성은 기존 시스템의 복잡한 제약에 맞춰야 하는 문제이고, 비가시성은 복잡한 구조를 눈에 보이는 형태로 그려낼 수 없다는 문제이며, 변경 용이성이 어려운 이유도 결국 시스템이 복잡해서 어디를 건드려야 할지 알 수 없기 때문이다. 네 가지 성질은 서로 다른 이름을 달고 있지만 뿌리를 파보면 하나로 수렴한다.
이 결론에 무게가 실리는 이유는 복잡성이 소프트웨어의 신뢰성, 납기, 보안, 성능 문제 대부분의 공통 원인으로 지목되기 때문이다. 어떤 문제든 해결하려면 먼저 시스템을 이해해야 하는데, 그 이해를 가로막는 것이 다름 아닌 복잡성이다. Dijkstra는 “스스로 만든 복잡함에 짓눌리지 않으려면 명료하고 얽힘 없고 단순하게 유지해야 한다”고 했고, 1990년 튜링상 수상 연설에서 Corbató는 “야심 찬 시스템의 일반적인 문제는 복잡성”이라며 복잡성은 어려움을 곱절로 늘리는 방식으로 작동하기 때문에 단순함과 우아함의 가치를 강조해야 한다고 말했다.
Backus는 1977년 튜링상 연설에서 기존 언어들의 복잡함과 취약함을 지적하며 “프로그램을 사고하도록 돕는 강력한 방법론이 절실히 필요하다”고 했다. Hoare는 1980년 튜링상 연설에서 한발 더 나아간다. “돈으로 살 수 없는 단 하나의 품질이 있다면 신뢰성이다. 신뢰성의 대가는 철저한 단순함을 추구하는 것”이라고 하면서, 소프트웨어를 설계하는 방법은 두 가지뿐이라고 정리한다. 결함이 없는 게 뻔히 보일 만큼 단순하게 만들거나, 결함이 있는지조차 보이지 않을 만큼 복잡하게 만들거나. 그리고 첫 번째 방법이 훨씬 어렵다고 덧붙인다.
단순함이 어렵다는 이 결론은 “Out of the Tar Pit” 저자들도 그대로 받아들인다. “Simplicity is Hard”라는 논문의 소제목에서 알 수 있다. 다만 저자들은 복잡성이 어디서 생기는지 원인을 추적할 수 있다면 그중 상당수는 피할 수 있다고 이 논문에서 낙관한다.
이 글은 그 추적 경로를 네 부로 나눠 따라간다. 1부는 복잡성이 실제로 어디서 생겨나는지, 그중 어디까지가 피할 수 없는 부분인지 이론으로 해부한다. 2부는 이 해부를 진단 질문으로 바꿔 지금 코드에 어떤 복잡성이 있는지 확인하는 도구로 만든다. 3부는 진단에서 걸리는 문제마다 구체적인 처방을 하나씩 놓고, 그 자리에서 곧바로 정기결제 지갑 서비스의 코드로 확인한다. 등록, 충전, 충전 수단, 상태 전이라는 네 기능이 이 순서를 따라 하나씩 늘어난다. 4부는 이 모든 처방이 손대지 못하는 영역, 즉 업무 자체에 내재한 복잡성 앞에서 무엇을 해야 하는지로 마무리한다. Stratified Design이라는 용어가 어디서 왔고 왜 같은 “층”이라는 말이 서로 다른 두 갈래를 가리키게 됐는지처럼 본문의 흐름을 끊는 계보 이야기는 부록으로 따로 뺐다.
제1부 복잡성의 해부
복잡성의 4가지 원인
”Out of the Tar Pit“은 대 규모 시스템에 남아 있는 복잡성의 원인을 상태(State), 제어(Control), 코드량(Code Volume) 세 가지로 좁히고, 여기에 언어의 과도한 자유도라는 네 번째 요인을 덧붙인다.
상태가 만드는 복잡성
고객센터에 전화를 걸었을 때 “다시 시도해보세요”, “새로고침 해보세요”, “프로그램을 재시작하세요”, 심하면 “운영체제를 재설치하고 프로그램을 다시 까세요”라는 답을 들어본 적이 있다면, 상태(state)가 만드는 복잡성을 몸으로 겪었다. 이런 답변이 낯설지 않은 이유는 실제로 효과가 있기 때문이고, 효과가 있는 이유는 많은 시스템이 상태를 다루는 데서 오류를 내기 때문이며, 오류가 나는 이유는 상태가 존재하는 것만으로 프로그램이 이해하기 어려워지기 때문이다.
문제의 크기는 숫자로도 드러난다. 상태를 저장하는 비트 하나를 추가할 때마다 프로그램이 가질 수 있는 전체 상태의 수는 두 배로 늘어난다. 상태가 몇 개만 쌓여도 사람이 머릿속으로 시뮬레이션할 수 있는 범위를 순식간에 넘어선다. 더 나쁜 것은 오염(contamination)이다. 상태를 갖지 않는 함수라도 내부에서 상태를 가진 다른 함수를 호출하는 순간, 그 함수 역시 상태의 맥락 안에서만 이해할 수 있게 된다. 낙타의 코끝만 천막 안으로 들여놓으면 결국 몸통까지 따라 들어온다는 속담 그대로다. 테스트도 마찬가지로 취약해진다. 어떤 입력에 대한 테스트 결과는 다른 입력에 대해 아무것도 말해주지 않는다는 것이 테스트의 근본적 한계인데, 상태가 있으면 같은 입력이라도 시스템이 어떤 상태에 있었는지에 따라 결과가 달라진다. 입력의 조합만으로도 감당하기 벅찬데, 여기에 상태의 조합까지 곱해진다.
비트 하나가 늘 때마다 막대는 배로 뛴다. 필드 몇 개만 더해도 사람이 머릿속으로 따라갈 수 있는 범위를 곧바로 벗어나는 이유가 이 그래프에 그대로 드러난다.
다음 코드는 정기결제 지갑의 충전 로직을 상태를 직접 변경하는 방식으로 짰다.
Before
class Wallet {
private long balance;
private int retryCount;
private boolean locked;
void charge(long amount) {
if (locked) {
retryCount++;
if (retryCount > 3) {
throw new IllegalStateException("잠금 해제 필요");
}
return;
}
balance += amount;
retryCount = 0;
}
}이 코드가 옳은지 확인하려면 charge를 한 번 호출한 결과만으로는 부족하다. 호출 시점에 locked가 어떤 값이었는지, retryCount가 몇이었는지에 따라 같은 amount를 넣어도 결과가 달라진다. balance, retryCount, locked 세 필드는 서로 다른 시점에 서로 다른 이유로 바뀌므로, 이 객체가 가질 수 있는 상태의 조합은 세 필드 각각의 가능한 값을 모두 곱한 만큼 존재한다. 필드 하나를 더 추가할 때마다 이 조합은 배로 불어난다.
제어가 만드는 복잡성
제어(control)는 일이 벌어지는 순서에 관한 개념이다. 문제는 프로그래머가 순서에 관심이 없는 경우에도, 대부분의 명령형 언어가 순서를 명시하도록 강제한다는 데 있다.
a = b + 3;
c = d + 2;
e = f * 4;이 세 줄은 서로 값을 주고받지 않는다. 어떤 순서로 실행되든 결과는 같다. 그런데도 언어의 의미론상 이 코드는 위에서 아래로 순서대로 실행된다고 규정되어 있다. 이 코드를 읽는 사람은 실제로는 무의미한 이 순서를 일단 유효한 제약으로 가정하고 읽기 시작해서, 다시 살펴본 뒤에야 순서가 무관하다는 것을 확인해야 한다. 짧은 예제에서는 이 확인이 순식간에 끝나지만, 코드가 복잡해질수록 순서가 정말 무의미한지 판단하는 일 자체가 만만치 않은 작업이 된다. 판단을 잘못하면 발견하기 어려운 버그로 이어진다. 프로그래머가 요청하지도 않은 순서를 언어가 강제로 부여하고, 그 부여된 순서가 실제로는 무관하다는 것을 다시 확인하는 이중의 낭비가 발생한다.
동시성이 더해지면 문제는 한 단계 더 나빠진다. 상태가 있는 두 스레드가 뒤섞이면 똑같은 초기 상태와 똑같은 입력으로 테스트를 두 번 실행해도 같은 결과가 나온다는 보장이 사라진다. 입력이 같아도 상태에 따라 결과가 달라지는 문제에, 이번에는 실행 순서에 따라서도 결과가 달라지는 문제가 겹친다.
코드량이 만드는 복잡성
코드량 자체는 부차적인 원인이다. 상태와 제어를 다루는 코드가 결국 코드량으로 쌓이기 때문에 따로 떼어 말할 필요가 없어 보이지만, 두 가지 이유로 독립적으로 짚을 가치가 있다. 측정하기 가장 쉬운 지표라는 점, 그리고 다른 원인들과 나쁜 방향으로 상호작용한다는 점이다. Brooks는 이 비선형 증가를 essential complexity의 증거로 들었지만, Dijkstra는 다른 의견을 냈다. 지적 노력이 프로그램 길이의 제곱에 비례해서 늘어난다는 법칙이 있다는 주장이 있지만 그 법칙이 증명된 적은 없으며, 추상화를 제대로 활용하면 이해에 드는 노력이 프로그램 길이에 비례하는 정도로 그칠 수 있다. 상태와 제어를 잘 관리한 시스템에서는 코드량이 늘어나도 복잡성이 비선형으로 폭증하지 않는다. 코드량의 비선형 증가는 코드 자체의 숙명이 아니라, 상태와 제어를 방치했을 때 나타나는 결과에 가깝다.

