Skip to content
Published on

의견 있는 도구 — 코드 포매터와 린터의 철학

Authors

들어가며 — 다시 뜨거워진 포매터 논쟁

2026년 상반기, GeekNews와 Hacker News에서 코드 포매터 이야기가 다시 화제가 되었습니다. gofumpt처럼 gofmt보다 더 엄격한 "의견 있는(opinionated)" 도구가 주목받고, Rust로 작성된 초고속 파이썬 도구 ruff가 사실상 표준으로 자리 잡으면서, "도구가 우리 대신 스타일을 정해주는 것이 좋은가"라는 오래된 논쟁이 다시 불붙었습니다.

이 논쟁은 사실 새롭지 않습니다. 탭이냐 스페이스냐, 중괄호를 다음 줄에 놓느냐, 세미콜론을 붙이느냐. 이런 질문은 소프트웨어 역사만큼 오래되었고, 팀마다 격렬한 종교 전쟁을 낳았습니다. 그런데 지난 10년간 흐름이 바뀌었습니다. "포맷은 도구가 결정하고, 사람은 논쟁하지 않는다"는 철학이 승리했습니다. 이 글은 그 승리가 왜 일어났는지, 그리고 각 언어 생태계의 도구들이 어떤 철학적 선택을 했는지를 살펴봅니다.

핵심 주장은 이렇습니다. 좋은 포매터의 가치는 그것이 만드는 스타일이 "옳기" 때문이 아니라, 스타일에 관한 논쟁 자체를 없애기 때문입니다. 의견 있는 도구는 선택지를 뺏는 대신 인지적 자유를 돌려줍니다.

포매터는 무엇을 해결하는가

포매터의 가치를 이해하려면 그것이 없는 세계를 상상하면 됩니다. 팀원 다섯 명이 각자 다른 들여쓰기, 다른 줄바꿈 규칙, 다른 괄호 스타일로 코드를 씁니다. Pull Request의 diff는 실제 변경과 스타일 변경이 뒤섞여 리뷰가 어렵고, 코드 리뷰 시간의 상당 부분이 "여기 공백 하나 더 넣어주세요" 같은 무의미한 지적에 낭비됩니다.

포매터는 이 문제를 근본적으로 해결합니다. 저장하는 순간, 혹은 커밋하는 순간, 모든 코드가 하나의 정해진 형식으로 변환됩니다. 결과적으로 얻는 것은 세 가지입니다.

  • 논쟁의 종식: 스타일은 더 이상 토론 대상이 아닙니다. 도구가 정합니다.
  • 일관성: 저장소 전체가 한 사람이 쓴 것처럼 보입니다. 코드베이스를 읽는 인지 부하가 줄어듭니다.
  • 깨끗한 diff: 포맷이 항상 정규화되어 있으므로 diff에는 의미 있는 변경만 남습니다.

세 번째가 특히 중요합니다. 스타일 변경이 diff를 오염시키지 않으면, 리뷰어는 실제 로직 변경에만 집중할 수 있습니다.

의견 있음의 스펙트럼 — 엄격함 대 유연함

모든 포매터가 같은 철학을 가진 것은 아닙니다. 도구들은 "얼마나 사용자에게 선택권을 줄 것인가"라는 축에서 다르게 자리 잡습니다.

한쪽 끝에는 gofmt가 있습니다. 설정이 거의 없습니다. Go 팀의 결정은 명확했습니다. "포맷에 대한 논쟁은 시간 낭비다. 하나의 정답을 정하고 모두가 따르라." 옵션이 없다는 것이 곧 기능입니다. 아무도 .gofmt 설정 파일을 두고 싸우지 않습니다.

반대쪽 끝에는 전통적인 도구들이 있습니다. 수백 개의 옵션을 제공해 팀이 원하는 대로 스타일을 조립할 수 있게 합니다. 유연하지만 대가가 있습니다. 옵션이 많으면 그 옵션을 두고 다시 논쟁이 벌어집니다.

이 스펙트럼을 표로 정리하면 다음과 같습니다.

도구언어철학설정 가능성
gofmtGo최소 옵션, 하나의 정답거의 없음
gofumptGogofmt보다 더 엄격없음 (더 강함)
prettierJS/TS 등의견 있으나 일부 옵션소수 옵션
blackPython타협 없는 포매팅극소수
ruffPython초고속, 포맷+린트 통합폭넓게 조정 가능

흥미로운 점은 gofumpt입니다. 이것은 gofmt가 "충분히 엄격하지 않다"고 본 사람들이 만든 도구입니다. gofmt가 허용하는 미세한 자유마저 없애 더 일관된 결과를 냅니다. "의견 있음"의 방향으로 한 발 더 나간 것입니다.

각 도구의 철학 들여다보기

gofmt와 gofumpt — Go의 무관용

Go는 처음부터 포맷을 언어 문화의 일부로 만들었습니다. go fmt 명령이 표준 툴체인에 들어 있고, 사실상 모든 Go 코드가 동일하게 생겼습니다. 다음 코드를 gofmt에 넣으면,

package main
import "fmt"
func main(){
x:=1
fmt.Println(x)
}

항상 이렇게 정규화됩니다.

package main

import "fmt"

func main() {
	x := 1
	fmt.Println(x)
}

들여쓰기는 탭, import는 정렬, 중괄호 위치까지 고정됩니다. gofumpt는 여기서 더 나아가 불필요한 빈 줄 제거, 특정 관용구 강제 같은 추가 규칙을 얹습니다.

prettier — 웹 생태계의 통일자

prettier는 자바스크립트, 타입스크립트, CSS, 마크다운 등 웹 생태계 전반을 통일했습니다. 소수의 옵션(줄 길이, 세미콜론, 따옴표 종류)만 제공하고 나머지는 도구가 결정합니다. 이 "의견 있으나 완전히 독재적이지는 않은" 균형이 폭넓은 채택을 이끌었습니다.

black — 파이썬의 타협 없음

black의 슬로건은 유명합니다. "어떤 색이든 검정색이면 된다." 사용자가 조정할 수 있는 것이 거의 없습니다. 줄 길이 정도만 바꿀 수 있고 나머지는 모두 고정입니다. 이 극단적 단순함이 파이썬 커뮤니티에서 큰 호응을 얻었습니다.

ruff — 속도가 바꾼 판도

ruff는 Rust로 작성되어 기존 파이썬 도구보다 수십 배 빠릅니다. 게다가 린팅과 포매팅을 하나의 도구로 통합했습니다. 속도는 단순한 편의가 아닙니다. 저장할 때마다, 커밋할 때마다 즉각 실행되므로 개발자가 도구의 존재를 거의 느끼지 못합니다. 마찰이 사라지면 채택이 쉬워집니다.

포매터와 린터의 차이

여기서 자주 혼동되는 개념을 정리해야 합니다. 포매터와 린터는 다릅니다.

  • 포매터는 코드의 형태를 바꿉니다. 들여쓰기, 줄바꿈, 공백. 코드의 의미는 절대 바꾸지 않습니다.
  • 린터는 코드의 내용에 대해 경고합니다. 사용하지 않는 변수, 잠재적 버그, 안티패턴, 컨벤션 위반.

이 차이를 표로 보면 명확합니다.

구분포매터린터
대상코드의 형태코드의 내용과 품질
예시들여쓰기, 줄바꿈미사용 변수, 널 위험
자동 수정항상 (형태만)일부 (안전한 것만)
의미 변경없음가능 (수정 시 주의)
실패 시재포맷경고 또는 에러

경계가 흐려지는 경우도 있습니다. ruff처럼 린터와 포매터를 통합한 도구가 늘고 있습니다. 하지만 개념적으로는 여전히 구분하는 것이 안전합니다. 포매터의 변경은 항상 안전하지만, 린터의 자동 수정은 때로 의미를 바꿀 수 있어 검토가 필요합니다.

CI 강제와 pre-commit — 자동화의 위치

포매터가 진짜 힘을 발휘하려면 강제되어야 합니다. 개발자가 "잊어버려서" 포맷하지 않으면 일관성이 깨집니다. 강제하는 위치는 크게 세 곳입니다.

  개발자 에디터          로컬 커밋           CI 파이프라인
+---------------+     +---------------+     +---------------+
| 저장 시 포맷  | --> | pre-commit    | --> | fmt --check   |
| (즉각 피드백) |     | (커밋 차단)   |     | (머지 차단)   |
+---------------+     +---------------+     +---------------+
   가장 부드러움         중간 관문             최후의 보루

세 곳을 겹쳐서 방어하는 것이 이상적입니다. 에디터 저장 시 포맷은 마찰이 가장 적고, pre-commit 훅은 잘못된 포맷이 커밋되는 것을 막고, CI는 어떤 경우에도 통과하지 못하게 하는 최후의 보루입니다.

CI에서는 보통 포맷을 "적용"하지 않고 "검사"만 합니다. 다음처럼요.

# 포맷이 어긋나면 0이 아닌 종료 코드로 실패
gofmt -l .
# 또는 파이썬
ruff format --check .

--check 모드는 파일을 고치지 않고 "고쳐야 할 파일이 있는가"만 판정합니다. 어긋난 파일이 있으면 CI가 실패하고 개발자는 로컬에서 포맷 후 다시 푸시합니다. CI가 직접 코드를 고쳐서 커밋하는 방식도 있지만, 이는 예상치 못한 커밋을 만들어 논란이 될 수 있어 신중해야 합니다.

pre-commit 훅 설정 예시는 다음과 같습니다.

repos:
  - repo: local
    hooks:
      - id: format-check
        name: format
        entry: ruff format
        language: system
        types: [python]

팀 도입 전략

새 포매터를 기존 코드베이스에 도입하는 것은 생각보다 까다롭습니다. 가장 큰 걸림돌은 "대재포맷(the big reformat)"입니다. 도구를 처음 적용하면 저장소의 거의 모든 파일이 바뀌고, 이 거대한 커밋이 git blame 히스토리를 오염시킵니다.

이를 완화하는 실무 기법이 있습니다.

  • 단일 재포맷 커밋: 포맷만 바꾸는 커밋을 하나 만들고, 다른 변경과 절대 섞지 않습니다.
  • blame 무시 설정: git이 그 커밋을 blame에서 건너뛰도록 무시 파일에 등록합니다.
  • 점진 도입 자제: 파일별로 조금씩 포맷하면 diff가 계속 지저분해집니다. 한 번에 전부 하는 것이 낫습니다.

도입 순서를 정리하면 다음과 같습니다.

  1. 팀이 도구와 최소 설정에 합의합니다.
  2. 단일 커밋으로 전체 재포맷을 수행합니다.
  3. blame 무시 파일에 그 커밋을 등록합니다.
  4. pre-commit과 CI 검사를 활성화합니다.
  5. 에디터 저장 시 포맷을 팀에 안내합니다.

포매터와 코드 리뷰의 관계

포매터가 코드 리뷰에 미치는 영향은 생각보다 큽니다. 앞서 언급했듯, 포맷이 자동화되면 리뷰어는 스타일 지적에서 해방됩니다. 이것이 리뷰의 질을 근본적으로 바꿉니다.

포매터가 없는 팀의 리뷰를 상상해 봅시다. Pull Request마다 "여기 들여쓰기가 다르네요", "이 괄호는 다음 줄로", "공백 하나 빼주세요" 같은 코멘트가 쌓입니다. 이런 지적은 대개 옳지만 아무런 가치를 만들지 않습니다. 리뷰어의 시간과 작성자의 인내심을 소모할 뿐입니다. 더 나쁜 것은 이런 사소한 지적에 파묻혀 진짜 중요한 문제, 즉 로직 결함이나 설계 문제가 묻힌다는 것입니다.

포매터가 이 층을 완전히 제거하면 리뷰는 본질에 집중합니다.

항목포매터 없을 때포매터 있을 때
스타일 코멘트많음없음
diff 노이즈작음
리뷰 초점분산됨로직/설계
감정 소모작음

특히 "감정 소모" 항목이 중요합니다. 스타일 지적은 종종 개인적으로 받아들여집니다. 작성자는 "내 취향이 부정당했다"고 느끼고, 리뷰어는 "잔소리하는 사람"이 됩니다. 도구가 이 갈등을 대신 떠맡으면, 사람 사이의 리뷰는 훨씬 건설적이고 협력적이 됩니다. 아무도 gofmt에게 화내지 않습니다.

이것이 포매터의 숨은 가치입니다. 포매터는 단지 코드를 예쁘게 만드는 것을 넘어, 팀의 인간관계를 개선합니다. 논쟁의 씨앗을 제거함으로써 사람들이 진짜 중요한 협업에 에너지를 쓰게 합니다.

포맷 전쟁의 역사 — 우리는 어떻게 여기까지 왔나

포매터가 승리하기까지의 역사를 잠깐 돌아보면 현재를 더 잘 이해할 수 있습니다. 소프트웨어 초창기부터 개발자들은 코드 스타일을 두고 다퉜습니다. 이 다툼은 사소해 보이지만 놀라울 만큼 격렬했습니다.

가장 유명한 논쟁은 들여쓰기입니다. 탭이냐 스페이스냐, 스페이스라면 2칸이냐 4칸이냐. 이 질문은 수십 년간 개발자들을 갈라놓았고, 밈과 농담의 단골 소재가 되었습니다. 중괄호를 여는 위치, 줄의 최대 길이, 세미콜론의 유무도 마찬가지였습니다.

이런 논쟁의 문제는 그것이 아무런 객관적 정답이 없다는 데 있습니다. 대부분의 스타일 선택은 순전히 취향이고, 어느 쪽도 명확히 우월하지 않습니다. 그런데 정답이 없기 때문에 오히려 논쟁이 끝나지 않았습니다. 각자 자기 취향을 정당화하려 했고, 팀은 합의에 이르지 못한 채 에너지를 소모했습니다.

전환점은 두 가지 깨달음이었습니다. 첫째, 스타일의 일관성이 스타일의 내용보다 훨씬 중요하다는 것. 어떤 스타일이든 팀 전체가 일관되게 쓰기만 하면 대부분의 이점을 얻습니다. 둘째, 그 일관성을 사람이 손으로 지키는 것은 비현실적이라는 것. 사람은 실수하고 잊어버립니다. 도구만이 완벽한 일관성을 강제할 수 있습니다.

이 두 깨달음이 만나 의견 있는 포매터가 탄생했습니다. gofmt가 "논쟁하지 말고 하나로 정하자"고 선언했을 때, 그것은 단순한 도구가 아니라 문화적 선언이었습니다. 그리고 그 선언은 지난 10년간 소프트웨어 세계 전체로 퍼졌습니다. 오늘 우리가 스타일 논쟁에서 자유로운 것은 이 역사의 결과입니다.

결정론과 멱등성 — 좋은 포매터의 조건

좋은 포매터가 갖춰야 할 기술적 성질이 두 가지 있습니다. 결정론(determinism)과 멱등성(idempotency)입니다.

결정론은 같은 입력에 항상 같은 출력을 낸다는 뜻입니다. 어떤 개발자의 기계에서 돌리든, 언제 돌리든 결과가 동일해야 합니다. 만약 포매터가 환경이나 시각에 따라 다른 결과를 낸다면, 그것은 재앙입니다. 팀원마다 다른 포맷이 나오면 애초에 포매터를 쓰는 이유가 사라집니다.

멱등성은 이미 포맷된 코드를 다시 포맷해도 바뀌지 않는다는 뜻입니다. 한 번 포맷한 결과를 또 포맷하면 그대로여야 합니다. 다음을 생각해 봅시다.

  원본 코드 --포맷--> 결과 A --포맷--> 결과 A (동일)
                                  |
                          멱등성이 깨지면
                                  |
  원본 코드 --포맷--> 결과 A --포맷--> 결과 B (다름!)

멱등성이 깨진 포매터는 CI 검사에서 끝없는 문제를 일으킵니다. 개발자가 로컬에서 포맷하고 푸시했는데, CI의 포매터 버전이 미묘하게 달라 다시 포맷이 필요하다고 나오면, 무한 루프에 빠집니다. 그래서 팀 전체가 포매터의 정확히 같은 버전을 고정해서 쓰는 것이 중요합니다.

이 두 성질 때문에 포매터 버전 관리는 생각보다 중요한 문제입니다. 많은 팀이 포매터 버전을 의존성 파일에 명시적으로 고정하고, CI와 로컬이 정확히 같은 버전을 쓰도록 강제합니다.

대규모 코드베이스에서의 성능

포매터가 느리면 개발 경험이 나빠집니다. 저장할 때마다 몇 초씩 멈추면 개발자는 포맷을 끄고 싶어집니다. 여기서 ruff 같은 도구가 왜 판도를 바꿨는지가 드러납니다.

전통적인 파이썬 도구들은 파이썬 자체로 작성되어 느렸습니다. 반면 ruff는 Rust로 작성되어 수십 배 빠릅니다. 이 속도 차이는 단순한 편의를 넘어섭니다. 저장할 때마다, 커밋할 때마다 실행되는 도구가 즉각적이면 개발자는 그 존재를 거의 느끼지 못하고, 마찰이 사라지면 저항 없이 채택됩니다.

성능이 도입 성패를 가르는 상황을 정리하면 다음과 같습니다.

상황느린 포매터빠른 포매터
저장 시 자동 포맷지연 체감, 끄고 싶어짐즉각, 무의식적
pre-commit 훅커밋마다 대기순간
대규모 CI 검사파이프라인 병목무시 가능
에디터 통합타이핑 방해매끄러움

이것이 "속도도 기능이다"라는 말이 나오는 이유입니다. 아무리 좋은 규칙을 가진 도구라도 느리면 결국 꺼지고, 꺼진 도구는 아무 가치가 없습니다.

포맷 무시 주석 — 탈출구의 딜레마

거의 모든 포매터는 특정 구역을 포맷에서 제외하는 무시 주석을 제공합니다. 정렬된 데이터 테이블이나 의도적으로 배치한 코드처럼, 자동 포맷이 오히려 가독성을 해치는 경우를 위한 탈출구입니다.

하지만 이 탈출구는 딜레마를 낳습니다. 무시 주석이 없으면 포매터가 가끔 나쁜 결과를 강제하고, 무시 주석이 흔해지면 포매터의 일관성이라는 핵심 가치가 무너집니다. 저장소 곳곳에 무시 주석이 흩어져 있으면, 결국 "어디는 포맷되고 어디는 안 되는" 예측 불가능한 상태가 됩니다.

실무 원칙은 이렇습니다. 무시 주석은 정말 드물게, 명확한 이유가 있을 때만 씁니다. 그리고 왜 무시하는지 짧은 설명을 함께 답니다. 무시 주석이 늘어난다면 그것은 포매터가 나쁜 것이 아니라 코드 구조에 문제가 있다는 신호일 수 있습니다.

언어 생태계별 문화 차이

흥미롭게도 포매터에 대한 태도는 언어 생태계마다 다릅니다. 이 차이는 각 언어의 설계 철학과 커뮤니티 문화를 반영합니다.

Go 커뮤니티는 처음부터 "하나의 포맷"을 문화로 받아들였습니다. gofmt에 옵션이 없다는 것에 아무도 불평하지 않고, 오히려 그것을 자랑스러워합니다. 반면 자바스크립트 생태계는 오랫동안 스타일 다양성이 컸고, prettier가 등장하기 전까지 팀마다 제각각이었습니다. 파이썬은 그 중간 어디쯤이었다가 black과 ruff로 빠르게 표준화되었습니다.

이 문화 차이는 실무에 시사점을 줍니다. 새 언어를 도입할 때 그 생태계의 표준 포매터가 무엇인지, 커뮤니티가 어떤 태도를 갖는지 파악하면 불필요한 논쟁을 피할 수 있습니다. 표준이 확고한 생태계에서는 그것을 따르는 것이 최선이고, 표준이 약한 생태계에서는 팀이 일찍 하나를 정하는 것이 중요합니다.

자동화의 함정과 비판적 시각

포매터는 대체로 축복이지만 함정도 있습니다.

과도한 엄격함의 반작용. 도구가 너무 엄격하면 가끔 가독성을 해치는 결과를 강제합니다. 예를 들어 정렬된 테이블처럼 의도적으로 맞춘 코드를 포매터가 흩뜨리는 경우가 있습니다. 대부분의 도구는 이런 경우를 위해 포맷 무시 주석을 제공하지만, 남용하면 도구의 가치가 사라집니다.

린터 규칙의 무한 팽창. 포매터는 논쟁을 없애지만 린터는 오히려 논쟁을 늘릴 수 있습니다. "이 규칙을 켤까 말까"를 두고 팀이 끝없이 토론하게 됩니다. 규칙 세트가 비대해지면 개발자는 경고에 무뎌지고, 진짜 중요한 경고까지 무시합니다.

도구가 사고를 대체한다는 착각. 포맷이 예쁘다고 코드가 좋은 것은 아닙니다. 포매터는 형태를 정리할 뿐 설계를 개선하지 않습니다. 도구에 의존해 "통과했으니 괜찮다"고 생각하면 진짜 문제를 놓칩니다.

설정 파일 관리의 부담. 유연한 도구일수록 설정 파일이 커지고, 그 설정 자체가 유지보수 대상이 됩니다. 이것이 gofmt 같은 무설정 도구가 매력적인 이유입니다. 관리할 설정이 없으면 설정을 두고 싸울 일도 없습니다.

다국어 저장소에서의 포매팅

현대의 많은 저장소는 한 언어로만 되어 있지 않습니다. 백엔드는 Go, 프론트엔드는 타입스크립트, 인프라 스크립트는 파이썬, 설정은 YAML과 JSON. 이런 다국어 저장소에서는 각 언어마다 다른 포매터를 조율해야 하는 문제가 생깁니다.

각 언어의 표준 포매터가 다르므로, 저장소 전체를 하나의 도구로 포맷할 수는 없습니다. 대신 언어별 포매터를 하나의 통합된 진입점 뒤에 모으는 방식을 씁니다.

  저장소 포맷 명령 (하나)
        |
        +--> Go 파일   --> gofmt
        +--> TS 파일   --> prettier
        +--> Python    --> ruff
        +--> YAML/JSON --> prettier

이 구조에서 중요한 것은 개발자가 언어마다 다른 명령을 외울 필요가 없다는 것입니다. pre-commit 프레임워크 같은 도구가 파일 종류를 보고 알맞은 포매터를 자동으로 부릅니다. 개발자는 그냥 커밋하면 되고, 뒤에서 각 파일이 올바른 도구로 포맷됩니다.

다국어 저장소의 함정은 도구 버전 관리가 복잡해진다는 것입니다. 여러 언어의 포매터를 각각 고정하고 CI와 로컬에서 일치시켜야 하므로 관리 지점이 늘어납니다. 이를 위해 도구 버전을 한곳에서 선언하는 매니페스트를 두고, 모든 환경이 그것을 참조하게 하는 것이 좋습니다.

린터 규칙의 세 층위

린터를 잘 쓰려면 규칙을 무작정 다 켜는 것이 아니라 층위로 나눠 생각해야 합니다. 실무에서 린터 규칙은 대략 세 층위로 구분됩니다.

  • 오류 층: 거의 확실히 버그인 것들. 미사용 변수, 도달 불가능한 코드, 잘못된 비교. 이것들은 강제하는 것이 옳습니다.
  • 경고 층: 대개 나쁘지만 예외가 있는 것들. 복잡도가 높은 함수, 긴 매개변수 목록. 경고로 두되 강제하지는 않습니다.
  • 취향 층: 순전히 선호의 문제인 것들. 이것들은 대개 켜지 않는 것이 낫습니다. 켜면 논쟁만 생깁니다.

이 층위를 표로 정리하면 다음과 같습니다.

층위예시처리
오류미사용 변수, 널 위험CI에서 강제 실패
경고높은 복잡도, 긴 함수표시하되 통과
취향특정 문법 선호대개 비활성화

핵심은 오류 층만 강제하고 나머지는 신중하게 다루는 것입니다. 모든 규칙을 오류로 강제하면 개발자는 사소한 것에 발목 잡히고, 곧 린터 자체를 우회하려 듭니다. 규칙 세트는 작고 명확할수록 존중받습니다.

레거시 코드베이스에 도입하기

지금까지의 논의는 새 프로젝트를 전제했습니다. 하지만 현실의 많은 팀은 이미 수년간 스타일이 뒤섞인 레거시 코드베이스를 다룹니다. 여기에 포매터를 도입하는 것은 더 어려운 문제입니다.

가장 큰 두려움은 대재포맷이 무언가를 망가뜨릴 것이라는 우려입니다. 포매터는 형태만 바꾸므로 원칙적으로 동작을 바꾸지 않지만, 극히 드물게 예외가 있습니다. 예를 들어 문자열 안의 공백이나 특정 매크로 처리에서 미묘한 차이가 날 수 있습니다. 그래서 도입 후 전체 테스트를 돌려 회귀가 없는지 확인하는 것이 필수입니다.

레거시 도입의 안전한 순서는 다음과 같습니다.

  1. 포매터를 설치하되 아직 아무 파일도 바꾸지 않습니다.
  2. 검사 모드로 돌려 얼마나 많은 파일이 바뀔지 규모를 파악합니다.
  3. 테스트 스위트가 충분한지 확인합니다. 부족하면 먼저 보강합니다.
  4. 단일 커밋으로 전체 재포맷을 수행합니다.
  5. 전체 테스트를 돌려 회귀가 없음을 확인합니다.
  6. blame 무시 파일에 그 커밋을 등록합니다.

여기서 테스트 스위트의 중요성이 드러납니다. 테스트가 탄탄하면 대재포맷이 무언가를 망가뜨렸을 때 즉시 잡힙니다. 테스트가 부실하면 대재포맷은 도박이 됩니다. 그래서 역설적으로, 포매터 도입의 선결 조건이 테스트 커버리지인 경우가 많습니다.

또 하나의 현실적 고려는 진행 중인 브랜치들입니다. 대재포맷 커밋은 거의 모든 파일을 건드리므로, 아직 머지되지 않은 브랜치들과 대규모 충돌을 일으킵니다. 그래서 대재포맷은 진행 중인 작업이 적은 시점(예: 릴리스 직후)에 하고, 모든 팀원에게 미리 공지해 브랜치를 정리하게 하는 것이 좋습니다.

포매터가 못 하는 것 — 명명과 구조

포매터의 힘을 과대평가하지 않는 것이 중요합니다. 포매터는 코드의 형태를 정리하지만 코드의 질을 개선하지는 않습니다. 다음 두 함수를 봅시다.

def f(x, y, z):
    a = x + y
    b = a * z
    return b

이 코드는 완벽하게 포맷되어 있습니다. 들여쓰기도, 공백도 흠잡을 데 없습니다. 하지만 함수 이름 f와 변수 이름 a, b는 아무것도 설명하지 않습니다. 포매터는 이 문제를 절대 고치지 못합니다. 이름 짓기, 함수 분해, 추상화 수준 같은 진짜 설계 문제는 사람의 판단 영역입니다.

이것이 포매터와 린터를 맹신하면 안 되는 이유입니다. 도구를 통과한 코드가 "충분히 좋다"는 착각을 심으면, 정작 중요한 명명과 구조에 대한 고민이 사라집니다. 도구는 형태의 하한선을 보장할 뿐, 설계의 상한선을 끌어올리지 못합니다. 좋은 코드는 여전히 사람이 만듭니다.

실무 권장 사항

지금까지의 내용을 실무 지침으로 압축하면 다음과 같습니다.

  • 언어에 표준 포매터가 있으면 그것을 그대로 씁니다. 바퀴를 다시 발명하지 마세요.
  • 설정은 최소로. 옵션을 늘릴수록 논쟁이 늘어납니다.
  • 포매터와 린터를 개념적으로 구분해 각자의 역할에 충실하게 합니다.
  • CI에서는 적용이 아니라 검사만 합니다.
  • 도입은 단일 재포맷 커밋으로, blame 무시와 함께.
  • 린터 규칙은 소수 정예로 유지해 경보 피로를 막습니다.
  • 도구는 형태를 정리할 뿐 설계를 대신하지 않는다는 점을 기억합니다.

에디터 통합 — 마찰을 없애는 마지막 조각

포매터가 팀에 정착하려면 개발자의 일상 도구인 에디터에 매끄럽게 통합되어야 합니다. 이상적인 경험은 개발자가 포매터의 존재를 거의 의식하지 않는 것입니다. 코드를 쓰고 저장하면 자동으로 정리됩니다. 별도의 명령을 칠 필요도, 규칙을 외울 필요도 없습니다.

에디터 통합의 핵심 방식은 저장 시 포맷(format on save)입니다. 파일을 저장하는 순간 포매터가 돌아 코드를 정규화합니다. 이렇게 하면 개발자는 스타일을 신경 쓰지 않고 로직에만 집중할 수 있습니다.

  코드 작성 (스타일 무관하게)
        |
        v
  저장 (Ctrl+S / Cmd+S)
        |
        v
  포매터 자동 실행
        |
        v
  정규화된 코드가 화면에

여기서 중요한 것은 팀 전체가 같은 에디터 설정을 공유하는 것입니다. 저장소에 에디터 설정 파일을 커밋해 두면, 새 팀원이 저장소를 열자마자 올바른 포맷 설정이 적용됩니다. 이렇게 하면 "내 에디터에서는 다르게 포맷된다"는 혼란을 막을 수 있습니다.

다만 저장 시 포맷에도 주의점이 있습니다. 매우 큰 파일에서는 저장이 느려질 수 있고, 다른 사람의 코드를 열어 한 줄만 고쳤는데 파일 전체가 재포맷되어 diff가 커지는 경우가 있습니다. 후자는 앞서 말한 단일 재포맷 커밋으로 저장소를 미리 정규화해 두면 대부분 해결됩니다.

포매터의 미래 — 통합과 지능화

포매터 도구의 진화 방향은 두 가지로 요약됩니다. 통합과 지능화입니다.

통합 흐름은 ruff가 잘 보여줍니다. 예전에는 포매터, 린터, 임포트 정렬기가 각각 별개의 도구였습니다. 각각을 설치하고 설정하고 CI에 붙여야 했습니다. ruff는 이것들을 하나로 합쳐 설정과 실행을 단순화했습니다. 도구가 적을수록 관리 부담이 줄고 일관성이 높아집니다. 이 통합 흐름은 다른 언어 생태계로도 퍼질 가능성이 큽니다.

지능화 흐름은 아직 초기입니다. 전통적인 포매터는 순수하게 규칙 기반입니다. 하지만 AI가 코드 스타일을 학습해 팀의 관행을 자동으로 파악하고, 규칙으로 명시하기 어려운 미묘한 스타일까지 맞춰주는 방향이 논의됩니다. 다만 여기에는 결정론과 멱등성이라는 포매터의 근본 요구가 걸림돌입니다. AI의 확률적 특성이 "같은 입력에 같은 출력"이라는 원칙과 충돌하기 때문입니다.

그래서 당분간 포매터의 핵심은 여전히 결정론적 규칙 기반일 것입니다. AI는 규칙을 제안하거나 예외적 케이스를 다루는 보조 역할에 머무를 가능성이 큽니다. 포매터에서만큼은 예측 가능성이 유연성보다 훨씬 중요한 가치이기 때문입니다.

마치며

의견 있는 포매터의 부상은 소프트웨어 문화의 성숙을 보여줍니다. 우리는 스타일 논쟁이 아무런 가치를 만들지 않는다는 것을 배웠고, 그 논쟁을 도구에 위임함으로써 진짜 중요한 문제에 집중할 수 있게 되었습니다. gofmt가 시작한 "옵션 없음"의 철학은 이제 여러 언어 생태계로 퍼졌습니다.

하지만 도구가 논쟁을 없애준다고 해서 판단까지 없애주는 것은 아닙니다. 포매터는 형태를 정하고, 린터는 함정을 경고하지만, 좋은 설계와 명확한 코드는 여전히 사람의 몫입니다. 도구가 잘하는 것은 도구에게 맡기고, 사람은 도구가 할 수 없는 곳에 에너지를 쓰는 것. 이것이 의견 있는 도구가 우리에게 주는 진짜 선물입니다.

스타일을 두고 싸우는 데 쓸 시간을, 이제 더 좋은 소프트웨어를 만드는 데 씁시다.

참고 자료