코드를 쓰는 사람도 저고, 호출기를 드는 사람도 접니다. 이 두 가지 사실이 저희 스튜디오의 기술 부채를 어떤 프로세스보다 잘 관리합니다. 그리고 같은 이유로, 기술 부채에 대해 쓰인 글 대부분이 저에게는 쓸모가 없습니다.
흔한 글은 인력을 배분할 수 있는 임원과, 일정을 비워 둘 수 있는 로드맵과, 지름길을 택한 사람과 그것을 떠안는 사람이 서로 다른 조직을 전제합니다. 저희는 대전의 작은 스튜디오이고, 한국에서 세 개의 제품을 운영하며, 그 제품은 저희가 직접 관리하는 장비 위에서 돌아갑니다. 여기에 '엔지니어링 역량의 20%' 같은 것은 없습니다. 있는 것은 저녁 시간과, 출시일을 기다리는 고객입니다.
그래서 제가 신경 쓰는 질문은 어떤 코드가 얼마나 나쁜가가 아닙니다. 어느 코드베이스에나 나쁜 코드는 있고, 그 대부분은 아무 해도 끼치지 않습니다. 질문은 이겁니다. 이 지름길은 무엇을 청구하는가, 누구에게 청구하는가, 그리고 언제 청구서가 오는가.
세 종류의 부채, 그리고 그중 하나만 복리로 붙습니다
코드 품질로 부채를 분류하면 거의 아무것도 알 수 없습니다. 파급 범위로 분류하면 화요일 오후에 무엇을 할지가 정해집니다.
| 종류 | 사는 곳 | 누가, 언제 지불하는가 | 안고 갈까? |
|---|---|---|---|
| 국소 부채 | 인터페이스가 버텨 주는 하나의 모듈 안 | 제가, 그 파일을 열 때만 | 예, 때로는 영원히 |
| 기반 부채 | 데이터 모델, 번역 경계, 인증, 결제 | 그 뒤에 만든 모든 것이, 모든 기능마다 | 아니요, 아직 쌀 때 고칩니다 |
| 운영 부채 | 배포, 마이그레이션, 백그라운드 작업, 로그 | 서비스가 이상해졌을 때 깨어 있던 사람이 | 아니요, 이건 이자까지 붙습니다 |
국소 부채는 안전하고, 대부분의 부채는 국소적입니다
Keasy 뒤의 운영 화면은 저희가 쓰려고 만든 것입니다. 중복된 마크업이 있고, 프로퍼티를 열한 개 받는 컴포넌트가 있고, 남에게 보여 주기 부끄러운 함수도 최소 하나 있습니다. 그리고 그것들은 지금까지 저에게 아무 비용도 청구하지 않았습니다. 국소 부채의 비용은 그 파일을 얼마나 자주 여는가로 묶여 있고, 저는 그 파일을 1년에 두 번 엽니다.
판단 기준은 짧습니다. 이 코드를 다시 건드리지 않는다면 비용이 얼마인가? 시드 스크립트, 내부 도구, 일회성 내보내기, 관리자 화면이라면 답은 보통 0입니다. 그런 것을 정리하는 일은 취미지 업무가 아닙니다. 못생긴 걸 알면서 일부러 내보낸 코드가 있고, 그 이자를 한 푼도 낸 적이 없으며, 내일 다시 같은 선택을 할 겁니다.
복사해 붙여넣기도 사람들이 인정하는 것보다 훨씬 자주 이 범주에 들어갑니다. 비슷한 로직의 사본 세 개는, 그 셋이 같은 것일 때 문제입니다. 셋이 실제로 서로 다른 방향으로 갈라지는 중이라면, 그리고 다국어 제품에서는 대개 그렇습니다, 그걸 하나로 묶으려고 쓴 추상화가 중복보다 더 나쁜 부채가 됩니다.
기반 부채는 그 뒤의 모든 것이 형태를 물려받기 때문에 복리로 붙습니다
WS 폰샵 상담 앱은 7개 언어로 돌아갑니다. 컴포넌트 하나에 박힌 문자열은 5분이면 고칩니다. 하지만 번역 경계를 만들기 전에 문자열을 화면에 박아 넣기로 한 결정은 45분짜리 수정 여러 개가 아닙니다. 잘못된 형태 하나가 마흔 번 반복된 것이고, 그 뒤에 만드는 모든 화면이 그 형태를 공짜로 물려받습니다. 그게 복리의 정의입니다. 부채는 이미 있는 코드가 아니라, 아직 쓰이지 않은 코드입니다.
한 층 아래, 데이터 행에서도 같은 일이 벌어집니다. 가이드의 카테고리를 자유 입력 텍스트로 저장하면 가이드가 32개일 때는 완벽하게 작동합니다. 300개가 되면 같은 카테고리의 철자가 네 가지가 되고, 이름 하나를 바꾸려면 모든 행을 건드려야 하고, 필터가 조용히 콘텐츠를 누락시킵니다. 실제 카테고리에 대한 참조로 저장했다면 이름 변경은 한 번의 업데이트입니다. 두 결정은 내리는 날에는 똑같아 보이고, 그 뒤로 영원히 다른 값을 청구합니다.
제가 쓰는 규칙은 이겁니다. 데이터는 코드보다 오래 삽니다. 화면은 다시 만들게 됩니다. 저희 제품이 출시 때 가지고 있던 것의 절반은 실제 사용자가 만진 뒤에 다시 그려졌습니다. 행은 다시 만들어지지 않습니다. 사용자가 입력하고, 올리고, 결제한 것은 모든 리디자인과 모든 방향 전환에서 살아남습니다. 그리고 그때 고른 형태도 함께 살아남습니다. 그러니 지름길은 버릴 예정인 층에서 택하세요.
여기서 엄격해야 할 현실적인 이유가 있습니다. 제 일정에 올라오는 가장 비싼 항목은 운영 중인 제품의 컬럼 하나를 고치는 마이그레이션입니다. 백필, 배포 창, 실제 데이터로 검증해야 하는 롤백 경로, 그리고 그동안 앱이 왜 불안정한지 고객에게 설명하는 대화까지 따라옵니다. 잘못된 컬럼은 바꿔야 하는 날이 오기 전까지 아무 비용도 들지 않다가, 그날 모든 비용이 한꺼번에 도착합니다.
운영 부채는 하필 가장 나쁜 시간에 청구됩니다
저희 운영 환경은 직접 관리하는 인프라 위의 Postgres, Redis, Docker입니다. 그 말은 코드가 어떻게 읽히는지와 아무 상관 없는 부채가 한 무더기 있다는 뜻입니다. 제 셸 히스토리에만 존재하는 배포 단계, 되돌릴 방법이 없는 마이그레이션, 재시도 대신 조용히 실패하는 백그라운드 작업, 문제가 된 행을 찾는 데 필요한 단 하나의 id를 빼고 메시지만 남기는 오류 분기 같은 것들입니다.
이 부채의 이율이 셋 중 가장 나쁩니다. 항상 갚을 여력이 가장 없는 순간에 청구되기 때문입니다. 새벽 두 시의 당신은 더 나쁜 엔지니어입니다. 그 시간에 출혈을 막으려고 쓴 코드가 내일의 국소 부채가 되고, 당신은 그걸 쓴 기억조차 못 합니다.
그래서 저는 이 범주에서는 마감이 무엇이든 절대 거래하지 않습니다. 모든 배포는 명령 하나이고 제 히스토리가 아니라 저장소에 있습니다. 모든 마이그레이션은 되돌릴 수 있거나, 아니면 병합 전에 되돌릴 수 없다고 문서로 선언합니다. 모든 백그라운드 작업은 재시도하거나 시끄럽게 실패합니다. 모든 오류 경로는 해당 레코드를 찾는 데 필요한 식별자를 남깁니다. 이건 품질 기준이 아닙니다. 수면 보험입니다.
지름길을 택한 사람이 호출을 받는 사람입니다
큰 조직에서는 이 유인 구조가 설계상 망가져 있습니다. 출시는 보이고 유지보수는 보이지 않으며, 엔지니어는 부채가 성숙하는 속도보다 빨리 팀을 옮깁니다. 그래서 지름길을 쓴 사람은 청구가 시작될 즈음이면 대개 다른 곳에 있습니다. 제가 읽은 기술 부채 관리 프로세스는 전부 이 하나의 문제를 우회하려는 장치였습니다.
저희에게는 그 문제가 없습니다. 3월에 출시를 맞추려고 통신사 규칙을 가격 화면에 박아 넣으면, 11월에 지원금 표가 바뀔 때 그 파일을 여는 사람도 접니다. 이 정렬이 제가 아는 가장 강력한 부채 통제 장치이고, 그건 프로세스가 아니라 그냥 회사의 크기입니다.
여기에는 실패 모드가 정확히 하나 있고, 그건 심각합니다. 제가 그 부채를 볼 수 없게 됩니다. 우회 방법을 전부 알고 있기 때문입니다. 머릿속에 지도가 이미 있으면 어떤 코드베이스도 어렵지 않습니다. 부채는 두 번째 사람이 그 파일을 여는 순간까지 보이지 않고, 그때쯤이면 이미 1년째 복리로 불어 있습니다.
그래서 온보딩이 진짜 감사입니다. 새로 온 사람이 첫 주에 막히는 지점이 어떤 린터가 알려 주는 것보다 정확한 부채 목록입니다. 그리고 유통기한이 짧습니다. 한 달이면 그 사람도 우회 방법을 익히기 때문입니다. 그러니 그 주에 적어 두거나, 영영 적히지 않거나 둘 중 하나입니다.
두 번째 시험은 운영 쪽이고, 저는 끝났다고 말하기 전에 모든 프로젝트에 이걸 적용합니다. 다른 사람이 일요일에 저에게 전화하지 않고 이걸 배포할 수 있는가? 답이 아니오라면, 코드 품질이 어떻게 보이든 그 간극이 부채입니다.
아플 때가 아니라 지름길을 택할 때 메모를 남기세요
TODO 주석은 소원입니다. 주인도 없고 결과도 없고, 1년이 지나면 TODO로 가득한 코드베이스는 아무도 읽지 않는 배경 무늬가 됩니다.
대신 통하는 것은 트리거가 붙은 메모입니다. 날짜가 아닙니다. 주석에 2분기라고 적혀 있다는 이유로 뭔가를 한 사람은 없습니다. 트리거는 이 지름길을 틀린 것으로 만드는 조건이고, 아직 그것이 또렷하게 보이는 순간에 적어 둡니다.
# DEBT(2024-11-04): 보증금 금액을 여기서 한 번, 포스터 렌더러에서 또 한 번
# 포매팅한다. 규칙 하나에 사본이 둘.
# TRIGGER: 보증금 규칙이 바뀌거나, 통화 형식이 다른 언어가 추가될 때.
# COST IF UNPAID: 견적 레이아웃을 바꿀 때마다 두 번 고쳐야 하고,
# 두 결과물이 서로 어긋난다.
이건 백로그가 아니고, 주기적으로 검토하지도 않습니다. 값은 다른 데 있습니다. 고객이 견적 표시 방식을 바꾸는 데 얼마가 드느냐고 물었을 때, 그 영역에서 DEBT를 grep하면 10초 만에 제가 남긴 경고를 찾습니다. 숫자를 말하기 전에요. 제가 잘못 산정한 견적 대부분은 판단력의 문제가 아니었습니다. 필요한 사실이 6개월 전 제 머릿속에 있었고 질문받는 순간 눈앞에 없었던 경우였습니다.
원하시는 것이 막혀 있다고 고객에게 말하기
이게 이 주제에서 가장 어려운 부분이고, 엔지니어링과는 아무 상관이 없습니다. 지난 분기에 고객은 더 싼 쪽을 골랐습니다. 이번 분기에 원하는 기능은 그 선택 때문에 비싸진 기능입니다. 이 대화를 버티게 해 주는 규칙이 네 개 있습니다.
청구서에 '리팩터링'이라는 단어를 절대 쓰지 마세요. 고객은 리팩터링을 사지 않고, 그게 맞습니다. 고객이 사는 것은 기능입니다. 기능 견적 안에 앞선 지름길의 비용이 평이한 말로 항목화되어 들어갑니다. 두 번째 통신사를 추가하는 데 5일이고, 그중 2일이 통신사 작업이며 3일은 3월에 출시를 맞추려고 건너뛴, 요금제 규칙을 화면 밖으로 빼는 작업입니다.
이 대화는 그때 그 결정을 적어 뒀을 때만 버틸 수 있습니다. 코드의 언어가 아니라 고객의 언어로요. 출시일 전달 메일에 넣은 한 문장이 저장소의 어떤 주석보다 값이 큽니다. "이 일정을 맞추기 위해 요금제 규칙을 한 통신사 기준으로 고정합니다. 나중에 두 번째 통신사를 추가하려면 대략 일주일이 듭니다." 이 한 문장이 11월을 비난에서 상기로 바꿉니다.
'그때 돈을 안 쓰신 거잖아요'라고 절대 말하지 마세요. 정확히 그게 사실이더라도요. 그건 함께 내린 결정이었습니다. 대안이 출시를 놓치는 것이었고 출시를 놓치는 쪽이 고객에게 더 나빴기 때문에, 지름길을 권한 것은 저였습니다. 제 몫을 인정하는 것은 예의가 아니라 정확함입니다.
죄가 아니라 이자를 가격으로 매기세요. 숫자가 붙은 두 가지 선택지를 드립니다. 이번 기능 안에서 갚으면 다음 세 건은 정상 가격에 나갑니다. 다시 덧대면 다음 건이 이번 것보다 비쌉니다. 이렇게 틀을 잡으면 고객은 좋은 결정을 내립니다. 선택지가 '예'와 '코드 품질에 대한 훈계' 둘뿐일 때 나쁜 결정을 내립니다.
마감이 살 수 있는 것
저희가 내놓은 것은 전부 실제 날짜가 박힌 MVP로 나갔습니다. MVP의 범위는 곧 떠안기로 합의한 부채의 범위입니다. 그래서 분류는 출시 뒤가 아니라 출시 전에 합니다.
SGP 지원 도구가 깔끔한 예입니다. 작성된 이력서를 인쇄 레이아웃과 내려받을 수 있는 PDF, 두 가지로 만들어 냅니다. 인쇄용 스타일시트가 안에서 엉망이라면 아무도 손해 보지 않고 저는 몇 년이든 안고 갈 수 있습니다. 하지만 항목 목록이 서식과 PDF 템플릿에 각각 한 번씩 두 벌 존재한다면, 대학이 공식 서식을 개정할 때마다 두 곳을 고쳐야 하고, 언젠가 한쪽이 틀린 채로 남아 학생이 접수 창구에서 그것을 발견하게 됩니다. 그래서 항목 정의는 마감 압박이 있어도 출시 전에 하나여야 합니다. 스타일시트는 그런 보호를 받지 못합니다.
마감에 절대 내주지 않는 것이 두 가지 있습니다. 누가 무엇을 볼 수 있는지 정하는 것과 누가 얼마를 내는지 정하는 것입니다. 이 둘에서의 지름길은 부채가 아닙니다. 시한이 붙은 결함이고, 그것을 부채라고 부르는 것이 그것이 출시되는 경로입니다.
리팩터링 스프린트가 없는 스튜디오의 상환 방식
여기에는 따로 떼어 둔 비율이 없고, 있는 척하면 매달 조용히 못 지키는 숫자 하나가 생길 뿐입니다. 실제로 일하는 것은 세 가지입니다.
부채는 그것에 걸려 넘어진 기능 안에서 갚습니다. 고객이 지름길을 지나가는 무언가를 요청하면, 상환이 그 견적의 일부가 되고 그 일정 안에서 처리됩니다. 정직하게 말해서 그것이 요청하신 것의 비용이기 때문입니다. 지저분하다는 이유로 정리되는 것은 없습니다. 이제 돈 받는 일을 가로막고 있다는 이유로 정리됩니다. 밤 열한 시에도 지킬 수 있는 훨씬 쉬운 기준입니다.
삭제가 가장 싼 상환이고, 다국어 제품에서는 더 쌉니다. 아무도 쓰지 않는 기능은 리뷰 시간과, 지원하는 모든 언어의 번역 문자열과, 내비게이션의 한 자리와, 그 뒤의 모든 스키마 결정에 붙는 제약을 소비합니다. 7개 언어 제품에서 화면 하나를 지우면 화면 일곱 개 분량의 유지보수가 사라집니다. 가장 좋은 리팩터링은 종종 삭제이고, 앞으로 모든 기능의 견적을 작게 만드는 유일한 리팩터링입니다.
세 번째는 멋없지만 실재합니다. 작은 스튜디오에는 빈 구간이 있습니다. 고객 피드백을 기다리고, 스토어 심사를 기다리고, 로고에 대한 결정을 기다리는 시간입니다. 그때 트리거가 이미 발동한 국소 부채를 정리합니다. 계획된 것이 아니고, 계획인 척하는 것도 그만뒀으며, 제가 적어 본 어떤 계획보다 많은 부채를 실제로 갚았습니다.
안고 가기로 한 부채
운영 중인 세 제품 모두에, 무엇이 잘못됐는지 정확히 알면서도 고치지 않기로 한 자리가 있습니다. 마켓플레이스의 검색은 언젠가 더 나은 것이 되어야 할 단순한 쿼리이고, 트리거는 제 취향이 아니라 상품 수입니다. 어떤 내부 도구는 운영자 한 명을 위해 쓰였고, 트리거는 두 번째 운영자입니다. 견적 포매팅은 렌더러 여러 곳에 존재하고, 트리거는 그 아래 규칙의 다음 변경입니다.
이것들은 다 괜찮습니다. 각각을 급한 것으로 만드는 조건을 제가 말할 수 있기 때문입니다. 정말 걱정되는 부채는 다른 종류입니다. 너무 오래전에 택해서 더 이상 지름길로 보이지도 않고, 그 뒤의 모든 것이 그 위에 쌓였으며, 제가 아예 보지 못하게 된 것. 메모는 그것 때문에 있고, 프로젝트에 새로 온 사람의 첫 주를 적어 둘 가치가 있는 이유도 그것입니다.
