Skip to content
Published on

의견 있는 도구 — gofmt, gofumpt와 코드 스타일의 정치학

Authors

들어가며 — 다시 떠오른 포매터 논쟁

최근 GeekNews에서 gofumpt에 관한 글이 다시 화제가 되었습니다. "gofmt만으로는 부족하다"며 더 엄격한 규칙을 적용하는 이 포매터를 두고, 의견 있는 도구(opinionated tooling)가 과연 어디까지 강제해도 좋은가에 대한 논쟁이 다시 불붙었습니다.

흥미로운 것은 이 논쟁이 단순한 취향 다툼이 아니라는 점입니다. 들여쓰기를 탭으로 할지 스페이스로 할지, 중괄호를 어디에 둘지 같은 사소해 보이는 결정들은 사실 팀의 시간과 감정을 놀라울 만큼 많이 소모해 왔습니다. Go 진영은 이 문제를 gofmt라는 단 하나의 도구로 정리해 버렸고, 그 결단은 지금도 다른 언어 생태계가 부러워하는 모델로 남아 있습니다.

이 글에서는 자동 포매팅의 철학에서 출발해, gofmt와 더 엄격한 gofumpt를 비교하고, prettier와 black 같은 다른 언어의 도구와 견주어 봅니다. 그리고 의견 있는 도구가 팀의 합의와 CI에 어떻게 녹아드는지, 그 장단점은 무엇인지 깊이 들여다보겠습니다.

자동 포매팅의 철학 — 논쟁을 끝내기 위한 도구

코드 스타일 논쟁은 소프트웨어 역사만큼이나 오래되었습니다. 그리고 그 논쟁의 대부분은 정답이 없는 취향의 문제였습니다. 어느 쪽이든 일관성만 있다면 코드는 잘 동작합니다. 그럼에도 사람들은 이 사소한 차이에 대해 끝없이 논쟁했습니다.

Go 언어 설계자들의 통찰은 여기에 있었습니다. "스타일에 정답은 없다. 그러니 논쟁 자체를 없애자." gofmt는 옳은 스타일을 강제하기 위한 도구가 아니라, 스타일에 대한 논쟁을 끝내기 위한 도구입니다. 어떤 스타일인지는 중요하지 않습니다. 모두가 같은 스타일을 쓴다는 사실이 중요합니다.

이 철학은 한 문장으로 요약됩니다. "Gofmt's style is no one's favorite, yet gofmt is everyone's favorite." 누구도 gofmt의 스타일을 가장 좋아하지는 않지만, 모두가 gofmt를 가장 좋아한다는 말입니다.

// 작성자가 어떻게 쓰든
func   add(a int,b int)int{return a+b}

// gofmt를 거치면 항상 같은 모양이 된다
func add(a int, b int) int {
	return a + b
}

핵심은 "선택지를 없앤다"는 데 있습니다. 선택지가 없으면 논쟁할 거리도 없습니다. 그리고 논쟁이 없으면 그 에너지를 실제 문제 해결에 쏟을 수 있습니다.

gofmt의 탄생 — Go 초창기의 결단

이 철학이 단순한 구호가 아니라 도구로 구현된 배경에는 Go 언어 초창기의 결단이 있었습니다. Go는 2009년 공개될 때부터 gofmt를 함께 들고 나왔습니다. 언어가 막 세상에 나오는 시점에 포매터를 표준 도구로 못 박은 것은 당시로서는 대담한 선택이었습니다. 대부분의 언어는 수년에 걸쳐 여러 스타일 가이드가 난립한 뒤에야 뒤늦게 포매터를 만들었기 때문입니다.

Go 팀이 일찍 포매터를 표준화한 이유는 명확했습니다. 스타일 파편화가 일어나기 전에 못을 박아야 한다는 것입니다. 일단 여러 스타일이 코드베이스에 뿌리내리면, 그 다음에 표준을 도입하는 것은 정치적으로 거의 불가능해집니다. 모두가 자기 스타일에 익숙해진 뒤이기 때문입니다. Go는 그 혼란이 시작되기도 전에 단 하나의 답을 제시함으로써 이 문제를 원천 봉쇄했습니다.

Rob Pike와 Russ Cox를 비롯한 Go 설계자들의 사고방식은 이른바 "Go 잠언(Go Proverbs)"에 잘 드러납니다. 이 잠언들은 Go가 추구하는 단순함과 명료함의 가치를 압축한 문장들입니다.

  • "Gofmt's style is no one's favorite, yet gofmt is everyone's favorite."
  • "A little copying is better than a little dependency."
  • "Clear is better than clever."
  • "The bigger the interface, the weaker the abstraction."

이 잠언들을 관통하는 정신은 일관됩니다. 영리함보다 명료함을, 추상화의 과시보다 단순함을 택한다는 것입니다. gofmt는 바로 이 정신을 코드의 표면에 적용한 결과물입니다. "어떤 스타일이 더 영리한가"를 따지는 대신, "모두가 같은 모양을 본다"는 명료함을 선택한 것입니다.

흥미롭게도 gofmt는 단순한 포매터를 넘어 Go의 도구 문화 전체에 영향을 주었습니다. gofmt가 코드의 AST(추상 구문 트리)를 다루는 방식은 이후 gorename, gopls 같은 도구들이 코드를 안전하게 변형하는 기반이 되었습니다. 즉 gofmt는 "코드를 텍스트가 아니라 구조로 다룬다"는 Go 도구 생태계의 출발점이기도 했습니다.

gofmt — 표준이 된 포매터

gofmt는 Go 표준 도구 체인에 처음부터 포함되어 있습니다. 별도 설치도, 설정 파일도 필요 없습니다. 이 "설정 없음"이라는 특징은 의도된 설계입니다. 설정이 가능하다면 사람들은 설정을 두고 또 논쟁할 테니까요.

# 한 파일 포매팅
gofmt -w main.go

# 패키지 전체 포매팅
go fmt ./...

# 변경 사항만 미리 보기 (쓰지 않고 diff 출력)
gofmt -d main.go

gofmt가 다루는 것은 주로 다음과 같습니다.

  • 들여쓰기는 탭으로 통일
  • 연산자 주변과 콤마 뒤의 공백 정규화
  • 임포트 블록의 정렬과 그룹화
  • 구조체 필드와 정렬 가능한 코드의 정렬
  • 불필요한 괄호 제거

여기서 한 가지 분명히 해야 할 점이 있습니다. gofmt는 일부러 "최소한"만 건드립니다. 동작에 영향을 주지 않으면서도 사람마다 의견이 갈릴 수 있는 영역 중 일부는 여전히 손대지 않고 남겨둡니다. 바로 그 빈틈을 파고든 것이 gofumpt입니다.

gofumpt — gofmt보다 한 걸음 더

gofumpt는 "gofmt가 옳지만 충분히 엄격하지는 않다"는 문제의식에서 출발했습니다. 이름부터 gofmt에 대한 일종의 말장난으로, gofmt의 출력은 모두 그대로 유효하면서 추가 규칙을 더 적용합니다. 즉 gofumpt를 통과한 코드는 gofmt도 항상 통과합니다(상위 호환).

gofumpt가 추가로 강제하는 규칙의 예를 몇 가지 보겠습니다.

// gofmt는 허용하지만 gofumpt는 정리하는 경우들

// (1) 함수 본문 시작과 끝의 불필요한 빈 줄 제거
func before() {

	doSomething()

}

func after() {
	doSomething()
}

// (2) 짧은 변수 선언 그룹화 권장
// (3) 불필요하게 긴 빈 줄 축소
// (4) 일부 복합 리터럴의 일관된 형태 강제

gofumpt의 매력은 "추가 설정 없이 더 깔끔하다"는 데 있습니다. gofmt의 무설정 철학을 그대로 계승하면서, 사람들이 흔히 동의하는 추가 정리를 자동으로 해줍니다. 많은 팀이 gofmt 대신 gofumpt를 기본으로 채택하는 이유입니다.

다만 gofumpt가 공식 표준은 아니라는 점은 기억해야 합니다. gofmt는 Go 팀이 보증하는 단일 표준이지만, gofumpt는 그보다 한 단계 더 의견을 얹은 서드파티 도구입니다. 이 미묘한 차이가 다음에 볼 정치학의 출발점입니다.

gofumpt 규칙 더 깊이 들여다보기

앞에서는 gofumpt의 규칙을 큰 틀에서만 봤습니다. 이제 좀 더 구체적인 규칙들을 코드로 살펴보겠습니다. 이런 세부 규칙들이 모여 "추가 설정 없이 더 깔끔한" gofumpt의 인상을 만듭니다.

첫째, 8진수 리터럴 표기를 현대적인 형태로 통일합니다. Go 1.13부터 도입된 0o 접두사를 권장합니다.

// gofmt는 둘 다 허용하지만, gofumpt는 0o 형태를 권장한다
perm := 0755   // 오래된 8진수 표기
perm := 0o755  // gofumpt가 선호하는 명시적 표기

둘째, 함수 시그니처에서 같은 타입의 연속된 파라미터를 그룹화하도록 유도합니다.

// 장황한 형태
func move(x int, y int, z int) {}

// gofumpt가 권장하는 그룹화된 형태
func move(x, y, z int) {}

셋째, import 그룹 정렬을 더 엄격하게 다룹니다. gofmt도 import를 정렬하지만, gofumpt는 표준 라이브러리와 서드파티 패키지 사이에 의미 없는 빈 그룹이 흩어지는 것을 정리합니다.

// gofumpt는 불필요하게 쪼개진 import 그룹을 정돈한다
import (
	"fmt"
	"strings"

	"github.com/foo/bar"
)

넷째, 빈 줄에 대한 규칙이 더 촘촘합니다. 블록의 시작과 끝에 붙은 빈 줄, 연속된 두 개 이상의 빈 줄 등을 정리합니다.

// gofumpt가 정리하는 빈 줄 패턴
type Config struct {
	Name string

	Port int
}

// 위처럼 필드 사이에 의미 없이 들어간 단일 빈 줄도
// 맥락에 따라 정돈 대상이 된다

이런 규칙들은 하나하나 보면 사소합니다. 하지만 코드베이스 전체에 일관되게 적용되면, 누가 작성했는지 알 수 없을 만큼 균질한 코드가 만들어집니다. 바로 이 균질함이 의견 있는 도구가 노리는 효과입니다.

goimports — 포매팅과 import 관리의 결합

gofmt 계열을 이야기할 때 빠뜨릴 수 없는 도구가 goimports입니다. goimports는 gofmt가 하는 모든 포매팅을 그대로 수행하면서, 추가로 import 구문을 자동으로 관리합니다. 사용하지 않는 import를 제거하고, 코드에서 쓰이지만 import되지 않은 패키지를 자동으로 찾아 추가합니다.

# 설치
go install golang.org/x/tools/cmd/goimports@latest

# gofmt처럼 쓰되 import까지 정리한다
goimports -w main.go

# 로컬 패키지 그룹을 별도로 분리하는 옵션
goimports -local github.com/myorg -w ./...

-local 옵션은 특히 유용합니다. 표준 라이브러리, 서드파티, 그리고 우리 조직의 내부 패키지를 각각 별도 그룹으로 정렬해 주기 때문에, import 블록만 봐도 의존성의 출처가 한눈에 들어옵니다.

많은 개발자가 일상에서 gofmt 대신 goimports를 기본 포매터로 씁니다. 어차피 import 정리는 항상 필요한 작업이고, goimports는 그것을 포매팅과 한 번에 처리하기 때문입니다. gofumpt 역시 goimports와 결합해 쓸 수 있어서, "gofumpt 수준의 엄격함 + 자동 import 관리"라는 조합이 실무에서 인기가 많습니다.

에디터 통합 — 저장하면 알아서 정리되는 경험

포매터의 가치를 일상에서 가장 크게 체감하는 순간은 "저장할 때 자동 포맷(format on save)"이 걸려 있을 때입니다. 개발자가 의식하지 않아도, 파일을 저장하는 순간 코드가 표준 형태로 정리됩니다. 이렇게 하면 포매팅은 더 이상 "기억해서 실행해야 하는 작업"이 아니라 "그냥 일어나는 일"이 됩니다.

Go 진영에서는 gopls(Go 공식 언어 서버)가 이 경험의 중심에 있습니다. gopls는 에디터와 연동되어 코드 완성, 정의로 이동 같은 기능뿐 아니라 저장 시 포매팅과 import 정리까지 담당합니다. 대부분의 에디터에서 gopls를 통해 gofmt 또는 gofumpt를 저장 트리거에 연결할 수 있습니다.

// VS Code의 settings.json 예시 — 저장 시 gofumpt로 포맷
{
	"editor.formatOnSave": true,
	"gopls": {
		"formatting.gofumpt": true
	}
}

여기서 중요한 점은 팀원 전체가 같은 에디터 설정을 공유해야 효과가 극대화된다는 것입니다. 누군가는 저장 시 포맷을 켜고 누군가는 끄면, 커밋마다 포맷이 들쭉날쭉해져 diff가 지저분해집니다. 그래서 많은 팀이 에디터 설정 자체를 저장소에 함께 커밋하거나, 뒤에서 볼 EditorConfig 같은 도구로 최소한의 공통 규칙을 공유합니다.

다른 언어와의 비교 — prettier, black, rustfmt

Go가 gofmt로 만든 모델은 다른 언어 생태계에도 큰 영향을 주었습니다. 몇 가지를 비교해 보겠습니다.

도구언어설정 가능성철학
gofmtGo거의 없음표준 강제, 논쟁 종식
gofumptGo없음 (더 엄격)gofmt + 추가 정리
prettierJS/TS 등일부 가능의견 있되 약간의 여지
blackPython거의 없음"타협 없는 포매터"
rustfmtRust상당히 가능표준 권장, 설정 허용

여기서 흥미로운 스펙트럼이 보입니다. 한쪽 끝에는 설정을 거의 허용하지 않는 gofmt와 black이 있고, 다른 끝에는 상당한 설정을 허용하는 rustfmt가 있습니다. prettier는 그 중간 어딘가에 위치합니다.

Python의 black은 스스로를 "The Uncompromising Code Formatter"라고 부릅니다. 이름에서부터 의견 있는 도구의 태도가 그대로 드러납니다. black 역시 설정 가능성을 최소화함으로써 "줄 길이 몇 자로 할까" 같은 논쟁을 봉쇄합니다.

반면 prettier는 일부 옵션(따옴표 종류, 세미콜론 유무 등)을 남겨두어 약간의 여지를 줍니다. 이는 자바스크립트 생태계의 다양성을 반영한 현실적 타협이지만, 동시에 "그 옵션을 두고 또 논쟁이 벌어진다"는 부작용도 낳습니다. 설정을 허용하는 순간, 그 설정 자체가 새로운 논쟁거리가 되는 것입니다.

설정 가능성의 스펙트럼 — dprint, Biome, clang-format, EditorConfig

앞의 표는 대표적인 도구 몇 개만 다뤘습니다. 시야를 넓혀 보면, 포매터들은 "설정을 얼마나 허용하는가"라는 축 위에 하나의 긴 스펙트럼을 이룹니다. 양극단을 이해하면 우리 팀이 어디에 서야 할지 판단하기 쉬워집니다.

한쪽 끝에는 거의 설정을 허용하지 않는 gofmt, gofumpt, black이 있습니다. 이들은 "선택지를 없애는 것" 자체가 목적입니다. 반대쪽 끝에는 거의 모든 것을 설정할 수 있는 도구들이 있습니다. 그 중간에 다양한 절충안이 자리합니다.

  • dprint: Rust로 작성된 빠른 멀티 언어 포매터로, 플러그인 구조를 통해 여러 언어를 한 도구로 다룹니다. prettier보다 설정 가능성을 의식적으로 더 열어 둔 편입니다.
  • Biome: JavaScript/TypeScript 생태계에서 prettier와 ESLint를 하나로 묶으려는 시도입니다. 포매팅과 린팅을 단일 도구로 통합해 빠른 속도를 강점으로 내세웁니다.
  • clang-format: C/C++ 진영의 대표 포매터로, 설정 가능성의 극단에 가깝습니다. 들여쓰기, 중괄호 위치, 정렬 방식 등 수십 가지 옵션을 세밀하게 조정할 수 있고, LLVM·Google·Mozilla 같은 사전 정의된 스타일도 제공합니다. 유연하지만, 바로 그 유연함 때문에 "우리 팀의 clang-format 설정"을 두고 논쟁이 벌어지곤 합니다.
  • EditorConfig: 엄밀히 말하면 포매터가 아니라, 에디터 간에 최소한의 공통 규칙(들여쓰기 종류, 줄 끝 문자, 파일 끝 개행 등)을 공유하기 위한 설정 표준입니다. 언어와 에디터를 가리지 않고 동작하기 때문에, 본격적인 포매터를 도입하기 전 단계의 "최소 합의"로 널리 쓰입니다.
# .editorconfig 예시 — 에디터 간 최소 공통 규칙
root = true

[*]
indent_style = tab
end_of_line = lf
insert_final_newline = true
charset = utf-8

이 스펙트럼이 주는 교훈은 분명합니다. 설정 가능성이 높을수록 도구는 더 많은 상황에 적응하지만, 그만큼 "설정을 어떻게 할까"라는 새로운 논쟁을 떠안습니다. 반대로 설정 가능성이 낮을수록 적응력은 떨어지지만 논쟁은 사라집니다. Go가 의도적으로 후자를 택했다는 사실은, 도구 설계가 곧 가치 판단임을 보여 줍니다.

린트와 포맷은 다르다

여기서 자주 혼동되는 개념을 정리하고 넘어가겠습니다. 포매팅(format)과 린팅(lint)은 목적이 다릅니다.

  • 포매팅: 코드의 "모양"을 다룹니다. 들여쓰기, 공백, 줄바꿈 등 동작에는 영향이 없는 표면적 형태입니다.
  • 린팅: 코드의 "내용"에 대한 잠재적 문제를 지적합니다. 사용하지 않는 변수, 의심스러운 비교, 가능한 버그 패턴 등입니다.
# 포매팅 - 모양을 자동으로 고침
gofumpt -w ./...

# 린팅 - 문제를 지적 (golangci-lint 예시)
golangci-lint run ./...

이 둘을 혼동하면 도구 선택이 꼬입니다. 포매터는 "이렇게 생기지 않은 코드는 자동으로 고친다"는 입장이고, 린터는 "이건 문제일 수 있으니 사람이 판단하라"는 입장입니다. 좋은 팀은 둘을 모두 쓰되 역할을 명확히 구분합니다. 포매팅은 기계에 완전히 맡기고, 린팅은 경고로 받아들여 사람이 검토합니다.

포매터가 손대지 않는 것 — 한계와 예외

의견 있는 도구라고 해서 모든 것을 결정해 주지는 않습니다. gofmt가 일부러 손대지 않는 영역이 있다는 점은 앞에서 언급했는데, 그중 대표적인 것이 줄 길이입니다. prettier나 black은 "한 줄 최대 몇 자"라는 규칙으로 긴 줄을 자동으로 접지만, gofmt는 줄 길이에 대해 아무 규칙도 강제하지 않습니다. 줄을 어디서 끊을지는 사람의 판단에 맡기는 것입니다.

이는 의도된 설계입니다. 줄을 끊는 위치는 종종 코드의 의미 구조와 맞물려 있어서, 기계가 일률적으로 자르면 오히려 가독성을 해칠 수 있기 때문입니다. 예를 들어 다음과 같은 코드에서 어디서 줄을 바꿀지는 작성자가 의미를 보고 결정하는 편이 낫습니다.

// 사람이 의미를 보고 줄을 나눈 형태
result := computeScore(
	player,
	difficulty,
	bonusMultiplier,
)

또 한 가지, 포매터의 결정을 일부러 거스르고 싶을 때가 있습니다. 표 형태로 정렬한 데이터나 ASCII 아트 주석처럼, 자동 정렬이 오히려 망가뜨리는 경우입니다. 많은 포매터는 이를 위한 "여기는 건드리지 마라"는 지시어를 제공합니다.

// gofmt는 인접한 줄의 주석을 정렬하지만,
// 다음처럼 의도적으로 정렬을 유지하고 싶을 때가 있다
var weekdays = []string{
	"Mon", // 월요일
	"Tue", // 화요일
	"Wed", // 수요일
}

흥미로운 것은 gofmt 철학의 일관성입니다. prettier는 // prettier-ignore 같은 명시적 탈출구를 제공하지만, gofmt는 그런 일반적인 "포맷 무시" 지시어를 두지 않습니다. 탈출구를 열어 주는 순간 "어디까지 무시할 것인가"라는 새로운 논쟁이 생기기 때문입니다. 대신 gofmt는 정렬을 깨고 싶으면 빈 줄을 하나 넣는 식의, 코드 구조 자체로 표현하는 방법만 남겨 둡니다. 이 작은 차이에서도 "선택지를 최소화한다"는 철학이 일관되게 드러납니다.

결국 포매터의 한계를 이해하는 것은 포매터를 잘 쓰는 일의 일부입니다. 포매터는 만능이 아니며, 의미가 개입하는 영역은 여전히 사람의 몫으로 남습니다. 좋은 도구는 자신이 무엇을 하지 않는지를 분명히 함으로써, 사람이 어디에 집중해야 하는지를 알려 줍니다.

팀 합의와 CI 강제 — 도구를 규칙으로 만들기

의견 있는 도구의 진짜 가치는 CI에서 강제될 때 드러납니다. 아무리 좋은 포매터도 누군가 안 쓰면 코드베이스의 일관성은 깨집니다. 그래서 많은 팀이 포매팅을 CI 검사로 못 박습니다.

# CI에서 포매팅 위반을 검사하는 전형적인 패턴
# (변경이 필요한 파일이 있으면 목록을 출력하고 실패시킴)
gofumpt -l . | tee /tmp/fmt.txt
test ! -s /tmp/fmt.txt

실제 CI 파이프라인에서는 이 검사를 워크플로 한 단계로 넣어 둡니다. 예를 들어 GitHub Actions에서는 다음과 같은 형태가 됩니다.

name: lint
on: [push, pull_request]
jobs:
  format:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: stable
      - name: Install gofumpt
        run: go install mvdan.cc/gofumpt@latest
      - name: Check formatting
        run: test -z "$(gofumpt -l .)"

마지막 단계가 핵심입니다. gofumpt -l .은 포맷이 어긋난 파일의 목록을 출력하는데, 출력이 비어 있지 않으면 검사가 실패합니다. 즉 "포맷되지 않은 파일이 하나라도 있으면 머지를 막는다"는 규칙이 코드로 박히는 것입니다.

이 작은 검사 하나가 코드 리뷰의 풍경을 바꿉니다. 더 이상 리뷰에서 "여기 공백 하나 빠졌네요" 같은 지적이 오갈 필요가 없습니다. 그런 것은 기계가 알아서 거르기 때문입니다. 리뷰어는 진짜 중요한 것, 즉 설계와 로직에 집중할 수 있습니다.

2026년에는 여기에 한 가지 흐름이 더해졌습니다. AI 코딩 에이전트가 보편화되면서, 에이전트가 생성한 코드를 포매터와 린터로 자동 정리하는 파이프라인이 표준이 되었습니다. AI가 만든 코드라도 gofumpt를 거치면 사람이 쓴 코드와 구분되지 않습니다. 의견 있는 도구는 이렇게 인간과 기계의 결과물을 같은 형태로 수렴시키는 평준화 장치로도 기능합니다.

의견 있는 도구의 장단점

이제 의견 있는 도구의 빛과 그림자를 정리해 보겠습니다.

장점

  • 논쟁 종식: 스타일에 쏟던 에너지를 실제 문제에 쓸 수 있습니다.
  • 일관성: 코드베이스 전체가 동일한 모양이라 읽기 쉽습니다.
  • 온보딩 단순화: 신규 멤버가 스타일 가이드를 외울 필요가 없습니다. 도구가 알아서 맞춰줍니다.
  • 리뷰 집중: 표면적 지적이 사라져 본질에 집중하게 됩니다.

단점과 비판

  • 유연성 상실: 특정 상황에서 더 읽기 좋은 형태가 있어도 도구가 강제하는 형태를 따라야 합니다.
  • 표준의 권위 문제: gofumpt처럼 비공식 도구를 채택하면, "왜 공식 gofmt가 아니라 이걸 쓰느냐"는 또 다른 논쟁이 생길 수 있습니다.
  • 의견의 주입: 도구를 만든 사람의 취향이 모두에게 강제됩니다. 이것이 "코드 스타일의 정치학"이라 불리는 이유입니다. 누군가의 의견이 도구라는 형태로 권력을 갖게 되는 것입니다.

여기서 중요한 통찰이 하나 있습니다. 의견 있는 도구가 좋은 이유는 그 의견이 "옳아서"가 아니라, 의견이 "하나로 고정되어서"입니다. 즉 도구의 가치는 내용의 정당성이 아니라 결정의 종결성에 있습니다. 이 점을 이해하면, 포매터를 두고 "이 규칙이 맞냐 틀리냐"를 따지는 일이 얼마나 본질을 비껴가는지 알 수 있습니다.

실전 사례 — 대규모 코드베이스에 포매터를 처음 도입하기

이론은 깔끔하지만 현실은 다릅니다. 수년간 포매터 없이 자라온 거대한 코드베이스에 어느 날 gofumpt를 도입하기로 했다고 해봅시다. 가장 큰 고민은 "그동안 쌓인 수십만 줄을 어떻게 한 번에 정리할 것인가"입니다.

가장 단순한 방법은 전체를 한 번에 포맷해서 하나의 거대한 커밋으로 만드는 것입니다.

# 저장소 전체를 한 번에 포맷하는 거대한 커밋
gofumpt -w ./...
git add -A
git commit -m "style: apply gofumpt across the entire codebase"

이 방법은 깔끔해 보이지만 한 가지 심각한 부작용이 있습니다. 바로 git blame 오염입니다. 이 거대한 포맷 커밋이 거의 모든 파일의 거의 모든 줄을 건드리기 때문에, 이후 git blame을 돌리면 수많은 줄의 "마지막 변경자"가 실제 작성자가 아니라 이 포맷 커밋으로 표시됩니다. 누가 왜 이 코드를 작성했는지 추적하는 일이 매우 어려워지는 것입니다.

다행히 Git에는 이 문제를 위한 해결책이 있습니다. 특정 커밋들을 blame 계산에서 무시하도록 지정하는 기능입니다. 저장소 루트에 .git-blame-ignore-revs 파일을 만들고, 무시할 포맷 커밋의 해시를 적어 둡니다.

# 포맷 커밋의 해시를 무시 목록에 추가
echo "a1b2c3d4e5f6 # style: apply gofumpt across the entire codebase" >> .git-blame-ignore-revs

# Git이 이 파일을 항상 참조하도록 설정
git config blame.ignoreRevsFile .git-blame-ignore-revs

이렇게 설정하면 git blame은 포맷 커밋을 건너뛰고 그 이전의 실제 작성자를 보여 줍니다. GitHub과 GitLab 같은 호스팅 서비스도 이 파일을 자동으로 인식해 웹 UI의 blame 화면에 반영합니다. 즉 "거대한 포맷 커밋"의 가장 큰 단점이 사라지는 것입니다.

마이그레이션 전략을 정리하면 다음과 같습니다.

  1. 한 번에, 별도 커밋으로: 포맷 변경은 로직 변경과 절대 섞지 않습니다. 순수하게 포맷만 바꾸는 단일 커밋을 만듭니다.
  2. 무시 목록에 등록: 그 커밋 해시를 .git-blame-ignore-revs에 추가해 blame 오염을 막습니다.
  3. 진행 중인 브랜치 정리: 거대한 포맷 커밋은 진행 중이던 모든 브랜치에 충돌을 일으킵니다. 미리 팀에 공지하고, 가능하면 작업이 적은 시점을 골라 진행합니다.
  4. 즉시 CI 강제 시작: 한 번 정리한 직후부터 CI 검사를 켜서, 다시 어질러지지 않도록 막습니다.

이 사례가 보여 주는 것은, 포매터 도입이 단순한 명령어 한 줄이 아니라 팀의 협업 흐름과 도구를 함께 고려해야 하는 작은 프로젝트라는 점입니다. 도구의 철학이 아무리 우아해도, 도입의 현실은 이렇게 디테일에 좌우됩니다.

실무 적용 가이드

마지막으로 실무에서 의견 있는 도구를 도입할 때의 권고를 정리합니다.

  1. 하나를 정하고 끝까지 간다: gofmt든 gofumpt든, 팀에서 하나를 정했으면 더는 논쟁하지 않습니다. 도구의 목적 자체가 논쟁 종식임을 기억하세요.
  2. CI에서 강제한다: 사람의 자율에 맡기면 일관성은 반드시 무너집니다. 검사를 자동화하세요.
  3. 에디터에 저장 시 포맷을 건다: 개발자가 의식하지 않아도 항상 포매팅된 코드를 작성하게 됩니다.
  4. 린트와 분리한다: 포매팅은 자동 수정, 린팅은 사람 검토로 역할을 나눕니다.
  5. AI 생성 코드도 같은 파이프라인을 통과시킨다: 출처가 무엇이든 코드베이스의 형태는 하나여야 합니다.

마치며

코드 포매팅 도구의 역사는 사소해 보이는 문제를 어떻게 우아하게 종결할 것인가에 대한 이야기입니다. Go는 gofmt로 "선택지를 없애 논쟁을 끝낸다"는 강력한 모델을 제시했고, gofumpt는 그 위에 한 걸음 더 나아갔습니다. black, prettier, rustfmt는 각자의 방식으로 이 철학을 변주했습니다.

의견 있는 도구가 우리에게 가르쳐 주는 것은, 모든 결정이 토론을 거쳐야 하는 것은 아니라는 사실입니다. 어떤 결정은 그저 누군가 내려주기만 하면 됩니다. 그 결정이 완벽하지 않아도, 모두가 따른다는 사실 자체가 완벽함보다 더 큰 가치를 만들어 냅니다. 코드 스타일이라는 작은 영역에서 시작된 이 통찰은, AI가 코드를 대량으로 생성하는 시대에 오히려 더 중요해지고 있습니다.

조금 더 멀리서 보면, gofmt가 던지는 질문은 코드 스타일을 훌쩍 넘어섭니다. "어디까지를 합의로 정하고, 어디부터를 도구에 맡길 것인가"는 사실 모든 협업의 근본 질문입니다. 의견 있는 도구는 이 질문에 대해 한 가지 분명한 답을 내놓습니다. 정답이 없는 영역에서는, 결정을 내리는 행위 자체가 결정의 내용보다 중요하다는 것입니다. 우리가 매일 마주하는 수많은 사소한 선택들 — 파일 구조, 명명 규칙, 커밋 메시지 형식 — 중 상당수가 이 교훈의 적용 대상입니다. gofmt는 그 작은 영역에서 먼저 답을 보여 주었을 뿐, 그 정신은 훨씬 넓은 곳까지 닿아 있습니다. 좋은 도구는 우리를 자유롭게 하지 않습니다. 오히려 사소한 자유를 빼앗는 대신 더 큰 자유를 돌려줍니다. 무엇을 고민하지 않아도 되는지를 정해 주는 것, 그것이 의견 있는 도구가 주는 가장 값진 선물입니다.

참고 자료