- 오랜 개발 경험에서 반복되는 가장 큰 교훈은 복잡성이 코드베이스의 “영원한 적”이며, 작은 변경이 엉뚱한 곳을 깨뜨리는 상태를 만든다는 점임
- 복잡성을 줄이는 가장 강한 도구는 기능과 추상화에 “no”라고 말하는 것이지만, 현실적 타협이 필요할 때는 80/20 해법으로 대부분의 가치를 적은 코드로 전달함
- 설계와 테스트는 너무 이른 추상화보다 시스템의 형태가 드러난 뒤 좋은 경계점을 찾는 쪽에 무게를 두며, 단위 테스트보다 오래 유지되는 통합 테스트를 중시함
- 도구, 타입 시스템, 로깅, 디버거, API 설계는 개발자의 인지 부담을 줄여야 하며, 타입 시스템의 큰 실용 가치는 IDE 자동완성에 있음
- DRY, 관심사 분리, 마이크로서비스, SPA, 제네릭, 과한 리팩터링처럼 좋은 이름의 기법도 코드 이해와 유지보수를 어렵게 만들면 복잡성의 통로가 됨
복잡성은 코드베이스의 최상위 위험
- 복잡성은 처음에는 이해 가능하던 시스템을 갑자기 다루기 어렵게 만들고, 한 곳의 변경이 관련 없어 보이는 다른 곳을 깨뜨림
- 좋은 의도를 가진 개발자나 프로젝트 매니저를 통해 복잡성이 들어올 수 있으며, 때로는 자신이 들여온 복잡성이 문제의 원인이 됨
- 복잡성을 해결하는 만능 “클럽”은 없고, 가장 좋은 대응은 처음부터 들이지 않는 것임
“no”와 “ok”: 거절과 현실적 타협
- 복잡성을 막는 가장 좋은 무기는 “no”임
- “그 기능은 만들지 않음”
- “그 추상화는 만들지 않음”
- 다만 “no”는 좋은 엔지니어링 조언이어도, 더 많은 보상이나 승진에는 불리할 수 있음
- 현실적으로 타협해야 할 때는 “ok”라고 답한 뒤 80/20 solution을 찾음
- 원하는 가치의 80%를 코드의 20%로 구현하는 방식
- 모든 장식 기능을 갖추지 못하고 조금 못생길 수 있지만, 대부분의 가치를 전달하면서 복잡성을 낮게 유지함
- 프로젝트 매니저가 세부 요구를 잊거나 이동하거나 퇴사하는 경우도 있으므로, 때로는 80/20 방식이 더 나은 선택이 됨
설계와 코드 분해는 너무 이르면 위험함
- 프로젝트 초반에는 시스템의 형태가 아직 “물”처럼 불명확하므로 너무 이른 분해를 피함
- 시간이 지나면 좋은 경계점이 드러남
- 나머지 시스템과의 인터페이스가 좁음
- 적은 수의 함수나 추상화로 내부 복잡성을 감춤
- 추상화를 너무 일찍 만들면 잘못된 추상화가 생기기 쉬워서, 기다리는 쪽에 더 무게를 둠
- 초반에 큰 추상화를 만들려는 개발자에게는 UML 같은 버릴 수 있는 산출물을 주거나, 내일 동작하는 데모를 요구하는 방식이 도움이 됨
- 초기 프로토타입은 실제로 동작하는 코드와 현실적인 제약을 더 빨리 보여줌
테스트는 “나중에 몰아서”가 아니라 “형태가 잡히며 함께”
- 테스트는 많은 문제를 막아주지만, 도메인도 이해하기 전부터 test first를 강제하는 방식에는 반대함
- 프로토타입 이후 코드가 단단해지기 시작할 때 대부분의 테스트를 작성하는 편을 선호함
- “내 기계에서는 됨”이라며 테스트를 건너뛰는 태도는 매우 나쁨
- 테스트 유형별 선호는 뚜렷함
- unit tests: 프로젝트 초반에는 유용하지만 구현 변경에 쉽게 깨지고 리팩터링을 어렵게 만들 수 있음
- end to end: 전체 시스템 동작을 보여주지만, 깨졌을 때 원인을 이해하기 어렵고 자주 깨지면 무시되기 쉬움
- integration tests: 시스템 정확성을 볼 만큼 높고, 디버거로 원인을 찾을 만큼 낮아 가장 좋은 지점에 가까움
- 경계점 API가 안정되면 그 주변에 통합 테스트를 강하게 투자함
- end-to-end 테스트는 가장 흔한 UI 기능과 중요한 일부 edge case에 집중해 작고 잘 관리되는 모음으로 유지함
- mocking은 꼭 필요할 때만, 가능하면 큰 경계점 단위로만 사용함
- 버그가 발견된 경우에는 예외적으로 먼저 회귀 테스트로 재현한 뒤 수정하는 방식을 선호함
Agile, 리팩터링, Chesterton's Fence
- Agile 자체는 최악은 아니며 개발 조직 방식 중 하나로 쓸 수 있지만, 실패의 면책 도구처럼 쓰는 “agile shaman”은 경계해야 함
- 프로토타입, 좋은 도구, 좋은 개발자 채용이 소프트웨어 성공에 더 중요하며, 어떤 프로세스도 모든 문제를 해결하는 silver bullet은 아님
- 리팩터링은 특히 프로젝트 후반 코드가 단단해졌을 때 유용하지만, 큰 리팩터링일수록 실패 가능성이 커짐
- 작게 나누어 진행함
- 시스템이 가능한 한 계속 동작하는 상태를 유지함
- 각 단계가 끝난 뒤 다음 단계로 넘어감
- 과한 추상화는 리팩터링 실패와 시스템 실패로 이어질 수 있음
- Chesterton's Fence는 보기 싫은 코드라도 왜 존재하는지 이해하기 전에는 제거하지 말라는 태도와 연결됨
- 동작 중인 코드는 완벽하지 않아도 존중해야 하며, 큰 시스템일수록 먼저 이해하는 시간이 필요함
마이크로서비스와 도구
- 마이크로서비스는 시스템을 제대로 나누는 어려운 문제에 네트워크 호출까지 추가함
- 도구는 개발자의 생산성과 이해력을 크게 높임
- 새로운 환경에서는 도구를 배우는 데 시간을 쓰는 편이 좋음
- 2주간 도구를 익히면 개발 속도가 2배 빨라질 수 있음
- 문서가 없으면 다른 개발자에게 물어보고 파고들 필요가 있음
- IDE의 코드 완성은 API를 모두 기억하지 않아도 되게 해주며, Java 개발에는 거의 필수에 가까움
- 좋은 디버거는 매우 중요함
- 조건부 중단점
- 표현식 평가
- 스택 탐색
- 새 개발자는 사용 가능한 디버거를 깊이 배워야 하며, 이는 대학 수업보다 컴퓨터에 대해 더 많이 가르칠 수 있음
타입 시스템, 표현식, DRY
- 타입 시스템의 가장 큰 가치는 점을 찍었을 때 가능한 동작 목록이 나타나는 도구 지원에 있음
- 타입 정확성도 좋지만, 코드 완성과 탐색이 더 큰 실용적 가치를 가짐
- 타입 시스템을 지나치게 추상적으로 사용하는 개발자는 경계 대상임
- 제네릭은 특히 위험할 수 있음
- 대부분의 경우 컨테이너 클래스 정도로 제한하는 편을 선호함
- 짧은 조건식을 한 줄에 몰아 쓰기보다, 중간 변수에 의미 있는 이름을 붙여 나누는 편이 디버깅과 이해에 좋음
- DRY는 좋은 조언이지만 균형이 필요함
- 단순하고 명확한 중복 코드는 때로 복잡한 콜백, 클로저, 객체 모델보다 나음
- 중복 제거 자체가 복잡성을 키우면 손해가 될 수 있음
관심사 분리, 클로저, 로깅
- Separation of Concern은 강력한 아이디어지만, 실제로는 여러 파일을 오가며 동작을 이해해야 해 시간을 낭비하게 만들 수 있음
- 대안으로 locality of behavior를 선호함
- “동작하는 것” 가까이에 코드를 두면, 그 대상을 볼 때 무엇을 하는지 바로 알 수 있음
- 클로저는 컬렉션 연산을 추상화하는 데 적합하지만, 소금처럼 적은 양으로 충분함
- JavaScript의 “callback hell”은 클로저를 과하게 사용한 복잡성의 예임
- 로깅은 특히 클라우드 배포 환경에서 매우 중요함
- 주요 논리 분기마다 로그를 남김
- 여러 머신을 지나는 요청에는 request ID를 포함해 로그를 묶을 수 있게 함
- 가능하면 로그 레벨을 동적으로 조절함
- 가능하면 사용자별 로그 레벨을 조절해 특정 사용자 문제를 디버깅함
- 로깅 라이브러리는 복잡할 수 있지만, 로깅 인프라를 제대로 만드는 투자는 나중에 큰 보상을 줌
동시성, 최적화, API
- 동시성은 두려워할 만한 영역이며, 가능한 한 단순한 모델을 선호함
- 상태 없는 웹 요청 핸들러
- 서로 의존하지 않는 원격 작업 큐
- 단순한 API
- 웹에서는 optimistic concurrency가 잘 맞을 수 있음
- Java의 ConcurrentHashMap 같은 동시성 자료구조도 조심해서 써야 함
- 최적화는 실제 성능 프로파일로 구체적 문제가 확인된 뒤 시작해야 함
- CPU만 보지 말아야 함
- 네트워크 접근은 많은 CPU 사이클에 해당하므로 가능하면 줄여야 함
- 중첩 루프를 보고 바로
O(n^2)제거에 들어가면 복잡성이 커질 수 있음
- 좋은 API는 개발자가 많이 생각하지 않아도 되게 해야 함
- API 작성자가 구현 관점이나 도메인 관점에 갇히면 사용하기 어려워짐
- 단순한 경우에는 단순한 API를 제공하고, 복잡한 경우에는 더 복잡한 API를 따로 제공하는 계층화가 좋음
- 객체지향이라면 동작은 가능한 한 그 대상 위에 있어야 함
- Java의 stream/collector 예시는 흔한 작업을 불필요하게 우회하게 만드는 API로 비판받음
파싱, Visitor Pattern, 프런트엔드
- 파서는 recursive descent 방식이 재미있고 아름답다고 봄
- parser generator는 생성 코드의 이해와 디버깅이 어렵고, 문법의 재귀적 성질을 숨김
- 실제 프로덕션 파서는 대부분 recursive descent이며, Bob Nystrom의 Crafting Interpreters를 추천함
- Visitor pattern은 “bad”로 짧게 정리됨
- 프런트엔드와 백엔드를 분리하고 SPA 라이브러리와 GraphQL JSON API를 HTTP로 연결하는 방식은 복잡성의 둥지를 두 개 만드는 일에 가까움
- 단순한 웹사이트나 폼을 데이터베이스에 넣는 작업에도 큰 프런트엔드 라이브러리를 쓰는 흐름을 경계함
- 복잡성을 낮추기 위해 htmx와 hyperscript를 만들었음
- 단순한 HTML을 유지함
- 많은 JavaScript를 피함
- React는 일자리와 일부 애플리케이션 유형에는 더 낫지만, 프런트엔드 복잡성의 길로 들어서는 선택으로 봄
유행, FOLD, impostor syndrome
- 개발에는 유행이 많고, 특히 프런트엔드에서 더 두드러짐
- 혁명적이라는 새 접근에는 의심을 가져야 함
- 컴퓨터 분야에서는 이미 많은 아이디어가 한 번쯤 시도되었음
- 나쁜 아이디어가 새 이름으로 돌아올 수 있음
- senior 개발자가 공개적으로 “이건 너무 복잡해서 이해하기 어렵다”고 말하는 것은 좋음
- FOLD는 Fear Of Looking Dumb의 약자로, 멍청해 보일까 봐 복잡함을 인정하지 못하는 상태임
- senior가 복잡하다고 말하면 junior도 이해하지 못함을 인정하기 쉬워짐
- FOLD는 특히 젊은 개발자에게 복잡성이 힘을 얻는 주요 원천임
- 개발자는 자신이 모든 것을 지배하는 느낌과 아무것도 모르는 느낌 사이를 오가며, impostor syndrome은 흔함
- 모두가 impostor처럼 느낀다면 누구도 impostor가 아니며, 젊은 개발자는 좌절과 걱정이 있어도 커리어를 이어갈 수 있음
추천 읽을거리와 결론
- 추천 읽을거리
- 마지막 결론은 처음과 같음: 복잡성은 매우, 매우 나쁨