🎧 ▶ 5분 브리핑으로 듣기
▶ 오디오북 재생 (Google Drive)
NotebookLM 오디오 개요 (AI 생성)
📕 전자책 전문 읽기 리팩토링의 규율 · 31쪽
브라우저가 PDF를 바로 표시하지 못합니다. 새 탭에서 열기
이 글은 요약해 다시 쓴 것이고, PDF가 전문입니다.

자기가 지은 코드베이스를 혼자 수년째 돌리고 있다면, 이 글은 당신을 위한 것이다. 얻을 것은 한 줄의 잣대와 절차 하나입니다. 어디부터 고칠지를 어떻게 정하고 어떻게 고쳐야 동작을 깨뜨리지 않는지.

먼저 결론부터 말하겠습니다. 먼저 고칠 코드는 가장 자주 만지는 것이다. 대부분의 개발자는 코드베이스를 보기 싫음의 순서대로 정렬해서 맨 앞에서부터 손을 댑니다. 그 순서는 거의 항상 낭비입니다. 추함의 비용은 코드를 바꿀 때만 내기 때문이다.

아래 논증은 그 한 줄에서 출발합니다. 왜 변경 빈도가 추함보다 잣대가 되는지, 자주 바뀌는 코드는 어디에서 드러나는지, 한 줄도 움직이기 전에 무엇부터 설치해야 하는지, 움직임의 단위는 왜 작아야 하는지, 그리고 의지 없이도 이걸 계속 돌리는 방법은 무엇인지.

추한 코드가 먼저가 아니다. 자주 만지는 코드가 먼저다 개념을 형상화한 이미지 글의 핵심 개념을 형상화했습니다.

추함은 공짜다. 마찰은 매달 내는 돈이다

실험 하나부터 하자. 최근에 고쳤던 버그를 떠올려 보라. 수정 자체에 쓴 시간과, 고칠 곳을 찾는 데 쓴 시간, 어느 쪽이 더 길었는가. 유지보수자에게는 찾는 시간이 거의 항상 더 깁니다. 그 비율이 나쁜 형태의 진짜 비용입니다. 나쁜 형태는 작업의 위치를 찾는 일을 늦게 합니다.

시간의 각도에서 비용을 보자. 추지만 거의 바뀌지 않는 코드는, 그 사이에는 공짜입니다. 추함의 비용을 내는 순간은 코드를 바꿀 때뿐입니다. 안 바꾸면 비용도 내지 않습니다. 다시 읽는 부담, 이름의 혼란, 불확실함에 대한 불안이 전부, 코드가 가만히 있는 동안은 비용이 되지 않는다. 반면 한 달에 다섯 번은 만지는 코드는 마찰을 매달 내고 있는 겁니다. 매번 컨텍스트를 다시 읽고 전제를 다시 확인하고 검증을 다시 돌린다.

숫자로 셈해 보자. 어떤 함수가 형태 때문에 바꿀 때마다 15분씩 더 읽어야 한다고 하자. 그 함수를 한 달에 4번 고친다면, 매달 1시간을 내고 있는 겁니다. 30분을 들여 그 함수의 형태를 쉽게 만들면, 다음 달부터 매월 30분이 남습니다. 회수 기간은 1개월입니다. 2년을 더 고친다면, 순수 이익은 대략 72시간입니다. 같은 추함이 3개월에 한 번만 바뀌는 함수에 있다면, 그 30분 투자의 회수에는 8개월이 걸립니다.

같은 추함, 다른 위치, 전혀 다른 가치. 위 두 사례는 자주 바뀌는 코드와 드물게 바뀌는 코드의 차이입니다. 그래서 고칠 순서를 정할 때 첫 질문은 언제나, 내가 언제 또 만지게 되느냐다. 운영에서 장애가 잦은 서비스를 오래되고 추하지만 안정된 서비스보다 먼저 보는 것과 같은 논리죠.

핵심 개념 요약 인포그래픽 1 NotebookLM이 소스를 종합해 생성한 인포그래픽입니다.

고칠 곳의 목록은 git 히스토리가 낸다

그렇다면 변경 빈도는 어떻게 재는가. 기억이나 느낌으로 하면 안 합니다. git 히스토리가 이미 기록해 두고 있습니다. 지난 6개월 커밋을 가져와 파일별로 변경 횟수를 세면, 가장 많이 변한 파일부터 나열된 목록이 나오게 됩니다. 그 목록 맨 위가 리팩토링 투자가 가야 할 곳입니다.

git log --since="6 months ago" --name-only --oneline | sort | uniq -c | sort -rn | head -20

이 명령은 지난 6개월 동안 가장 자주 변한 파일 목록을 내는 겁니다. 상위 10개 중 절반을 같은 파일이 차지한다면, 그것은 증거죠.

목록은 장소만 알려 줍니다. 그 파일을 열면, 어떤 형태의 문제를 보고 있는지 알아야 합니다. 유지보수에서 마찰을 가장 자주 만드는 형태는 다섯 가지입니다. 검증을 계산과 저장을 통지와 같이 한 함수가 하는 경우, 같은 로직이 두 군데 이상에 쓰인 경우, 논리적 변화 하나가 파일 여러 개를 건드리는 경우, if 안에 if가 파인 경우, 그리고 계산 속에 뜻 없이 떠 있는 숫자와 문자열. 이 가운데 중복과 파도처럼 퍼지는 수정이 다음 변경을 가장 직접적으로 늦추게 됩니다.

파일을 열기 전에 냄새를 볼 수 있는 신호도 있죠. 파일 이름이 util이든 helper든 manager든, 대개 로직의 집이 없는 쓰레기장입니다. 또 고침, 임시 처리, 여기만 건드리지 마 같은 커밋 메시지는 코드가 마찰을 냈다는 직접적 증거입니다. 이 세 신호를 먼저 훑고 파일을 열면, 판단이 훨씬 빨라집니다.

the-refactoring-discipline 슬라이드 1

고쳐도 좋을 만한 추함의 역순서, 세 가지

반대 목록도 같은 무게로 적어 두자. 고치고 싶어도 고치지 말아야 하는 추한 코드는 세 종류입니다. 첫째는 데드 코드입니다. 이미 대체된 기능, 더 이상 읽히지 않는 설정. 만지지 않을 코드는 꾸미지 않습니다. 대개 정답은 삭제입니다. 삭제는 1인 개발자에게 회수가 가장 높은 리팩토링입니다. 다만 참조를 실제로 확인해서 판단해야지, 아무도 안 쓴다는 기억에 의존하면 안 합니다.

둘째는 동작을 검증할 수 없는 코드입니다. 테스트가 없고 입출력이나 데이터베이스, 네트워크, 타이밍에 엉켜 있어서 쓰기에도 어려운 경우. 그곳에서 큰 움직임은 도박입니다. 그런 곳에는 가장 보수적인 움직임, 이름 바꾸기나 상수 추출 정도와, 테스트 영역을 넓히는 일만 들어갑니다.

셋째는 모양에 대한 불만입니다. 구조가 조금만 다르다면 더 나아질 텐데,는 정당한 생각입니다. 하지만 우선순위 목록에서는 맨 아래에 잡힙니다. 코드가 추지만 변경 빈도가 0이고 마찰이 낮다면, 얻을 아름다움은 자신에게만 보이는 것이며, 그 대가는 혼자 짊어지는 회귀 위험입니다.

이 세 종류는 공통점이 있죠. 고치면 기분이 좋고 회수는 없기 때문이다. 변경 빈도라는 잣대의 역할은 바로 그 만족감에서 당신을 지키는 일입니다. 바꾸지 않을 코드를 고치고 싶어지는 순간, 멈추고 회수가 언제 오느냐를 물어 보라. 답이 나오지 않는다면, 그 코드는 이번 주에는 안 되는 겁니다.

the-refactoring-discipline 슬라이드 2

앵커 없이는 한 줄도 움직이지 않는다

잣대와 목록은 정했습니다. 이제 진짜 어려운 질문입니다. 동작을 바꾸지 않고 코드를 실제로 움직이는 방법. 동작은 그대로라는 약속은, 검증 수단이 없으면 공염불입니다. 테스트 없이 코드를 옮기면, 동작이 바뀌었는지를 아는 유일한 방법은 앱을 돌리고 눈으로 보는 것뿐이고, 여러 기능이 동시에 돌아가는 1인 개발자에게 그 방법은 믿기 어렵습니다.

그래서 리팩토링의 첫 단계는 늘 앵커를 설치하는 일입니다. 테스트가 있으면 전체를 돌려 다 초록이 된 것을 커밋해 기준선으로 삼습니다. 테스트가 없으면 특화 테스트를 씁니다. 특화 테스트의 목표는 올바른 동작을 확인하는 게 아니라, 현재 동작을 있는 그대로 기록하는 겁니다. 현재 동작에 버그가 있어도, 테스트는 그 버그를 그대로 기록합니다. 고치지 말고 기록한다. 순서가 바뀌면 리팩토링이 아닙니다.

순서는 단순합니다. 대표적 입력을 고릅니다. 정상 케이스, 경계 값, 에러 케이스. 가격 함수라면 입력 4건에서 6건이면 충분합니다. 현재 코드를 돌려 출력을 적습니다. 출력한 값을 그대로 복사해 적기만 하면 됩니다. 적어 둔 출력을 기대값으로 어설션을 씁니다. 이 테스트가 초록이 되는 순간, 앵커가 설치된 겁니다. 이 시점부터 형태를 바꾸고 출력이 달라지면, 테스트가 멈춰 세웁니다.

구체적 예를 보자. 할인 로직을 가진 가격 함수의 입력으로 네 가지를 고릅니다. 6월의 골드 사용자, 12월의 골드 사용자, 12월의 실버 사용자, 쿠폰을 한 장 가진 실버 사용자. 옛 코드를 돌려 할인 출력을 적어 두면, 현재 동작은 이 네 건으로 고정됩니다. 이제 함수의 형태를 바꿔도, 아무도 이 값을 건드릴 수 없습니다. 코드가 입출력에 엉켜 테스트가 어렵다면, 모듈 전체를 앵커하지 말고 그 안의 순수 리프 함수부터 앵커합니다. 같은 입력이 같은 출력을 내는 함수부터. 앵커는 사다리입니다. 한 칸씩 올라가면 됩니다.

the-refactoring-discipline 슬라이드 3

한 칸, 검증, 커밋

절차는 다섯 단계입니다. 앵커, 한 칸, 검증, 커밋, 반복. 핵심은 한 칸이라는 단어에 있죠. 가장 작은 변경 단위 하나만 합니다. 변수를 이름 바꾸고 함수 하나를 빼내고 줄 한 줄을 옮기는 것. 이 단계에서 잠시 이것도 하고는 금지입니다. 잠시 이것도가 대청소의 입구입니다.

왜 한 칸이 최대 단위인가. 이유는 실패를 설명하는 데 있습니다. 한 칸을 하고 테스트가 빨강이 되면, 원인은 거의 분명히 그 칸입니다. 세 칸을 한꺼번에 하고 빨강이 되면, 셋 중 어디인지를 찾지 못해 디버깅이 시작합니다. 한 칸은, 실패를 설명 가능한 범위 안에 남기는 규칙입니다. 그것이 리팩토링과 디버깅의 경계입니다.

작은 움직임에는 메뉴가 있고 안전한 것부터 위험한 것으로 줄이 선다. 이름 바꾸기는 가장 안전하고 이것은 무엇인지라는 질문을 강제합니다. 상수 추출은 영향 범위를 그 숫자가 쓰이던 곳으로만 제한합니다. 깊은 중첩을 앞선 return으로 평탄화하는 것은 위험이 낮고 효과가 즉각적입니다. 함수 추출은 한 칸 크게, 클래스 사이로 함수를 옮기는 것은 구조를 바꾸므로 작은 움직임 가운데 가장 위험합니다. 흔히 범하는 실수는 둘입니다. 여러 움직임을 한 세션에 몰아넣는 것과, 당연히 맞을 것 같아서 검증을 빼는 것.

실전 경계를 하나 더 두자. 3줄이면 고칠 수 있는 것은 백로그에 적지 말고 그 자리에서 고칩니다. 이름 바꾸기, 상수 추출, 조기 return. 작은 과제를 목록에 적고 나중에 맥락에 다시 들어가는 비용이, 그냥 하는 비용보다 큰 일이 자주 있습니다. 반대의 규율도 있습니다. 3줄이던 수정이 중간에 30줄로 불어난다면, 멈춥니다. 그때 그것은 프로젝트고, 한 칸 절차로 돌아가야 합니다. 대부분의 실패는 이것이 작은 일인지 프로젝트인지 정하지 못하는 지점에서 나옵니다.

the-refactoring-discipline 슬라이드 4

의지가 아니라 리듬

의지 있을 때만 돌아가는 기법은 썩습니다. 주말에 리팩토링을 하자고 하는 계획은 거의 살아남지 못합니다. 주말은 오지 않거나, 오면 버그에게 통째로 먹혀 끝납니다. 1인 개발자에게는 그 시간을 대신 채워줄 사람도 없습니다. 그래서 배분은 기능 작업 안의 몫으로 합니다. 기능을 추정할 때, 5분의 1 정도를 형태 몫으로 더합니다. 3일짜리 기능 추정에는, 지나는 구역에서의 6시간에서 8시간짜리 작은 움직임이 포함된다는 뜻입니다.

그 시간의 쓰임은 제한됩니다. 이미 그 기능 때문에 건드리고 있는 파일 안에서만 고칩니다. 이 제한의 이익은 두 가지입니다. 대청소로 흘러가지 않고 테스트와 컨텍스트가 갓 만든 상태라는 점입니다. 형태 몫을 안 쓰면, 그것도 신호입니다. 지나는 구역이 고치기 쉬웠다는 뜻이며 그 자체를 기록할 가치가 있습니다. 쉬운 구역에는 투자를 넣지 않습니다.

매주 15분, 나와의 리뷰 미팅을 합니다. 금요일 오후가 좋습니다. 액션은 셋입니다. 첫째, 이번 주 커밋을 되돌아보며 어려운 커밋을 찾습니다. 메시지를 쓰는 데 시간이 걸린 것, 파일을 많이 건드린 것, 고친 뒤에 또 고침이 붙은 것. 그것들이 마찰의 자국이고 자국이 있는 곳이 이번 주 핫스팟입니다. 둘째, 리팩토링 로그를 대조합니다. 이번 주에 건 항목과 건지 못한 항목을 구분하고, 건지 못한 항목은 지우지 말고 재결정합니다. 다음 주, 혹은 더 이상 안 함. 셋째, 다음 주 항목을 하나만 고릅니다. 위치, 움직임, 예상 크기, 한 줄을 로그 맨 위에 씁니다.

빚이 갚는 속도보다 빨리 올라오는 주도 있습니다. 기능이 급하고 버그가 꼬리를 물고 형태 몫이 가장 먼저 사라집니다. 1인 개발자에게는 특권이 하나 있습니다. 자신의 속도를 고를 수 있는 것. 빚이 무거운 주에는 의식적으로 이번 주 기능을 작게 만듭니다. nice-to-have를 하나 떨어뜨리거나 다음 주로 미룹니다. 손해처럼 느껴지지만 그것은 상환입니다. 지난주와 같은 작업량을 받는 것은 차입이고, 차입에는 마찰 이자가 붙습니다. 판단 기준은 단순합니다. 15분 리뷰를 하고 나서 불안이 남으면, 당신의 속도는 아직 너무 빠릅니다.

코드 형태는 다음 주에 회수되는 자산

이 규율이 회수되는지 아는 방법도 정해 두자. 허영 지표는 지운 줄 수와 추출한 함수 수를 재는 겁니다. 그 숫자가 올라가는 동안 코드는 나빠질 수 있습니다. 대신 재야 할 것은 셋입니다. 변경 요청을 받은 때부터 자신 있게 커밋할 때까지의 시간, 평균 커밋 크기, 한 기능을 고치다 다른 기능이 망가진 일의 빈도. 월에 한 지표면 충분합니다. 측정의 목적은 보정입니다. 변경 빈도라는 잣대와 작은 움직임과 주간 리듬이, 알맞은 데로 가고 있는지를 보는 일.

기록은 한 줄이면 됩니다. 리팩토링 로그 맨 아래에, 8월 1일 결제 함수 수정, 읽기 40분 쓰기 20분. 9월 1일 같은 함수, 읽기 15분 쓰기 20분. 숫자 자체보다 적어야 한다는 점에서 가치가 있습니다. 적다 보면, 길게 느꼈던 시간이 실제로 짧아졌다는 것을 알게 됩니다.

모아 보겠습니다. 코드 형태는 다음 변경을 쉽게 만드는 만큼 자라나는 자산이고 다음 변경은 생각보다 빨리 옵니다. 오늘 내딛는 작은 걸음은 다음 주의 작업에서 회수됩니다. 바뀌지 않는 추함은 공짜이고, 자주 바뀌는 마찰은 복리로 붙습니다. 그래서 고칠 순서는 변경 빈도가 정하고, 앵커와 한 칸과 검증과 작은 커밋과 15분 리뷰가 그 투자를 도박에서 지킵니다.

이 논증의 전체 버전, 우선순위가 실린 냄새 목록과 중단 기준이 포함된 루프와 레거시 코드 5일 루틴, 그리고 가격 함수를 세 칸으로 실제로 통과시키는 예까지, 전자책 ‘리팩토링의 규율’에 있습니다. 31쪽 분량으로, 이 글에서 더 깊이 보고 싶은 사람의 다음 읽을거리입니다.

참고 자료

본문의 리팩토링 정의, 특화 테스트 앵커, 작은 움직임 메뉴, 코드의 냄새, 빚과 이자 비유는 아래 자료와 대조해 보실 수 있습니다.

핵심 개념 요약 인포그래픽 2 NotebookLM이 소스를 종합해 생성한 인포그래픽입니다.

태그: characterization-test, code-quality, refactoring, software-maintenance, solo-developer, technical-debt

카테고리:

업데이트: