- 오래 프로그래밍한 “작은 뇌” 개발자의 경험담을 통해, 소프트웨어 개발의 가장 큰 적은 복잡성이며 이를 줄이는 태도가 실무의 중심이어야 함을 강조함
- 복잡성을 막는 기본 무기는 “no”라고 말하기이고, 타협이 필요할 때는 80/20 해법·프로토타입·작은 리팩터링·늦은 추상화로 현실적인 해법을 찾음
- 테스트는 코드가 어느 정도 굳은 뒤 통합 테스트를 중심에 두고, 단위 테스트와 소수의 엔드투엔드 테스트를 보조로 쓰며, 버그는 회귀 테스트로 먼저 재현한 뒤 고침
- 도구, 타입 시스템, 로깅, 디버거, 단순한 API는 개발자의 기억과 추론 부담을 줄여주지만, 과도한 제네릭·콜백·마이크로서비스·프런트엔드 프레임워크는 복잡성을 키울 수 있음
- 기존 코드와 조직 프로세스에는 겸손하게 접근하고, 이해하지 못한 코드를 함부로 제거하지 않으며, “너무 복잡하다”고 말할 수 있는 문화가 필요함
복잡성은 개발자의 영원한 적
- 소프트웨어 개발에서 가장 위험한 적은 복잡성임
- 복잡성은 코드베이스에 서서히 들어와, 한 곳을 바꾸면 관련 없어 보이는 곳까지 깨지게 만듦
- 그럭은 이를 보이지 않는 “악마”에 비유하며, 개발자가 직접 보거나 쉽게 때려잡을 수 없는 존재로 다룸
- 복잡성은 선의의 개발자나 프로젝트 매니저를 통해서도 들어올 수 있음
- 기능, 추상화, 프로세스가 늘어날수록 코드는 이해하기 어려워짐
- 그럭 자신도 때로는 복잡성을 들여온 장본인이었다고 인정함
“no”와 “ok”의 사용법
- 복잡성을 막는 가장 강한 무기는 “no” 임
- 필요 없는 기능을 만들지 않음
- 필요 없는 추상화를 만들지 않음
- 다만 “no”는 좋은 엔지니어링 조언이어도, 경력상 항상 유리하지는 않음
- “yes”는 더 많은 보상이나 관리직으로 이어질 수 있음
- 그래도 개발자로서 자신에게 진실하려면 “no”가 중요함
- 타협이 필요할 때는 “ok” 라고 답한 뒤 80/20 해법을 찾음
- 80/20 solution은 20%의 코드로 80%의 요구를 만족시키는 접근임
- 모든 장식적 기능을 갖추지는 못해도 대부분의 가치를 전달하고 복잡성을 억제할 수 있음
- 때로는 프로젝트 매니저에게 세부 구현 방식을 모두 설명하지 않고 단순한 방식으로 해결하는 편이 낫다고 봄
코드 구조화와 리팩터링
- 프로젝트 초기에 애플리케이션을 너무 빨리 나누지 말아야 함
- 초반에는 시스템의 모양이 아직 불분명하고, 무엇을 만드는지도 완전히 이해되지 않은 상태임
- 시간이 지나면 좋은 절단점(cut point) 이 드러남
- 좋은 절단점은 나머지 시스템과의 인터페이스가 좁음
- 적은 수의 함수나 추상화로 내부 복잡성을 감춤
- 그럭은 이를 복잡성 악마를 수정 안에 가두는 것에 비유함
- 큰 뇌 개발자가 프로젝트 초기에 많은 추상화를 만들려 할 때는 피해를 줄이는 방식이 필요함
- UML 다이어그램처럼 코드에 직접 피해를 주지 않는 산출물로 돌릴 수 있음
- “내일 동작하는 데모”를 요구하면 실제로 작동하는 코드와 현실을 빨리 보게 됨
- 이 데모 접근을 프로토타입이라고 부를 수 있음
- 리팩터링은 프로젝트 후반에 코드가 굳은 뒤 유용함
- 큰 리팩터링일수록 실패할 가능성이 커짐
- 시스템이 계속 동작하는 상태를 유지하며 작은 단계로 진행하는 편을 선호함
- 엔드투엔드 테스트는 리팩터링 중 생명줄이 되지만, 깨졌을 때 원인을 이해하기 어려울 수 있음
- 과도한 추상화는 리팩터링 실패와 시스템 실패로 이어질 수 있음
- J2EE는 과도한 추상화의 예로 등장함
- OSGi 도입은 복잡성을 줄이려던 시도였지만 오히려 더 강한 복잡성을 만들었고, 여러 인년의 재작업이 필요했다고 함
테스트 전략
- 테스트는 많은 시간을 구해주지만, “무조건 테스트 먼저”에는 회의적임
- 아직 도메인을 이해하지 못한 상태에서는 무엇을 테스트할지 알기 어려움
- 그럭은 프로토타입 이후 코드가 굳기 시작할 때 대부분의 테스트를 쓰는 편을 선호함
- 테스트를 나중에 쓰더라도 규율이 필요함
- “내 기계에서는 동작한다”는 이유로 테스트를 건너뛰면 안 됨
- 다른 기계나 미래의 같은 기계에서 동작한다는 보장이 없음
- 단위 테스트, 통합 테스트, 엔드투엔드 테스트의 역할을 구분함
- unit tests는 프로젝트 초반에 도움이 되지만 구현 변경에 따라 잘 깨지고 리팩터링을 어렵게 만들 수 있음
- end to end 테스트는 전체 시스템 동작을 보여주지만, 깨졌을 때 원인을 파악하기 어렵고 자주 깨지면 무시될 수 있음
- integration tests는 시스템 정확성을 확인할 만큼 높고, 디버거로 원인을 볼 만큼 낮아 “스위트 스팟”에 가까움
- 테스트 구성의 이상적인 형태는 다음과 같음
- 초반에는 일부 단위 테스트
- 절단점이 생기고 시스템이 안정될수록 강한 통합 테스트
- 가장 흔한 UI 기능과 중요한 엣지 케이스만 담은 작고 잘 관리되는 엔드투엔드 테스트
- 모킹은 드물게만 사용하고, 필요할 때도 시스템 경계 같은 큰 단위에서만 쓰는 편을 선호함
- 버그를 고칠 때는 예외적으로 먼저 회귀 테스트로 버그를 재현한 뒤 수정함
프로세스, 애자일, 기존 코드에 대한 태도
- 애자일은 최악은 아니지만 좋다고만 보기도 어려움
- 개발 조직화 방식으로 어느 정도 괜찮을 수 있음
- 실패할 때마다 “애자일을 제대로 하지 않았다”고 말하는 애자일 전문가를 경계함
- 성공에는 프로토타입, 도구, 좋은 개발자 채용이 더 중요함
- 애자일 프로세스는 도움이 될 수 있지만, 너무 진지하게 받아들이면 해가 될 수 있음
- no silver club, 즉 모든 소프트웨어 문제를 해결하는 은탄환은 없음
- Chesterton’s Fence는 기존 코드 제거에 대한 경고로 쓰임
- 어떤 울타리의 용도를 모르면 먼저 이해해야 하며, 바로 치우면 안 된다는 내용임
- 못생긴 코드라도 오늘 동작하고 있다면 존중해야 함
- 특히 큰 시스템일수록 먼저 이해한 뒤 개선해야 함
- 테스트는 왜 어떤 “울타리”가 존재하는지 알려주는 힌트가 될 수 있음
도구와 타입 시스템
- 도구는 개발자가 직접 기억하고 추론해야 할 부담을 줄여줌
- 새 환경에 들어가면 주변 도구를 배우는 데 시간을 쓰는 편이 생산성을 높일 수 있음
- 문서가 없으면 다른 개발자에게 물어보며 파악해야 할 수 있음
- IDE의 코드 완성은 API를 모두 기억하지 않아도 되게 해줌
- Java 프로그래밍은 코드 완성 없이는 거의 불가능하다고 표현함
- 좋은 디버거는 매우 중요함
- 조건부 중단점, 표현식 평가, 스택 탐색 같은 기능을 깊이 배워야 함
- 새 개발자에게 디버거 학습은 대학 수업보다 컴퓨터에 대해 더 많이 가르칠 수 있다고 봄
- 타입 시스템의 가장 큰 가치는 “점(.)을 눌렀을 때 할 수 있는 일이 뜨는 것”임
- 타입 정확성도 좋지만, 그럭에게는 도구 지원과 코드 완성이 더 큰 가치임
- 과도한 타입 추상화나 제네릭은 실제 업무 코드를 어렵게 만들 수 있음
- 제네릭은 주로 컨테이너 클래스에 제한해 쓰는 편을 선호함
표현식, DRY, 관심사 분리
- 짧은 코드보다 디버깅하기 쉬운 코드를 선호함
- 복잡한 조건식을 한 줄에 쓰기보다 중간 변수로 나누면 각 표현식의 결과와 의미를 보기 쉬움
- 줄 수가 늘어나더라도 조건문의 이해와 디버깅이 쉬워짐
- DRY는 좋은 조언이지만 균형이 필요함
- 단순하고 명확한 중복 코드는 복잡한 콜백, 클로저, 객체 모델보다 나을 수 있음
- 중복을 제거하려다 복잡성이 커지면 손해가 될 수 있음
- Separation of Concern에 대해서는 더 비판적임
- 웹 개발의 전형적 예는 CSS, HTML, JavaScript 분리임
- 그럭은 대안으로 locality of behavior를 선호함
- 동작하는 대상 가까이에 관련 코드를 두면, 그 대상을 볼 때 무엇을 하는지 바로 알 수 있음
- 클로저는 컬렉션 작업 추상화 같은 올바른 용도에는 유용함
- 하지만 소금, 타입 시스템, 제네릭처럼 조금만 써도 충분하며 과하면 해로움
- JavaScript의 “callback hell”은 클로저 남용의 예로 나옴
로깅, 동시성, 최적화
- 로깅은 특히 클라우드 배포 환경에서 매우 중요함
- 주요 논리 분기마다 로그를 남김
- 요청이 여러 머신에 걸치면 모든 로그에 request ID를 넣어 묶을 수 있게 함
- 가능하면 로그 레벨을 동적으로 조절함
- 가능하면 사용자별 로그 레벨도 조절함
- Java 로깅 라이브러리는 복잡할 수 있지만, 로깅 인프라에 제대로 투자하면 나중에 큰 보상이 돌아옴
- 동시성은 두려워해야 할 대상임
- 가능한 한 상태 없는 웹 요청 핸들러와 독립적인 원격 작업 큐처럼 단순한 모델을 사용함
- optimistic concurrency는 웹 영역에서 잘 동작한다고 봄
- thread local variable은 주로 프레임워크 코드를 쓸 때 가끔 사용함
- Java의 ConcurrentHashMap 같은 동시성 자료구조도 여전히 조심해서 써야 함
- 최적화는 실제 성능 프로파일이 있을 때 시작해야 함
- “premature optimization is the root of all evil”에 동의함
- 실제 병목은 예상과 다를 수 있음
- CPU만 보지 말아야 하며, 네트워크 호출은 수백만 CPU 사이클에 해당할 수 있으므로 가능한 줄여야 함
API, 파싱, 방문자 패턴
- 좋은 API는 개발자가 많이 생각하지 않게 만듦
- 나쁜 API는 구현 내부나 도메인 모델 중심으로 설계되거나, 지나치게 추상적일 때 생김
- 단순한 경우에는 단순한 API를 제공하고, 복잡한 경우에는 더 복잡한 API를 제공하는 계층화를 선호함
- 객체지향 API는 기능을 대상 객체에 붙이는 편이 낫다고 봄
- Java에서 리스트를 필터링하려고 Stream으로 바꾸고 다시 List로 수집해야 하는 흐름을 나쁜 예로 듦
- 흔한 작업인
filter()는 list에 있고 list를 반환해야 한다고 봄
- 파서는 recursive descent가 재미있고 아름다운 방식임
- 파서 생성기는 이해하기 어렵고 디버깅하기 어렵다고 비판함
- 실제 프로덕션 파서는 거의 항상 재귀 하강 방식이라고 주장함
- Bob Nystrom의 Crafting Interpreters를 추천함
- 온라인에서 무료로 볼 수 있지만 책 구매도 추천함
- 다만 visitor pattern은 함정이라고 표현함
- Visitor pattern에 대한 평가는 짧게 “bad”임
프런트엔드, 마이크로서비스, 유행
- 마이크로서비스는 “시스템을 제대로 나누는 가장 어려운 문제에 네트워크 호출까지 추가한다”는 점에서 위험함
- 프런트엔드 개발에서는 복잡성이 특히 강함
- 단순한 폼 저장이나 브로슈어 사이트에도 SPA 라이브러리, GraphQL JSON API, HTTP 백엔드가 사용되는 상황을 비판함
- 프런트엔드와 백엔드를 나누면 복잡성의 거처가 두 개 생긴다고 표현함
- 그럭은 복잡성을 낮추기 위해 htmx와 hyperscript를 만들었다고 밝힘
- 단순한 HTML을 유지하고 JavaScript를 많이 쓰지 않는 방향을 선호함
- React는 취업과 특정 종류의 애플리케이션에는 더 좋을 수 있다고 인정함
- 개발에는 유행이 많고, 특히 프런트엔드에서 그렇다고 봄
- 백엔드는 더 지루해졌고 많은 나쁜 아이디어가 이미 시도되었다고 표현함
- 혁명적 새 접근에는 소금을 한 알 치듯 조심스럽게 접근하라고 함
- 많은 아이디어는 이미 한 번쯤 시도되었고, 재활용된 나쁜 아이디어가 시간을 낭비할 수 있음
두려움과 가면 증후군
- 시니어 개발자가 공개적으로 “이건 너무 복잡하다”고 말하는 것은 좋음
- 개발자들은 Fear Of Looking Dumb(FOLD) 때문에 이해하지 못한다고 말하기 어려워함
- 시니어가 먼저 인정하면 주니어도 복잡함과 이해 부족을 말할 수 있음
- FOLD는 특히 젊은 개발자에게 복잡성이 힘을 얻는 주요 원천임
- 유머 감각과 과거 실패 사례를 기억하는 태도가 도움이 된다고 봄
- 가면 증후군도 개발에서 흔함
- 그럭은 자신이 모든 것을 지배하는 느낌과 무엇을 하는지 모르는 느낌 사이에 있다고 표현함
- htmx와 _hyperscript의 오픈소스 성과가 있어도 여전히 실수와 실패를 두려워한다고 함
- 모두가 가면 증후군을 느낀다면 아무도 진짜 가짜는 아니라는 식으로 받아들이는 편이 낫다고 봄
추천 읽을거리와 결론
- 추천 읽을거리
- 마지막 결론은 단순함
- 복잡성은 매우, 매우 나쁨