계속 발에 총을 쏘고 있다면, 총을 고치세요
- 팀에서 시스템에 대해 자주 실수하는 부분이 있지만, 그 실수를 줄일 방법에 대해 생각하지 않는 경우가 많음
- 이런 경우 시스템을 개선하여 실수를 줄이는 것이 중요함
- 경험:
- iOS 개발 시 CoreData를 사용할 때, UI 업데이트는 메인 스레드에서만 가능함
- 구독 콜백이 메인 스레드와 백그라운드 스레드에서 모두 발생하여 문제가 종종 생겼음
- 기존 팀원들은 이를 인지하고 잘 처리하지만, 신입 팀원의 리뷰에서 종종 언급됨
- 가끔 실수가 발생하면 충돌 보고서를 보고 DispatchQueue.main.async를 추가해 왔음
- 이를 해결하기 위해 구독 레이어를 업데이트하여 구독자를 메인 스레드에서 호출하도록 변경함. 딱 10분 걸림.
- 전체 충돌 클래스와 약간의 정신적 부담을 제거함
- 누구나 몇 분 동안 생각하면 명백한 문제였을 것임
- 그러나 이러한 문제를 해결할 자연스러운 시기가 없기 때문에 이상하게 오래 지속됨
- 즉 이러한 문제는 팀에 오래 있으면 배경으로 사라지기 쉬움
- 마음가짐의 변화가 필요함
- 이런 문제가 있을 때 자신과 팀의 삶을 더 쉽게 만들 수 있다는 것을 가끔 상기시켜야 함
품질과 속도 사이의 균형 맞추기
- 구현 속도와 정확성에 대한 자신감 사이에는 항상 트레이드오프가 존재함
- 현재 상황에서 버그를 출시하는 것이 얼마나 괜찮은지 자문해 보아야 함
- 이에 대한 답변이 작업 방식에 영향을 미치지 않는다면, 지나치게 경직된 것임
- 첫 직장에서 데이터 처리 프로젝트를 할 때는 데이터를 소급하여 재처리할 수 있는 좋은 시스템이 갖추어져 있었음
- 버그 출시의 영향은 매우 미미했고, 이런 환경에서는 가드레일에 어느 정도 의존하고, 더 빠르게 움직일 수 있음
- 100% 테스트 커버리지나 광범위한 QA 프로세스는 개발 속도를 늦출 뿐임
- 두 번째 직장에서는 수천만 명이 사용하는 제품으로 고가치 금융 데이터와 개인 식별 정보를 다루어 버그가 치명적이었음
- 작은 버그라도 사후 분석이 필요했음
- 기능을 매우 느린 속도로 출시했지만, 그 해에 버그를 0개 냈다고 생각
- 대부분의 경우, 두 번째 회사와 같은 상황에 있지는 않음
- 버그가 치명적이지 않은 상황(예: 99%의 웹 앱)에서는 빠르게 출시하고 버그를 빠르게 수정하는 것이 더 나음
- 처음부터 완벽한 기능을 출시하는 데 시간을 들이는 것보다 더 발전할 수 있음
톱을 갈고 있는 시간은 거의 항상 가치가 있음
- 도구를 잘 다루는 것이 중요함
- 코드를 빠르게 작성하고, 주요 단축키를 알고, 운영체제와 셸에 능숙해야 함
- 이름 바꾸기, 타입 정의로 이동, 참조 찾기 등을 많이 하게 될 것
- 에디터의 주요 단축키를 모두 알고, 자신 있고 빠른 타이핑을 할 수 있어야 함
- 브라우저 개발 도구를 효과적으로 사용하는 것도 중요함
- 도구를 잘 선택하고 능숙하게 사용하는 것은 큰 장점임
- 새로운 엔지니어에게서 보이는 가장 큰 그린 플래그 중 하나는 도구 선택과 능숙한 사용에 대한 관심임
어려움을 쉽게 설명할 수 없다면, 그것은 아마도 우발적 복잡성일 것이고, 이 문제는 해결할 가치가 있음
- 내가 가장 좋아하는 관리자는 구현이 어렵다고 주장할 때마다 계속해서 압박하는 버릇이 있었음
- 종종 그의 대답은 대개 "X를 Y할 때 보내는 것에 불과한 것 아닌가요?" 또는 "몇 달 전에 했던 Z와 같은 것 아닌가요?"와 같은 식이었음
- 매우 높은 수준의 반대 의견이었고, 내가 설명하려고 했던 실제 함수와 클래스 수준이 아니었음
- 관리자가 이렇게 단순화하는 것은 그저 성가신 일이라는 것이 통상적인 견해임
- 하지만 놀랍게도 높은 비율로, 관리자가 계속 물어볼 때 내가 설명하던 복잡성의 대부분이 우발적 복잡성이라는 것을 깨달았음
- 그리고 실제로 그것을 먼저 해결함으로써 문제를 관리자가 말하는 것처럼 사소하게 만들 수 있었음
- 이런 식의 접근은 향후 변경을 더 쉽게 만드는 경향이 있음
버그를 한 층 더 깊이 해결하려고 노력하기
- 버그를 표면적으로 해결하는 대신, 근본적인 원인을 찾아 해결하는 것이 중요함
- 대시보드에 현재 로그인한 사용자의 상태에서 가져온
User
객체를 다루는 React 컴포넌트가 있을때
- Sentry에서 렌더링 중에
user
가 null
이었다는 버그 리포트가 나옴
-
if (!user) return null
을 빠르게 추가하거나
- 또는 조금 더 조사해 보면, 로그아웃 함수가 두 개의 별개 상태 업데이트를 수행한다는 것을 알 수 있음
- 첫 번째는 사용자를
null
로 설정하고, 두 번째는 홈페이지로 리디렉션
- 두 가지의 순서를 바꾸면, 이제 어떤 컴포넌트도 이 버그를 다시는 겪지 않을 것
- 대시보드 내에서는 사용자 객체가 절대
null
이 아니기 때문
- 첫 번째 유형의 버그 수정을 계속하면 엉망이 되지만,
두 번째 유형의 버그 수정을 계속하면, 깨끗한 시스템과 불변성에 대한 깊은 이해를 갖게 될 것
버그 조사를 위해 히스토리를 파고드는 것의 가치를 과소평가하지 말 것
- 나는
println
과 디버거와 같은 일반적인 도구를 사용하여 이상한 문제를 디버깅하는 데 꽤 능숙했음
- 그래서 버그의 히스토리를 파악하기 위해 git을 많이 보지 않았는데, 하지만 어떤 버그의 경우에는 이것이 매우 중요함
- 최근 서버에서 메모리가 지속적으로 누수되는 것 같았고, OOM에 의해 종료되고 다시 시작되는 문제가 있었음
- 어떤 가능성 있는 원인도 배제되었고, 로컬에서 재현할 수 없었음
- 눈을 가리고 다트를 던지는 것 같은 느낌이었음
- 커밋 히스토리를 살펴보니 Play Store 결제 지원을 추가한 후에 발생하기 시작했음
- 단지 몇 개의 HTTP 요청일 뿐이라 백만 년이 지나도 찾아보지 않았을 곳임
- 첫 번째 액세스 토큰이 만료된 후 액세스 토큰을 가져오는 무한 루프에 빠진 것으로 밝혀짐
- 각 요청은 메모리에 1kB 정도만 추가했을 수 있지만, 여러 스레드에서 10ms마다 재시도하면 빠르게 누적됨
- 보통 이런 일은 스택 오버플로를 야기했겠지만, Rust에서 비동기 재귀를 사용하고 있었기에 스택 오버플로가 발생하지 않았음
- 이는 절대 떠오르지 않았을 것이지만, 문제를 일으킨 것이 분명한 특정 코드를 살펴보게 되면서 갑자기 이론이 떠오름
- 이런 접근을 언제 해야 할지에 대한 규칙은 없음
- 직관에 기반한 것으로, 버그 보고서에 대한 다른 종류의 "어라?"가 이런 종류의 조사를 촉발함
- 시간이 지나면서 직관을 발전시킬 수 있지만, 때로는 매우 귀중하다는 것을 아는 것으로 충분함
- 문제가 적합한 경우
git bisect
를 시도해 볼 것
- 잘못된 것으로 아는 커밋 하나와 좋은 것으로 아는 커밋 하나가 있는 경우
나쁜 코드는 피드백을 주지만, 완벽한 코드는 그렇지 않음. 나쁜 코드를 작성하는 쪽으로 오류를 범할 것
- 끔찍한 코드를 작성하는 것은 정말 쉬움
- 하지만 모든 모범 사례를 절대적으로 따르는 코드를 작성하는 것도 정말 쉬움
- 단위, 통합, fuzz, 돌연변이 테스트를 모두 거쳐야 하는데, 스타트업은 그 전에 돈이 바닥날 것
- 프로그래밍의 많은 부분은 균형을 찾는 것
- 빨리 코드를 작성하는 쪽으로 오류를 범하면...
- 가끔은 나쁜 기술 부채로 인해 곤란을 겪게 될 것임
- "데이터 처리에 대해 훌륭한 테스트를 추가해야 한다"는 것을 배울 것임
- "테이블 설계를 정말 잘 생각해봐야 한다"는 것도 배울 것임
- 다운타임 없이 변경하는 것은 매우 어려울 수 있기 때문
- 완벽한 코드를 작성하는 쪽으로 오류를 범하면...
- 아무런 피드백을 받지 못함
- 모든 것이 보편적으로 오래 걸림
- 어디에 시간을 제대로 쓰고 있는지, 어디에서 시간을 낭비하고 있는지 모름
- 피드백 메커니즘은 학습에 필수적이지만, 그런 것을 얻지 못하고 있음
- "나쁜" 코드의 의미 명확히 하기
- "해시맵 생성 구문을 기억할 수 없어서 내부 루프를 두 번 사용했다"는 의미가 아님
- 다음과 같은 의미임:
- 특정 상태를 표현할 수 없게 하기 위해 데이터 수집을 재작성하는 대신, 몇 가지 핵심 검사점에서 불변성에 대한 어설션을 몇 개 추가함
- 서버 모델이 작성할 DTO와 정확히 동일하므로 그냥 직렬화함. 모든 상용구를 작성하는 대신 필요에 따라 나중에 DTO를 작성할 수 있음
- 이 컴포넌트들은 사소하고 버그가 있어도 큰 문제가 없으므로 테스트 작성을 건너뜀
디버깅을 쉽게 만들기
- 수년에 걸쳐 소프트웨어를 디버깅하기 쉽게 만드는 많은 작은 트릭들을 습득함
- 디버깅을 쉽게 하기 위한 노력을 하지 않으면, 소프트웨어가 점점 더 복잡해짐에 따라 각 이슈를 디버깅하는 데 엄청난 시간을 소비하게 될 것
- 변경을 하는 것이 두려워질 것임. 새로운 버그 몇 개를 파악하는 데 일주일이 걸릴 수도 있기 때문
- 디버깅 시간 중 설정, 재현, 사후 정리에 소요되는 시간을 추적하는 데 주의를 기울일 것
- 50% 이상이라면, 이번에는 약간 더 오래 걸리더라도 더 쉽게 만드는 방법을 찾아야 함
- 다른 조건이 동일하다면, 버그 수정은 시간이 지남에 따라 더 쉬워져야 함
팀에서 일할 때는 항상 질문하기
- "모든 것을 스스로 알아내려고 노력하는 것"부터 "사소한 질문으로 동료를 귀찮게 하는 것"까지 스펙트럼이 있음
- 대부분의 경력 초반자들은 전자 쪽에 너무 치우쳐 있다고 생각함
- 항상 코드베이스에 더 오래 있었거나, 기술 X를 훨씬 더 잘 알거나, 제품을 더 잘 알거나, 그냥 더 경험 많은 엔지니어가 주변에 있음
- 어딘가에서 처음 6개월 동안 일하다 보면 한 시간 넘게 무언가를 알아내는 데 시간을 허비하거나, 몇 분 만에 답을 얻을 수 있는 경우가 많음
- 질문을 할 것. 질문을 하는 것이 누군가에게 성가신 유일한 경우는 몇 분 안에 스스로 답을 찾을 수 있었다는 것이 분명할 때뿐임
배포 주기가 매우 중요함. 빠르고 자주 배포할 수 있는 방법을 신중히 고민해야 함
- 스타트업은 Runway가 제한적이고, 프로젝트에는 마감일이 있음
- 직장을 그만두고 독립할 때, 저축한 돈은 몇 달 동안만 지속될 것임
- 이상적으로는, 프로젝트 속도가 시간이 지남에 따라 복리로 증가해서, 상상할 수 있는 것보다 빠르게 기능을 배포하게 됨
- 빠르게 배포하려면 많은 것들이 필요함
- 버그에 취약하지 않은 시스템
- 팀 간 빠른 회전 시간(Turnaround)
- 새로운 기능의 10%를 잘라내는 의지 (엔지니어링 시간의 50%를 차지할 부분)와 그런 부분을 아는 통찰력
- 새로운 화면/기능/엔드포인트에 조합할 수 있는 일관되고 재사용 가능한 패턴
- 빠르고 쉬운 배포
- 속도를 늦추지 않는 프로세스 (불안정한 테스트, 느린 CI, 까다로운 린터, 느린 PR 리뷰, 종교처럼 여기는 JIRA 등)
- 그 외 수백만 가지
- 느리게 배포하는 것은 프로덕션을 중단하는 것만큼 사후 분석을 해야 함
- 우리 업계는 그렇게 운영되지 않지만, 그렇다고 해서 개인적으로 빠른 배포라는 북극성을 따를 수 없는 것은 아님