쉽게 풀이한 단순함 (Simple Made Easy)
아래 두 자료를 보면서 정리한 글입니다. 먼저 번역해주신 분들 덕분에 비원어민인 저도 이 자료를 접할 수 있었습니다. 훌륭한 지식을 쉽게 접할 수 있도록 앞서 기여해주신 번역자분들에게 감사드립니다.
단순함과 쉬움의 차이
단순함 (Simple)
- 어원은 ‘sim’ (하나) + ‘plex’(겹치다, 엮다). 반댓말은 complex
- ‘단순하다’라는 것은 뒤섞이거나 엮여있지 않았다는 것 (두 개의 줄을 하나로 엮어 하나의 줄로 만드는 이미지를 떠올리자)
- 임무, 역할, 일, 목표가 하나로 엮여있다. (‘하나만 존재 해야한다’ 혹은 ‘기능이 하나’라는 의미는 아님)
- 기능의 개수(cardinality)와 뒤섞임(interleaving)을 구별해야 한다.
- 우리는 “이 부분이 엮여있네!” 라고 보고 판단할 수 있다. 즉 쉬움과 달리 단순함은 객관적인 개념이다.
쉬움 (Easy)
- 어원은 ‘ease’ (근처에 눕다. 가까이 있다.)
- 손쉬움: 가까이에서 ‘쉽게’ 구할 수 있다 (설치 관리자로 하드에 저장할 수 있는 라이브러리, IDE 도구 등)
- 친숙함: 우리가 이미 알고 있는 것, 내가 가진 역량에 가까운 것
- 위 두 가지 개념에 심취하면 ‘설치해서 5초 안에 실행할 수 있을까?’ 만 고민하게 된다. 다루는 문제는 복잡한데 쉽게 가져다 쓸 수 있는지만 따지게 되는 함정에 빠진다.
- 쉬움은 상대적이다. 뒤섞이거나 꼬인 부분을 봤을 때 단순함을 어겼다고 할 수 있는 것과 구분된다. (내가 독일어를 읽지 못한다고해서 독일어가 해석 불가능한 언어인 것은 아니다. 내가 독일어를 모를 뿐, 독일어는 누구에게는 쉽고 누구에게는 어려울 수 있다.)
도구와 결과물
- 도구(constructs): 프로그래밍 언어, 라이브러리 등
- 결과물(artifacts): 사용자들이 사용할 소프트웨어
- 도구와 결과물을 대할 때 단순함과 쉬움을 구분해야한다.
- 도구의 편의성에만 몰두하는 경향: “몇 글자만 입력했는데 잘 되네요!”, “세미콜론이 없네요!” 등
- 소프트웨어의 퀄리티, 정확성, 유지보수성, 유연함은 우리가 사용하는 도구와는 거의 관련이 없다.
- 그러므로 우리는 결과물을 기준으로 도구를 평가해야한다. (코드의 겉모습이나 사용 경험 등이 아니라)
한계
- 우리의 이해력은 제한적이고 한 번에 아주 적은 숫자만 기억할 수 있다.
- 생각할 것들이 서로 뒤섞이면 각각을 떼어놓고 다루지 못하게 된다.
- 처음보는 코드가 다른 어떤 부분과 연결됐다면 둘 모두를 머릿속에 넣어야 한다. → 서로 얽혀있는 것들은 부담을 가중시킨다. (즉 이러한 복잡성이 시스템을 이해하는 데 한계를 느끼게 한다.)
- 세상에서 가장 뛰어난 저글링 선수도 스무개, 백개의 공을 다룰 수 없다. 우리가 다루는 문제에 비해 그걸 이해하는 능력은 통계적으로 비슷하게 나쁘다.
변경
- 소프트웨어를 수정하기 위해서는 무엇을 하는 소프트웨어인지 분석하고 무엇을 해야하는지 결정해야한다.
- “이 변화는 어디까지 영향을 끼치지?”, “그러면 수정 사항을 적용할 부분은 어디지?”라고 질문을 던져야 한다.
디버깅
- 실무에서 발생하는 모든 버그들은 이미 타입 체커와 모든 테스트를 통과했다.
- 이러한 ‘가드레일’은 안전장치일 뿐 목적지로 가는 방향을 알려주지 않는다. (세상에 일부러 가드레일에 부딪히면서 운전하는 사람은 없다.)
- 결국 개발자가 프로그램을 추론할 수 있는 능력이 디버깅에 있어서 가장 중요하다.
개발 속도
- 쉬움을 쫓다보면 초반 속도는 빨라진다. 하지만 복잡성을 무시하면 장기적으로는 반드시 속도가 느려진다.
- 단순함을 쫓다보면 초기 속도가 나지 않을 수 있다. 문제를 단순한 형태로 바라보는 일에 시간을 써야한다.
- 두 가지를 다 잡는 일은 훌륭한 일이다. 다만 쉬움에만 집중하면 결국엔 복잡성이 발목을 잡는다.
쉬운 것은 사실 복잡하다
- 복잡한 결과물(artifacts)을 생성하는 도구(constructs)의 특징: 간단명료하게 설명할 수 있다. 굉장히 친숙하다. 사용하기 쉽다.
- 하지만 사용자들은 개발자들이 소프트웨어를 만드는 동안 얼마나 재밌었는지 관심 있을리가 없다. 이 프로그램으로 무엇을 할 수 있는지, 잘 동작하는지가 중요하다.
- 단순해야 복잡한 결과물을 만들 수 있다. “우발적인 복잡함” 선택한 도구에 내재되어 있다. (’우발적’을 쉽게 쓰면면 ‘니 탓’이란 뜻이라고 함🥲)
- 복잡함을 만드는 도구, 예를 들면 베틀과 같은 기계를 사용하여 뜨개질로 만든 성을 만들 수 있다. 이 뜨개질로 만든 성은 애자일을 도입하든, 테스트와 리팩토링 도구를 사용하든 레고로 만든 성보다 빨리 변경할 수 없다.
단순함의 장점
- 이해하기 쉽다. 수정하기 쉽다. 디버깅이 쉽다.
- 유연성이 높다. (policy, location. etc)
어떻게 해야 더 쉬워질까?
- 가까운 도구상자에 넣어두고 꺼내쓰자. (가까움의 측면)
- 책을 구해서 읽고, 튜토리얼을 따라서 해보고, 설명을 부탁한다. (익숙함의 측면)
- 우리가 더 많은 걸 배울 순 있지만 더 똑똑해 질 수는 없다. → 우리의 뇌가 복잡함에 가까이 갈 수는 없다. 대신 단순하게 만들어 뇌 근처로 가져와야 한다.
여러분의 도구상자엔 무엇이 있나요?
Complexity | Simplicity |
---|---|
State, Objects | Values |
Methods | Functions, Namespaces |
vars | Managed refs |
inheritance, switch, matching | Polymorphism a la carte |
Syntax | Data |
Imperative loops, fold | Set functions |
Actors | Queues |
ORM | Declarative data manipulation |
Conditionals | Rules |
Inconsistency | Consistency |
Complect (엮여있다)
- 끼워넣거나 휘감거나 땋는다는 뜻 → 소프트웨어를 나쁘게 만드는 것! 안좋은 것!
- fig.1 과 fig.6 는 똑같은 줄이지만 fig.6가 훨씬 이해하기 어렵다.
Compose (합성하다)
- 합성하다. → 함께 놓는다는 의미
- 우리는 항상 합성가능한 시스템을 원한다. (단순한 요소들을 같이 놓는 것(composing)이 강력한 소프트웨어를 만드는 방법이기 때문에)
Modularity and Simplicity
- 왼쪽 그림에서 파란 블럭과 노란 블럭은 서로를 호출하지 않더라도 완벽히 엮여있다.
- 요소를 격리할 방법: 추상화(오른쪽 그림의 흰색 점선으로 표시된 블럭 윗부분) → 파란 블럭이 노란 블럭을 전혀 모르고, 노란 블럭도 파란 블럭을 전혀 모른다. 단순함이 유지된다.
- 단순함을 분할하기(partitioning)와 계층짓기(stratification)과 연관시키지 말 것. 오히려 분할과 계층짓기가 가능하게 만드는 것이 단순함이다.
복잡함을 만드는 도구 (Complexity toolkit)
- State: 시간과 값들이 엮인다.(complect) → 같은 인자를 전달해도 결과는 달라진다. state를 감싼 모듈을 사용하면 사용하는 그 모듈도 엮여버린다.
- Objects: 상태(state), 식별자(identify), value를 엮어서 분리하지 못하게 한다.
- Methods: 보통 함수와 상태를 엮는다.
- Syntax: 단방향으로 의미와 순서를 엮는다. 문법은 데이터보다 모든 면에서 열등하다.
- Inheritance: 상속이라는 말 그대로 두 타입을 엮는다.
- Switch/matching: 무엇(what)이 일어나면 누가(who) 무슨일을 하는지의 한 쌍으로 엮는다.
- var(iable)s: 값과 시간을 밀접하게 엮는다.
단순함을 만드는 도구 (Simplicity Toolkit)
- Values: final같은 불변으로 선언할 수 있다.
- Functions: a.k.a stateless method
- Data: 데이터는 단순하다. 선형적이고 순차적이다. (Maps, arrays, sets, JSON, etc)
- 다형성 (polymorphism): 데이터 구조(data structures)와 함수 정의에 대한 집합(definitions of sets of functions)이 있고, 이것들을 연결할 수 있다. 이 셋은 모두 독립적인 작업이다.
- Set functions, Queues: 라이브러리
- 일관성(Consistency): Transaction 혹은 값을 사용해야 한다.
단순함을 위한 추상화
- 어떻게 단순함을 위해 추상화할 수 있을까? 추상이라는 단어는 하나가 아니라는 암시를 준다. 물리적인 성질을 빼내어 떨어뜨리는 것이다.
- 복잡한 것을 숨기는 것(hiding)은 추상화가 아니므로 구별해야한다.
- “모르겠고, 알고 싶지도 않아.” 같은 태도를 취하는 방법도 도움이 된다.
- 설계 작업으로써 육하원칙을 적용해 볼 수 있다. “여기서 ‘누가’에 해당하는 것은? ‘무엇’에 해당하는 것은?” 이라고 질문을 던지면 대상을 분해하는데 도움이 된다.
설계 육하원칙
무엇 (What)
- ‘무엇’이란 우리가 성취하려고 하는 것, 명령어(operations)이다.
- 우리는 함수로 추상화를 구현한다. (추가로 언어가 이터페이스, 타입클래스 등을 지원하면 사용한다.)
- 함수/인터페이스들은 아주 작아야한다. 거대하게 만들지말고 다형성 구조로 표현하라
- 실제 구현과 분리하여 값과 추상화에 대한 정의를 해야한다. 이 때 ‘어떻게’와 엮이지 않도록 주의한다. 즉 함수가 어떻게 작동해야하는지 넌지시 암시하는 등 설계에 ‘어떻게’를 욱여넣지말라.
- ‘무엇’과 ‘어떻게’를 확실하게 구분하는 것이 핵심이다. 이것을 잘 한다면 ‘어떻게’는 다른 곳으로 넘겨버릴 수도 있다.
누구 (Who)
- ‘누구’란 데이터나 엔티티에 대한 것
- 추상화는 결국 프로그램이 작동하는 방식에 따라 데이터나 엔티티와 연결된다.
- 하위 컴포넌트를 상위 컴포넌트에 주입하는 방식으로 가려면 하위 컴포넌트가 많아져야 한다. (선행 조건: 인터페이스가 충분히 작아야함) → 정책과 기능을 분리할 수 있다.
어떻게 (How)
- ‘어떻게’는 프로그램이 구행하는 일, 실제 구현 코드이다.
- 스위치문, 패턴 매칭 등으로 엮지말고 다형서 구조로 추상을 연결하라
- 추상 안에 구현을 암시하는 경우를 주의하라. 구현할 사람의 손발을 꽁꽁 묶어버리는 셈이다. 선언적으로 작성하라.
- 그 어떤 것도 ‘어떻게’와 섞지 말라. 모든 구현 코드들은 가능한 한 그 자체로 외딴섬이어야 한다.
언제, 어디서 (When, Where)
- 다른 것들과 엮이는 상황을 최대한 막아라 (엮임은 의도치않게 시작된다.)
- A가 처리하고 그 다음 작업은 B에서 처리한다고 하자. 여기서 A가 B를 호출하도록 변경하면 A와 B의 ‘언제’와 ‘어디서’를 엮은 것이다. (A가 B를 호출하려면 어디 있는지 알아야하고, ‘언제’호출했는지도 엮여있다.)
- 큐를 사용해서 문제를 해결할 수 있다.
왜 (Why)
- 애플리케이션의 규칙과 정책
- 외부에서 선언적으로 관리하라
결론
단순함은 궁극적인 정교함이다. - 레오나르도 다 빈치
단순함은 선택에 달려있다.
- 복잡한 결과물을 만드는 도구를 계속 사용하면 복잡함을 벗어나지 못한다. (확증편향) → 가드레일이 단순함을 유발하지 않음을 명심하자
- 단순함을 위한 관심과 감각을 길러야한다. 단순함과 쉬움은 완전히 다른 것이다. ‘엮임’을 감지하는 레이더를 가져야한다. 요소들이 연결되는 부분을 찾고 독립시킬 수 있는지 살펴보라.
- ‘신뢰성’ 도구 (testing, refactoring, type systems)은 보조 도구(안전망)일 뿐 단순함에 대한 도구가 아니다. 이 도구들은 문제의 본질을 건들이지 않는다.
어떻게 단순하게 만들 수 있을까?
- 단순한 결과물을 만드는 도구를 선택하라. 복잡한 결과물을 만드는 도구를 사용하지 말라.
- 단순함이라는 기초 위에 추상(abstraction)을 생성하려고 노력하라. 단순함은 숫자가 적다는 뜻이 아니다. 몇개 안되는 줄이 엮여있는 것보다 많은 줄이 곧게 뻗어 이어진 편이 낫다.