더 이상 내가 가장 좋아하는 Git 커밋이 아니다
원문은 Michael Lynch님이 에 게재했습니다. 이 블로그 구독하기
6년 전, 데이비드 톰슨은 동료가 작성한 기발할 정도로 상세한 커밋 메시지를 기념하며 “My favourite Git commit”이라는 인기 블로그 글을 썼다. 당시 나는 그 글을 즐겁게 읽었고, 좋은 커밋 메시지의 모범 사례로 여러 팀원에게 공유하기도 했다.
최근 유용한 커밋 메시지 작성에 대한 나만의 가이드를 만들면서 톰슨의 글을 다시 읽게 됐다. 톰슨의 글이 왜 그렇게 효과적인 예시인지 설명해 보라는 요청을 받고는, 정작 설명할 수 없다는 사실에 스스로 놀랐다. 외부 관찰자로서는 읽기 재미있는 글이었지만, 좋은 소프트웨어 엔지니어링의 모범 사례로 정당화하기는 어려웠다.
톰슨이 가장 좋아한 커밋
당시 톰슨을 비롯해 나를 포함한 많은 사람들을 매료시켰던 커밋 메시지는 다음과 같다:
Convert template to US-ASCII to fix error
기능 브랜치에서
/etc/nginx/router_routes.conf의 내용과 일치하는지 확인하는 테스트를 추가했다.bundle exec rake spec이나bundle exec rspec modules/router/spec로 실행하면 잘 동작했다. 하지만bundle exec rake로 실행하면 각 should 블록이 다음과 같은 오류와 함께 실패했다:ArgumentError: invalid byte sequence in US-ASCII결국
.with_content(//)매처를 제거하면 오류가 사라진다는 것을 알게 됐다. 스펙 파일에는 이상한 문자가 없었다. 그리고 동일한 인터프리터에서 Puppet을 require하면 다음과 같이 재현할 수 있었다:rake -E 'require "puppet"' spec해당 템플릿이 우리 코드베이스에서 인코딩이
utf-8로 식별된 유일한 파일인 것으로 보인다. 나머지는 모두us-ascii다:dcarley-MBA:puppet dcarley$ find modules -type f -exec file --mime {} \+ | grep utf modules/router/templates/routes.conf.erb: text/plain; charset=utf-8해당 파일을 다시 US-ASCII로 변환하려 하자 문제를 일으킨 문자가 공백처럼 보이는 문자임이 드러났다:
dcarley-MBA:puppet dcarley$ iconv -f UTF8 -t US-ASCII modules/router/templates/routes.conf.erb 2>&1 | tail -n5 proxy_intercept_errors off; # Set proxy timeout to 50 seconds as a quick fix for problems # iconv: modules/router/templates/routes.conf.erb:458:3: cannot convert해당 문자를 (수동으로) 교체한 뒤 파일은 다시
us-ascii로 식별됐다:dcarley-MBA:puppet dcarley$ file --mime modules/router/templates/routes.conf.erb modules/router/templates/routes.conf.erb: text/plain; charset=us-ascii이제 테스트가 동작한다! 내 인생에서 돌려받을 수 없는 한 시간이다..
“펀치라인”은, 이렇게 긴 서론 끝에 톰슨이 실제 diff를 보여준다는 점이다:
그렇다, 이 커밋 메시지에는 공백 한 글자짜리 변경을 설명하기 위해 여섯 문단과 다섯 개의 코드 스니펫이 담겨 있다.
좋아하는 것과 최고는 다르다
이 커밋이 왜 매력적인지는 쉽게 알 수 있다.
대부분의 개발자라면 이 변경을 단순히 “Fix whitespace character”라고 기록했을 것이므로, 누군가 버그를 조사하고 수정한 과정을 설명하기 위해 그토록 공을 들였다는 사실은 유쾌한 놀라움이다.
톰슨이 제시한 모든 이유에서 이는 좋은 커밋 메시지다. 검색 가능한 기록을 만들고, 개발자의 도구와 과정에 대한 유용한 인사이트를 공유하기 때문이다.
이 글은 톰슨이나 원래 커밋 작성자를 공격하려는 것이 아니다. 톰슨은 그것이 “최고의” 커밋 메시지라고 주장한 적이 없으며, 단지 자신이 가장 좋아하는 커밋이라고 했을 뿐이다.
그렇기는 하지만, 이제는 이 커밋을 모범적인 커밋 메시지로 삼지 못하게 하는 결함들이 보인다.
가장 중요한 정보를 맨 끝에 묻어둔다
톰슨이 처음 블로그 글을 게시했을 때, 가장 흔한 비판 중 하나는 커밋 메시지가 너무 장황하다는 것이었다. 나는 그 비판이 잘못됐다고 생각했다.
커밋 메시지의 상세한 내용은 관련성이 있는 한 유용하며, 톰슨의 예시도 그랬다. 경험이 적은 팀원들이 작성자의 디버깅 과정과 도구 사용법을 배우는 데 도움이 되고, 경험이 많은 팀원들에게는 개발자가 무언가를 놓쳤거나 관련 도구를 모르고 있지는 않은지 확인할 기회를 주기 때문이다.
사람들이 톰슨의 예시를 지나치게 장황하다고 느낀 이유는 가장 중요한 정보를 커밋 메시지 깊숙한 곳에 묻어두었기 때문이다.
첫 문단을 다시 읽어보자:
기능 브랜치에서
/etc/nginx/router_routes.conf의 내용과 일치하는지 확인하는 테스트를 추가했다.bundle exec rake spec이나bundle exec rspec modules/router/spec로 실행하면 잘 동작했다. 하지만bundle exec rake로 실행하면 각 should 블록이 다음과 같은 오류와 함께 실패했다:ArgumentError: invalid byte sequence in US-ASCII
커밋 메시지에서 세 문장과 코드 스니펫을 읽고도 독자는 여전히 이 변경이 실제로 무엇을 하는지에 대한 정보를 전혀 얻지 못한다.
커밋 메시지는 가장 중요한 정보를 먼저 제시하고 점차 세부 사항으로 넘어가야 한다. 언론인들은 이를 역피라미드 글쓰기 방식이라고 부른다.
언론인은 가장 많은 사람에게 관련 있는 정보를 상단에 배치하는 역피라미드 구조로 기사를 작성한다.
커밋 히스토리를 스크롤할 때 각 커밋이 나와 관련 있는지 빠르게 파악하고 싶다. 커밋 메시지는 처음부터 변경 사항에 대한 높은 수준의 요약을 제공해야 한다.
문제를 끝내 제대로 설명하지 않는다
톰슨의 예시 커밋 메시지를 끝까지 읽고 나면, 그 변경을 이해했다고 할 수 있을까?
문제 설명에 가장 가깝게 다가간 부분은 다음과 같다:
해당 템플릿이 우리 코드베이스에서 인코딩이 utf-8로 식별된 유일한 파일인 것으로 보인다. 나머지는 모두 us-ascii다:
dcarley-MBA:puppet dcarley$ find modules -type f -exec file --mime {} \+ | grep utf modules/router/templates/routes.conf.erb: text/plain; charset=utf-8
메시지는 routes.conf.erb가 UTF-8 인코딩을 가지고 있다고 말하지만, 왜 그런지는 전혀 설명하지 않는다. 다행히 이 프로젝트는 오픈소스라 직접 조사해볼 수 있었다.
문제는 routes.conf.erb의 463번째 줄에 있다:
$ cat modules/router/templates/routes.conf.erb | head -n 463 | tail -n 1
# where civica QueryPayments calls are taking too long.
일반 텍스트 에디터나 웹 브라우저로는 문제를 볼 수 없지만, xxd 같은 도구로 파일의 원시 바이트를 덤프하면 문제가 드러난다:
$ cat modules/router/templates/routes.conf.erb \
| head -n 463 | tail -n 1 \
| xxd | head -n 1
00000000: 2020 23c2 a077 6865 7265 2063 6976 6963 #..where civic
^^ ^^
US-ASCII와 UTF-8 테이블을 외우고 있지 않다면, 해당 줄의 처음 몇 글자는 다음과 같다:
| 바이트 표현 | 텍스트 표현 |
|---|---|
0x20 | ' ' (space) |
0x20 | ' ' (space) |
0x23 | '#' |
0xC2 0xA0 | ' ' (UTF-8 줄 바꿈 없는 공백) |
즉, 파일에는 바이트 시퀀스 0xC2 0xA0이 들어 있었는데, 이는 US-ASCII 파일일 수 없다는 뜻이다. 0xC2와 0xA0 모두 US-ASCII 바이트 범위를 벗어나기 때문이다.
0xC2 0xA0 시퀀스는 routes.conf.erb를 사용하는 모든 애플리케이션이 텍스트를 인코딩하는 더 새롭고 국제 친화적인 방식인 UTF-8 인코딩으로 파일을 해석해야 한다는 것을 의미한다.
톰슨의 코드베이스는 Ruby 1.9.3을 사용했는데, 이 버전은 UTF-8 인코딩을 지원했지만 파일이 명시적으로 선언하지 않으면 기본값으로 US-ASCII를 사용했다.
소스 히스토리를 뒤져보니 UTF-8 문자를 최초로 추가한 것은 커밋 5a8607이었다. 해당 커밋 메시지에는 UTF-8 문자를 추가한 이유가 전혀 언급되어 있지 않아, 아마도 실수였던 것으로 보인다.
한 Hacker News 댓글 작성자는 그럴듯한 가설을 제시하며 routes.conf.erb에 그 stray UTF-8 문자가 나타나게 된 이유를 설명했다:
the likely origin of the invalid character is somebody using an Apple Ireland/UK keyboard layout where # is Option-3 (AltGr-3), and non-breaking space is Option-Space (AltGr-Space).
-messe on Hacker News
코드를 언급하면서 링크를 제공하지 않는다
톰슨의 예시 커밋은 외부 코드를 언급하며 시작한다:
기능 브랜치에서
/etc/nginx/router_routes.conf의 내용과 일치하는지 확인하는 테스트를 추가했다.bundle exec rake spec이나bundle exec rspec modules/router/spec로 실행하면 잘 동작했다.
하지만 커밋 메시지에서는 브랜치 이름을 밝히지도, 커밋 해시를 명시하지도 않아 독자는 개발자의 발견을 재현할 방법이 전혀 없다.
커밋 메시지 후반부에는 이렇게 쓰여 있다:
결국
.with_content(//)매처를 제거하면 오류가 사라진다는 것을 알게 됐다. 스펙 파일에서는 이상한 문자를 찾지 못했다.
커밋 해시나 링크가 없으면 독자는 개발자가 말하는 매처가 무엇인지, 어떤 스펙 파일을 의미하는지 알 수 없다.
커밋 메시지에서 외부 코드를 참조한다면, 코드 리뷰어나 미래의 유지보수자가 변경의 정확한 맥락을 볼 수 있도록 명시적으로 링크해야 한다.
내가 고쳐 쓴 버전
톰슨이 가장 좋아했던 Git 커밋을 내가 고쳐 쓴 버전은 다음과 같다:
Convert routes.conf.erb template to US-ASCII
routes.conf.erb에는 5a8607에서 실수로 추가된 것으로 보이는 stray UTF-8 문자가 있다.
rake는 US-ASCII 형식을 기대하므로,routes.conf.erb에 있는 단 하나의 UTF-8 문자 때문에rake에서 테스트가 실패한다.이번 변경에서는
rake에서의 테스트 실패를 방지하기 위해 해당 UTF-8 문자를 동등한 US-ASCII 문자로 교체한다.The stray UTF-8 character
문제는
modules/router/templates/routes.conf.erb의 463번째 줄에 있다:$ cat modules/router/templates/routes.conf.erb \ | head -n 463 | tail -n 1 \ | xxd | head -n 1 00000000: 2020 23c2 a077 6865 7265 2063 6976 6963 #..where civic ^^ ^^
0xC2 0xA0은 유효한 US-ASCII 바이트 시퀀스가 아니며, UTF-8 줄 바꿈 없는 공백 문자다. US-ASCII 인코딩을 기대하며 파일을 읽는 모든 도구는 실패하게 된다.How I discovered this
기능 브랜치에서
/etc/nginx/router_routes.conf의 내용과 일치하는지 확인하는 테스트를 추가했다(abcd123 참조).bundle exec rake spec이나bundle exec rspec modules/router/spec로 실행하면 잘 동작했지만,bundle exec rake로 테스트를 실행하자 각should블록이 다음과 같은 오류와 함께 실패했다:ArgumentError: invalid byte sequence in US-ASCII결국
.with_content(//)매처를 제거하면 오류가 사라진다는 것을 알게 됐다. 스펙 파일에서는 이상한 문자를 찾지 못했다. 동일한 인터프리터에서 Puppet을 require하면 다음과 같이 오류를 재현할 수 있었다:rake -E 'require "puppet"' spec해당 템플릿이 우리 코드베이스에서
file이utf-8로 식별하는 유일한 파일인 것으로 보인다. 나머지는 모두us-ascii다:$ find modules -type f -exec file --mime {} \+ | grep utf modules/router/templates/routes.conf.erb: text/plain; charset=utf-8해당 파일을 다시 US-ASCII로 변환하려 하자 문제를 일으킨 문자가 공백처럼 보이는 문자임이 드러났다:
$ iconv -f UTF8 -t US-ASCII modules/router/templates/routes.conf.erb 2>&1 \ | tail -n5 proxy_intercept_errors off; # Set proxy timeout to 50 seconds as a quick fix for problems # iconv: modules/router/templates/routes.conf.erb:458:3: cannot convert해당 UTF-8 문자를 (수동으로) 교체한 뒤
file은routes.conf.erb를 다시us-ascii로 식별했다:$ file --mime modules/router/templates/routes.conf.erb modules/router/templates/routes.conf.erb: text/plain; charset=us-ascii이제 테스트가 동작한다! 내 인생에서 돌려받을 수 없는 한 시간이다..
내가 가한 변경은 다음과 같다:
- 메시지 앞부분에 높은 수준의 요약을 추가했다.
- UTF-8 문자와 그 출처에 대해 더 명시적인 설명을 추가했다.
- 작성자의 원래 내용 대부분을 “How I found this” 섹션으로 옮겨, 해당 부분이 추가적인 읽을거리임을 명확히 했다.
- 가벼운 문법 수정을 했다.
- 수동태를 제거해 모호함을 줄였다.
- 터미널 프롬프트를
dcarley-MBA:puppet dcarley $에서 단순한$로 간소화했다. 전자는 대부분 잡음에 불과하기 때문이다.
눈여겨볼 점은, 문제의 핵심이 장황함이 아니라 개발자가 정보를 어떻게 구성하고 제시했는지에 있었기 때문에 세부 사항을 삭제하지 않았다는 것이다.
스스로 원칙을 정의하는 것의 가치
톰슨의 글을 다시 읽으면서 소프트웨어 엔지니어링 원칙을 스스로 정의하는 것이 얼마나 가치 있는 일인지 다시금 깨달았다.
나는 톰슨이 말한 장점에 동의했기 때문에 그 커밋을 좋은 예시로 받아들였다. 커밋 메시지에서 가장 중요하다고 생각하는 자질을 직접 앉아 정의하기 전까지는 톰슨의 예시가 가진 단점을 보지 못했다.
나는 여러 해에 걸쳐 다양한 소프트웨어 엔지니어링 관행에 대한 내 관점을 설명해 왔고, 그때마다 더 나은 개발자가 됐다. 당연하게 여기던 아이디어를 비판적으로 사고하게 만들고, 항상 달성할 수 없더라도 내가 생각하는 이상이 무엇인지 기억하도록 도와주기 때문이다.
더 읽어보기
- “How to Write Useful Commit Messages” - 좋은 커밋 메시지를 구성하는 요소에 대한 나의 더 자세한 설명.
govuk-puppet 프로젝트의 발췌 부분은 Crown Government Digital Service의 저작물로, MIT License에 따라 사용되었습니다.
글을 무작위로 읽기

댓글
로그인하고 댓글 남기기