포스트

보이스카웃 규칙, 책에서 어디선가 본 적은 있긴 한데,,

보이스카웃 규칙, 책에서 어디선가 본 적은 있긴 한데,,

며칠 전, 회사에서 팀장님과 PR 리뷰를 진행하다가, 팀장님께서 내게 물으셨다.

“추천해드린 Clean Code 책에 보면 ‘보이스카웃 규칙’이라고 나오는데, 살펴본 적 있어요?”

책에서 본 것 같긴 했다. 분명 어딘가 페이지를 넘기다 마주친 단어였다. 그런데 그게 정확히 어떤 내용이었는지, 그 자리에서 답을 내놓을 수가 없었다. 더듬더듬 비슷한 말을 꺼내보긴 했는데, 정확한 정의도 아니었고 내 것도 아니었다.

당황스러우면서 솔직히 부끄러웠다. 열심히 하는 모습만 보여드리고 싶었는데, 그러지 못한 게 그날 내내 마음에 걸렸다. 그런데 신기하게도 그 부끄러움은 며칠이 지나도 사라지지 않고, 오히려 그 키워드를 머릿속에 못박아두는 역할을 했다. 이 글은 그 키워드가 왜 내게 그렇게 들러붙었는지, 그리고 그게 어떤 깨달음으로 이어졌는지에 대한 기록이다.

보이스카웃 규칙이 뭔지

원래 보이스카웃에는 이런 격언이 있다고 한다. “캠프장은 처음 왔을 때보다 깨끗하게 하고 떠나라.” Robert C. Martin은 이걸 코드에 적용했다. 내가 작성하지 않은 코드라도, 내가 손댄 김에 조금이라도 더 낫게 만들고 떠나라는 것. 변수명 하나라도 더 명확하게, 죽은 코드 한 줄이라도 지우고, 흐름이 꼬인 부분이 있으면 살짝이라도 펴주고.

말로 들으면 당연한 얘기다. 그래서 책에서 봤을 때도 “그렇지” 하고 넘겼던 것 같다. 진짜로 이 규칙이 살아 움직이기 시작한 건, 며칠 뒤 내가 짠 코드를 다시 마주한 순간부터였다.

2달 전 내 코드를 살펴보니… 심각하다

그날 나는 2달 전쯤 작성해둔 코드를 다시 열어봐야 했다. 기능을 조금 손봐야 하는 상황이었다.

코드를 읽기 시작하자마자 거부감이 올라왔다. 내가 짠 코드인데, 내가 거부감이 들었다.

뭐가 거슬렸는지 하나하나 떠올려보면 이렇다.

변수명과 함수명이 의미 불명이었다. data, result, tmp 같은 이름들이 곳곳에 흩어져 있어서, 이 변수가 뭘 담고 있는 건지 위로 올라가서 다시 읽어야 했다. 함수 하나가 너무 길어서 흐름을 한 번에 따라가기 힘들었다. 무슨 의도로 짠 건지 주석이라도 있으면 좋았을 텐데, 정작 필요한 곳엔 주석이 없었고 엉뚱한 곳에 옛날 주석이 남아있었다. 팀에서 정한 컨벤션에서 벗어난 부분도 곳곳에 섞여 있었다. 어디는 따르고 어디는 안 따르고. 거기에 더해 쓰이지 않는 함수와 주석 처리된 코드 블록이 그대로 방치되어 있었다.

그게 누가 짠 코드도 아니고 2달 전의 나였다. 책에서 보이스카웃 규칙이 “남이 더럽혀놓은 캠프장을 청소하는 이야기”인 줄 알았는데, 정작 마주한 건 내가 더럽혀놓은 캠프장이었다.

이 장면 위에 팀장님이 던지셨던 키워드가 겹쳐졌다. 그제야 그 개념이 머릿속에서 살아 움직이기 시작했다. 책으로 봤을 때는 “당연한 말”이었던 게, 내 코드를 보고 나니 “절실한 말”이 되어 있었다.

왜 그렇게 짰냐 기철아,,👊

거부감보다 더 마음에 걸렸던 건, 왜 내가 그렇게 짰는지를 돌아봤을 때 나오는 답이었다.

이유는 솔직히 명확했다.

첫째, Django에 대한 기초적인 이해가 부족했다. ORM이 제공하는 도구를 충분히 알지 못한 채로 짜다 보니, 우회하는 코드가 많이 나왔다.

둘째, 일정이 급했다. “일단 돌아가게” 만드는 게 급선무였고, 다듬는 건 나중 일이었다. 그리고 그 “나중”은 오지 않았다.

셋째, 이게 가장 마음에 걸리는 부분인데, Claude Code의 플랜 모드만으로 거의 블랙박스처럼 구현을 맡겨버렸다. 빠르게 결과물이 나오니까 검토 없이 흘려보냈다. 그런데 Claude Code가 참고한 건 팀 컨벤션을 따르지 않은 채 남아있던 레거시 코드였다. 결과적으로 컨벤션이 더 흐트러진 코드가 생성됐고, 그게 그대로 머지됐다.

빠르게 짜준 도구를 탓하려는 게 아니다. 도구는 도구일 뿐이고, 결국 캠프장을 떠날 때 한 번 더 둘러보지 않은 건 나였다. 다만 이 경험에서 한 가지는 분명해졌다.

속도와 청소는 별개의 문제다. AI 도구는 짜는 속도를 줄여준다. 그런데 청소까지 자동으로 해주진 않는다. 오히려 빠르게 짜는 만큼 캠프장이 더러워지는 속도도 빨라진다. 게다가 AI가 참고하는 게 이미 더러운 캠프장이라면, 결과물은 그 더러움을 학습해서 한 번 더 더럽힌다.

생각해보면 AI 시대에는 보이스카웃 규칙이 더 중요해진 게 아닐까. 더러워지는 속도가 빨라진 만큼, 의식적으로 청소하지 않으면 캠프장은 손쓸 수 없게 망가진다.

코드로 보면 이런 거다 (Python/Django)

가상의 예로 사용자 목록을 가져오는 코드를 보자. 2달 전의 나라면 이렇게 짰을 법한 모습이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_data(req):
    # 활성 유저 가져오기
    tmp = User.objects.all()
    result = []
    for u in tmp:
        if u.is_active == True:
            result.append(u)
    # data = User.objects.filter(is_active=True)  # 옛날 방식
    
    final = []
    for r in result:
        final.append({
            'id': r.id,
            'name': r.name,
        })
    return JsonResponse({'data': final})

tmp, result, final, data — 다 무슨 뜻인지 모르겠는 이름들이다. is_active == True 같은 불필요한 비교, 주석 처리된 죽은 코드, Django ORM이 제공하는 filtervalues를 두고 Python에서 직접 도는 비효율까지. 짧은 함수인데도 다 거슬린다.

캠프장을 청소하고 떠나면 이렇게 된다.

1
2
3
def get_active_users(request):
    users = User.objects.filter(is_active=True).values('id', 'name')
    return JsonResponse({'data': list(users)})

거창한 리팩터링이 아니다. 함수명을 의도가 드러나게 바꾸고, ORM의 도구를 쓰고, 죽은 코드를 지웠을 뿐이다. 그런데 다음에 이 코드를 열어볼 누군가는(아마도 미래의 나는) 거부감 없이 읽을 수 있다.

프론트엔드도 마찬가지 (JS/React)

상품 검색 컴포넌트를 가정해보자. 정리되지 않은 버전이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import _ from 'lodash';  // 사용 안 함

function Comp({ d, fn }) {
  const [data, setData] = useState([]);
  const [tmp, setTmp] = useState('');

  useEffect(() => {
    axios.get('/api/products?q=' + tmp).then(r => {
      setData(r.data);
    });
  }, [tmp]);

  // const handleOld = () => { ... }

  return (
    <div>
      <input value={tmp} onChange={e => setTmp(e.target.value)} />
      {data.map(x => <div key={x.id} onClick={() => fn(x)}>{x.n}</div>)}
    </div>
  );
}

Comp, d, fn, tmp, x, n — 컴포넌트 이름과 prop 이름부터 의미를 알 수 없다. 안 쓰이는 import, 주석 처리된 핸들러, 검색어가 바뀔 때마다 디바운스 없이 호출되는 API까지. 한 번 보고 흐름을 이해하기가 쉽지 않다.

지나가는 김에 조금만 청소하면 이렇게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { useState, useEffect } from 'react';
import axios from 'axios';

function ProductSearch({ onSelect }) {
  const [products, setProducts] = useState([]);
  const [keyword, setKeyword] = useState('');

  useEffect(() => {
    if (!keyword) return;
    const timer = setTimeout(() => {
      axios.get(`/api/products?q=${keyword}`).then(res => setProducts(res.data));
    }, 300);
    return () => clearTimeout(timer);
  }, [keyword]);

  return (
    <div>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      {products.map(product => (
        <div key={product.id} onClick={() => onSelect(product)}>
          {product.name}
        </div>
      ))}
    </div>
  );
}

언어와 프레임워크가 달라도 캠프장 청소의 원리는 같다. 의미 있는 이름, 죽은 코드 제거, 명백한 버그성 패턴(디바운스 누락) 보완. 작업하러 들어온 김에 눈에 보이는 만큼만 정리하는 것.

5S, TPM — 같은 이야기를 다른 분야에서 하고 있더라

이 깨달음을 곱씹다 보니, 비슷한 개념을 다른 분야에서 본 적이 있다는 게 떠올랐다.

제조업에는 5S라는 개념이 있다. 정리(필요 없는 것 버리기), 정돈(필요한 걸 찾기 쉽게), 청소, 청결(앞의 세 단계를 표준화하기), 생활화(습관으로 만들기). 그리고 그 위에 TPM(전사적 생산보전) 이라는 더 큰 틀이 있다. 핵심은 단순하다. 작업 환경은 만들어지는 게 아니라 유지되는 것이다.

보이스카웃 규칙은 결국 소프트웨어 버전의 5S였다. 분야가 전혀 다른데 결론이 같다는 건, 이게 직군을 넘는 보편 원리에 가깝다는 뜻 같다. 좋은 환경은 만들어내는 게 아니라, 매일 조금씩 지켜내는 것이라는 원리.

다만 과하면 독이 된다

물론 균형 감각도 필요하다.

모든 PR마다 캠프장 전체를 청소하려 들면 PR 범위가 폭발한다. 원래 작업과 무관한 변경이 많아지면 리뷰어가 힘들어지고, 변경 의도가 흐려지고, 코드 리뷰는 본질에서 멀어진다. “이번 PR이 뭘 바꾸려는 거였는지” 자체가 안 보이게 된다.

그래서 보이스카웃 규칙은 완벽주의가 아니라 생활화에 가까운 개념이라고 생각한다. 지나가는 김에 눈에 들어오는 만큼만 깔끔하게. 한 번에 캠프장 전체를 갈아엎는 게 아니라, 매번 조금씩 더 나은 상태로 떠나는 것. 5S에서 마지막 항목이 “생활화”인 것도 같은 맥락일 거다.

이제는 나도 보이스카웃 규칙이 뭔지 몸으로 깨달았다.

팀장님이 다시 물어봐주신다면, 이제는 답할 수 있을 것 같다.

다만 책에서 본 정의를 외워서가 아니다. 2달 전 내 코드를 마주한 그날의 거부감이 답이 됐기 때문이다. 책의 한 페이지로 머물던 개념이, 내 키보드 앞에서의 감각으로 바뀌었다.

소프트웨어 개발주기의 80%는 유지보수라고들 한다. 그 80%를 좌우하는 건 거창한 아키텍처나 화려한 신기술이 아니라, 지나갈 때마다 조금씩 청소하는 생활화 일지도 모른다. 코드를 빠르게 짜주는 도구는 점점 늘어나고 있지만, 캠프장을 떠날 때 한 번 더 둘러보는 건 여전히 사람의 몫이다.

부끄러웠던 그날의 질문이, 결국 가장 오래 남는 배움이 됐다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.