금요일 오후 4시, 슬랙에 버그 리포트가 올라왔다. 장바구니에서 수량을 변경하면 가격이 0원으로 표시되는 버그. 재현 경로도 명확했고, 코드를 보니 원인이 금방 보였다. 수량 변경 시 API 응답을 기다리는 동안 로컬 상태가 초기화되는 타이밍 이슈였다. 상태 업데이트 순서를 바꾸고, 옵티미스틱 업데이트를 추가하고, 테스트 케이스 하나 작성했다. 30분 만에 PR 올리고, 리뷰 받고, 배포. 끝.
근데 이게 끝이 아니었다.
1차 사고와 2차 사고
Howard Marks라는 투자자가 있다. 이 사람이 1차 사고(first-order thinking)와 2차 사고(second-order thinking)를 명확하게 구분했다. 1차 사고는 "이 문제의 답은 뭐지?"다. 단순하고, 빠르고, 누구나 할 수 있다. 2차 사고는 "이 답이 불러올 결과는 뭐지? 그 결과의 결과는?"이다. 복잡하고, 느리고, 의도적인 노력이 필요하다.
Morgan Housel은 이걸 일상의 의사결정에 적용했다. 배고프니까 초콜릿을 먹는 건 1차 사고다. 초콜릿을 먹으면 당장 배고픔은 해결되지만, 한 시간 뒤에 혈당이 떨어지면서 더 피곤해지고, 이게 반복되면 건강에 영향을 준다는 걸 고려하는 건 2차 사고다.
Ray Dalio는 더 직접적으로 말했다. "2차, 3차 결과를 고려하지 못하는 것이 고통스럽게 잘못된 의사결정의 원인이다."
이걸 개발에 대입하면 어떻게 될까.
버그 수정의 1차 사고와 2차 사고
아까 그 장바구니 버그로 돌아가자.
1차 사고: "수량 변경 시 가격이 0원으로 표시되니까, 상태 업데이트 순서를 고치자."
2차 사고: "왜 이 버그가 생겼지? API 응답을 기다리는 동안 로컬 상태가 초기화되는 패턴이 장바구니에만 있나, 아니면 다른 곳에도 있나?"
2차 사고를 시작하자 보이는 게 달라졌다. 코드베이스를 검색해보니, 비슷한 패턴이 위시리스트, 쿠폰 적용, 배송지 변경에서도 쓰이고 있었다. 다 같은 구조였다. 서버 상태를 로컬 상태에 복사하고, API를 호출하고, 응답이 오면 다시 로컬 상태를 업데이트하는 패턴. 이 패턴 자체가 타이밍 이슈에 취약한 거였다.
3차 사고까지 가면: "이런 패턴이 반복되는 이유는 뭘까? 서버 상태와 클라이언트 상태의 동기화에 대한 팀 차원의 가이드라인이 없기 때문이다. TanStack Query 같은 서버 상태 관리 도구를 쓰고 있지만, 언제 옵티미스틱 업데이트를 쓰고 언제 서버 응답을 기다릴지에 대한 규칙이 없다."
장바구니 버그 하나를 고치는 건 30분이 걸렸다. 비슷한 패턴을 프로젝트 전체에서 찾아서 수정하는 건 3일이 걸렸다. 서버 상태 동기화 가이드라인 문서를 작성하는 건 1주일이 걸렸다. 근데 그 1주일이 앞으로 발생할 수십 개의 비슷한 버그를 예방했다. 1차 사고에 머물렀으면 그 수십 개를 하나씩 고쳤을 거다.
코드 리뷰에서의 2차 사고
코드 리뷰는 2차 사고의 훈련장이다.
주니어 시절에 코드 리뷰를 받으면서 당황했던 적이 있다. 내 코드에 기능적 문제가 없는데도, 시니어가 "이 방식으로 하면 나중에 X가 됐을 때 어떻게 할 거야?"라는 코멘트를 달았다. 당시엔 "지금 잘 되는데 왜 미래 걱정을 하지?"라고 생각했다. 그게 1차 사고였다.
지금은 코드 리뷰에서 제일 먼저 보는 게 "이 코드가 6개월 뒤에 어떤 상태일까"다. 구체적으로:
변수 네이밍 하나의 파급력. PR에서 data라는 변수명이 보이면 코멘트를 남긴다. 지금은 이 컴포넌트에 데이터 소스가 하나라서 data가 직관적이다. 근데 6개월 뒤에 데이터 소스가 2개가 되면? data1, data2가 되거나, 뒤늦게 리네이밍을 해야 한다. 처음부터 userProfile이라고 짓는 게 2차 사고다.
추상화 레벨의 결과. 같은 API 호출 로직이 3곳에서 반복되니까 커스텀 훅으로 빼는 PR이 올라왔다. 1차 사고로는 "좋은 리팩토링이네" 하고 승인한다. 2차 사고로는 이런 질문을 한다. "이 3곳의 API 호출이 정말 같은 건가, 아니면 우연히 지금 같은 건가? 나중에 하나만 다르게 동작해야 할 때 이 추상화가 오히려 방해가 되진 않을까?" 성급한 추상화는 중복보다 나쁠 수 있다.
타입 설계의 미래. TypeScript에서 status: string 대신 status: 'active' | 'inactive'로 유니온 타입을 쓰라는 건 당장의 타입 안전성 때문만이 아니다. 6개월 뒤에 'suspended' 상태가 추가될 때, string으로 되어 있으면 해당 상태를 처리하지 않는 코드를 찾을 수가 없다. 유니온 타입이면 컴파일러가 다 잡아준다. 이것도 2차 사고다.
아키텍처 결정에서의 "그래서 그 다음은?"
Farnam Street의 2차 사고 프레임워크에서 핵심 질문은 "And then what?"이다. 10분 뒤, 10개월 뒤, 10년 뒤를 생각하라는 거다. 아키텍처 결정에 적용하면 이렇다.
팀에서 마이크로 프론트엔드를 도입하자는 제안이 있었다.
1차 사고: "팀별로 독립 배포가 가능해지니까 개발 속도가 빨라지겠다."
10분 뒤 (And then what?): "각 팀이 독립적으로 기술 스택을 선택할 수 있게 되면, A팀은 React 18, B팀은 React 19, C팀은 Vue를 쓰겠다고 할 수도 있다."
10개월 뒤 (And then what?): "기술 스택이 파편화되면, 공통 컴포넌트 라이브러리를 유지하기가 어려워진다. 각 팀이 비슷한 컴포넌트를 각자 만들기 시작한다. 디자인 일관성이 깨진다."
10년은 아니더라도 2년 뒤 (And then what?): "프레임워크 간 공유 상태가 필요해질 때, 복잡한 어댑터 레이어가 생긴다. 새 개발자 온보딩 시 모든 스택을 알아야 한다. 처음에 해결하려던 '배포 속도' 문제보다 더 큰 문제가 생긴다."
이 사고 과정을 거치고 나서, 마이크로 프론트엔드가 아니라 모노레포 + 명확한 모듈 경계라는 대안을 선택했다. 독립 배포의 이점은 일부 포기하지만, 기술 스택 통일성과 공유 코드의 장점을 유지할 수 있었다.
커리어에서의 2차 사고
2차 사고는 코드에만 적용되는 게 아니다. 커리어 결정에서도 마찬가지다.
"이 회사가 연봉을 20% 더 준다."라는 건 1차 사고다. "연봉이 20% 높지만, 팀 규모가 작아서 시니어에게 배울 기회가 적다. 1-2년 뒤 성장이 정체되면, 그때의 이직 경쟁력은? 연봉 20%를 포기하고 기술적으로 깊은 팀에서 2년 배운 뒤의 시장 가치는?"라는 건 2차 사고다.
"이 기술이 인기 있으니까 배워야겠다."는 1차 사고다. "이 기술을 배우는 데 3개월이 든다. 그 3개월 동안 못하는 다른 것은 뭐지? 이 기술의 시장 수요가 2년 뒤에도 유지될까? 아니면 이미 포화 상태인 기술에 뒤늦게 뛰어드는 건가?"는 2차 사고다.
2024년 초에 동료 하나가 고민을 공유했다. 안정적인 대기업에서 초기 스타트업으로 이직을 고려하고 있었다. 스타트업에서 CTO 오퍼를 받았다고. 1차 사고로는 "CTO 타이틀! 의사결정권! 성장 가능성!"이다. 같이 2차 사고를 해봤다. 초기 스타트업이면 사실상 혼자 모든 기술 결정을 내려야 한다. 검증해줄 시니어가 없다. CTO라는 타이틀이 있지만 팀이 없다. 채용을 해야 하는데, 회사 인지도가 없으니 채용이 어렵다. 좋은 개발자를 뽑기 어려우면 혼자서 계속 달리게 되고, 번아웃이 온다. 번아웃이 오면 의사결정 품질이 떨어지고... 이렇게 연쇄 반응을 그려봤다.
결국 그 동료는 좀 더 큰 규모의, 하지만 여전히 성장 단계인 시리즈 B 스타트업으로 이직을 택했다. 리드 엔지니어로. 타이틀은 낮아졌지만, 배울 사람이 있고, 팀이 있고, 시스템이 있는 환경. 2차 사고가 더 나은 결정을 이끌었다고 생각한다.
2차 사고를 훈련하는 방법
2차 사고는 천재만 하는 게 아니다. 습관이다. 몇 가지 방법을 실제로 쓰고 있다.
5 Whys가 아니라 5 And-then-whats. 근본 원인을 찾는 5 Whys는 유명하다. 근데 미래를 예측하는 "5 And then whats"도 강력하다. 기술 결정을 내리기 전에 "그래서 그 다음은?"을 5번 연속으로 물어본다. 보통 3번째쯤에 예상 못한 결과가 보이기 시작한다.
결정 일지. 중요한 기술 결정을 내릴 때, 그 결정의 예상 결과를 적어둔다. 6개월 뒤에 다시 읽어본다. 예측이 맞았는지, 뭘 놓쳤는지 확인한다. 이 피드백 루프가 2차 사고의 정확도를 올려준다.
반대 시나리오. 어떤 기술을 도입하기로 결정했으면, 의도적으로 "이 결정이 최악의 결과를 낳는 시나리오"를 상상한다. 최악의 시나리오가 감당 가능한지 확인하는 거다. 감당 불가능하면, 아무리 최선의 시나리오가 좋아도 재고한다.
대부분의 개발자는 1차 사고에 머문다
이 말이 비판이 아니라 관찰이다. 대부분의 업무 환경이 1차 사고를 장려한다. 스프린트에 일감이 쌓여 있고, PM이 일정을 물어보고, 데일리 스탠드업에서 "오늘 뭐 했고 내일 뭐 할 건지" 보고해야 하는 구조에서, "이 버그의 근본 원인에 대해 3일 동안 생각해봤다"는 말을 하기가 쉽지 않다.
근데 Farnam Street에서 말하는 것처럼, 2차 사고는 경쟁 우위다. 대부분의 사람이 1차에서 멈추니까, 2차까지 가는 사람은 자연스럽게 차별화된다. 코드 리뷰에서 미래를 내다보는 코멘트를 다는 사람, 아키텍처 논의에서 3수 앞을 읽는 사람, 커리어에서 1년 뒤가 아니라 5년 뒤를 설계하는 사람.
그 차이가 주니어와 시니어를 가르는 건 아니다. 시니어인데 1차 사고만 하는 사람도 많다. 다만, 정말 임팩트를 내는 개발자는 거의 예외 없이 2차 사고를 한다. 버그를 고치는 게 아니라 버그가 발생하는 구조를 바꾸는 사람들.
금요일 오후 4시에 올라온 버그 리포트. 30분 만에 고칠 수 있다. 근데 거기서 "And then what?"을 한 번만 더 물어보면, 다음 금요일 오후 4시가 평화로워진다.
