No Longer My Favorite Git Commit

Michael Lynch

더 이상 내 최애 Git 커밋이 아니다

6년 전, David Thompson은 동료가 작성한 기발하면서도 세세한 커밋 메시지를 기념하는 “내가 가장 좋아하는 Git 커밋”이라는 인기 블로그 글을 썼습니다. 당시 저는 그 글을 즐겁게 읽었고, 좋은 커밋 메시지의 모범 사례로 여러 팀원에게 공유하기도 했습니다.

최근 유용한 커밋 메시지 작성하기에 대한 나만의 가이드를 만들면서 Thompson의 글을 다시 읽어 보았습니다. Thompson의 글이 왜 그렇게 효과적인 예시인지 설명해 보라는 요청을 받고는, 정작 설명할 수 없다는 사실에 놀랐습니다. 외부 관찰자로서 읽을 때는 재미있었지만, 좋은 소프트웨어 엔지니어링의 모범으로 정당화할 수는 없었습니다.

Thompson이 가장 좋아한 커밋

다음은 당시 Thompson을 비롯해 저를 포함한 많은 사람들을 매료시켰던 커밋 메시지입니다:

템플릿을 US-ASCII로 변환하여 오류 수정

피처 브랜치에서 /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

이제 테스트가 동작합니다! 다시는 돌려받을 수 없는 한 시간..

이 긴 서론 뒤에 Thompson이 보여주는 실제 diff가 바로 이 글의 ‘펀치라인’입니다:

그렇습니다. 이 커밋 메시지는 단 한 글자의 공백 수정을 설명하기 위해 여섯 문단과 다섯 개의 코드 조각을 담고 있습니다.

최애와 최고는 다르다

이 커밋이 왜 매력적인지는 쉽게 알 수 있습니다.

대부분의 개발자라면 이 변경을 그저 “Fix whitespace character”라고만 적고 끝냈을 텐데, 누군가 버그를 조사하고 수정한 과정을 이렇게 길게 설명했다는 점은 반가운 놀라움입니다.

Thompson이 제시한 모든 이유에서 이 커밋 메시지는 좋은 메시지입니다. 검색 가능한 기록을 남기고, 개발자의 도구와 과정에 대한 유용한 통찰을 공유하니까요.

이 글은 Thompson이나 원래 커밋 작성자를 비난하려는 것이 아닙니다. Thompson은 이 커밋 메시지가 ‘최고’라고 주장한 적이 없습니다. 그저 자신이 가장 좋아하는 커밋이라고 했을 뿐입니다.

그렇다고 해도, 이제는 이 커밋을 모범 사례로 삼기에는 몇 가지 결함이 보입니다.

가장 중요한 정보를 맨 마지막에 묻어둔다

Thompson이 처음 블로그 글을 올렸을 때 가장 흔한 비판 중 하나는 커밋 메시지가 너무 장황하다는 것이었습니다. 저는 그 비판이 잘못됐다고 생각했습니다.

커밋 메시지의 자세한 내용은 관련성이 있는 한 유용하며, Thompson이 소개한 내용도 그랬습니다. 경험이 적은 팀원은 작성자의 디버깅 과정과 도구 사용법을 배울 수 있고, 경험이 많은 팀원은 개발자가 무언가를 놓쳤거나 모르는 도구가 있는지 확인할 기회가 됩니다.

사람들이 Thompson의 예시를 지나치게 장황하다고 느낀 이유는 가장 중요한 정보를 커밋 메시지 깊숙한 곳에 묻어두었기 때문입니다.

첫 문단을 다시 읽어 보겠습니다:

피처 브랜치에서 /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

세 문장과 코드 조각을 읽고도 독자는 이 변경이 실제로 무엇을 하는지에 대한 정보를 전혀 얻지 못합니다.

커밋 메시지는 가장 중요한 정보를 먼저 제시하고 점차 세부 사항으로 넘어가야 합니다. 언론인들은 이를 역피라미드 글쓰기 방식이라고 부릅니다.

역피라미드

언론인은 가장 많은 사람에게 필요한 정보를 맨 위에 두는 역피라미드 구조로 뉴스를 작성합니다.

커밋 히스토리를 훑어볼 때 저는 각 커밋이 자신과 관련이 있는지 빨리 알고 싶습니다. 커밋 메시지는 맨 처음부터 변경 내용을 큰 그림에서 요약해 주어야 합니다.

문제를 제대로 설명하지 않는다

Thompson이 예시로 든 커밋 메시지를 끝까지 읽어도, 과연 이 변경을 이해할 수 있을까요?

문제 설명에 가장 가깝게 다가간 부분은 다음과 같습니다:

해당 템플릿은 우리 코드베이스에서 인코딩이 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 인코딩을 가지고 있다고 말하지만, 왜 그런지는 전혀 설명하지 않습니다. 다행히 이 프로젝트는 오픈소스라서 직접 조사해 볼 수 있었습니다.

문제는 463번째 줄에 있는 routes.conf.erb에 있습니다:

$ 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이 들어 있었고, 0xC20xA0 모두 US-ASCII 바이트 범위를 벗어나므로 이 파일은 US-ASCII 파일이 될 수 없습니다.

0xC2 0xA0 시퀀스는 routes.conf.erb를 소비하는 모든 애플리케이션이 텍스트를 인코딩하는 더 새롭고 국제 친화적인 방식인 UTF-8 인코딩으로 해석해야 함을 의미합니다.

Thompson의 코드베이스는 Ruby 1.9.3을 사용했는데, 이 버전은 UTF-8 인코딩을 지원했지만 파일이 명시적으로 선언하지 않으면 기본값으로 US-ASCII를 사용했습니다.

소스 히스토리를 뒤져보니 커밋 5a8607에서 처음 UTF-8 문자가 도입된 것을 확인했습니다. 해당 커밋 메시지에는 UTF-8 문자를 도입한 이유가 전혀 언급되어 있지 않아, 아마도 실수였던 것으로 보입니다.

한 Hacker News 댓글 작성자는 그럴듯한 가설을 제시하며 routes.conf.erb에 그 stray UTF-8 문자가 왜 생겼는지 설명했습니다:

잘못된 문자가 생긴 가장 유력한 원인은 누군가 Apple 아일랜드/영국 키보드 레이아웃을 사용했기 때문이다. 이 레이아웃에서는 #이 Option-3(AltGr-3)이고, 줄 바꿈 없는 공백은 Option-Space(AltGr-Space)이다.

-messe on Hacker News

코드를 언급하면서 링크를 제공하지 않는다

Thompson이 예시로 든 커밋은 외부 코드를 언급하며 시작합니다:

피처 브랜치에서 /etc/nginx/router_routes.conf의 내용과 일치하는 몇 가지 테스트를 추가했습니다. bundle exec rake spec이나 bundle exec rspec modules/router/spec으로 실행하면 문제없이 동작했습니다.

하지만 커밋 메시지는 브랜치 이름도, 커밋 해시도 명시하지 않으므로 독자는 개발자의 발견을 재현할 방법이 없습니다.

커밋 메시지는 뒤에서 이렇게 말합니다:

결국 .with_content(//) 매처를 제거하면 오류가 사라진다는 것을 알게 됐습니다. 스펙 파일에서는 이상한 문자를 찾지 못했습니다.

커밋 해시나 링크가 없으면 독자는 개발자가 말하는 매처가 무엇인지, 스펙 파일이 어떤 파일인지 알 수 없습니다.

커밋 메시지에서 외부 코드를 언급할 때는 명시적으로 링크를 걸어야 코드 리뷰어와 이후 유지보수자가 변경의 정확한 맥락을 볼 수 있습니다.

내가 다시 써 본 커밋

Thompson이 가장 좋아한 Git 커밋을 제가 다시 써 본 버전은 다음과 같습니다:

routes.conf.erb 템플릿을 US-ASCII로 변환

routes.conf.erb에 stray UTF-8 문자가 하나 있는데, 5a8607에서 실수로 들어간 것으로 보입니다.

rake는 US-ASCII 형식을 기대하므로, routes.conf.erb에 있는 단 하나의 UTF-8 문자 때문에 rake에서 테스트가 실패합니다.

이번 변경에서는 rake에서의 테스트 실패를 방지하기 위해 해당 UTF-8 문자를 동등한 US-ASCII 문자로 교체합니다.

stray UTF-8 문자

문제는 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 인코딩을 기대하며 파일을 읽는 모든 도구는 실패하게 됩니다.

발견 경위

피처 브랜치에서 /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

해당 템플릿은 우리 코드베이스에서 fileutf-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 문자를 교체한 뒤에는 fileroutes.conf.erb를 다시 us-ascii로 식별합니다:

$ file --mime modules/router/templates/routes.conf.erb
modules/router/templates/routes.conf.erb: text/plain; charset=us-ascii

이제 테스트가 동작합니다! 다시는 돌려받을 수 없는 한 시간..

제가 바꾼 점은 다음과 같습니다:

  • 메시지 앞부분에 high-level 요약을 추가했습니다.
  • UTF-8 문자와 그 출처에 대해 더 명시적인 설명을 추가했습니다.
  • 작성자의 원래 내용 대부분을 “발견 경위” 섹션으로 옮겨, 보너스 읽기 자료임을 분명히 했습니다.
  • 가벼운 문법 수정을 했습니다.
  • 수동태를 없애 모호함을 줄였습니다.
  • 터미널 프롬프트를 dcarley-MBA:puppet dcarley $에서 단순한 $로 단순화했습니다. 앞의 것은 대부분 노이즈에 불과하기 때문입니다.

눈여겨볼 점은 제가 세부 내용을 삭제하지 않았다는 것입니다. 문제는 장황함이 아니라 개발자가 정보를 어떻게 구성하고 제시했느냐에 있었기 때문입니다.

자신만의 원칙을 정의하는 가치

Thompson의 글을 다시 읽으며 스스로 소프트웨어 엔지니어링 원칙을 정의하는 것이 얼마나 가치 있는 일인지 다시금 깨달았습니다.

저는 Thompson이 말한 장점에 동의했기 때문에 그 커밋을 좋은 예시로 받아들였습니다. 커밋 메시지에서 가장 중요하다고 생각하는 자질을 직접 정의하고 나서야 Thompson의 예시가 가진 단점이 보이기 시작했습니다.

저는 지난 몇 년간 여러 소프트웨어 엔지니어링 관행에 대한 제 관점을 글로 정리해 왔고, 그럴 때마다 더 나은 개발자가 되었습니다. 당연하게 여기던 아이디어를 비판적으로 생각하게 만들고, 항상 달성하지 못하더라도 이상적인 모습이 무엇인지 기억하게 해 주기 때문입니다.

더 읽어보기


govuk-puppet 프로젝트 발췌 부분은 Crown Government Digital Service의 저작물이며, MIT License에 따라 사용되었습니다.

원문은 Michael Lynch님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.