Go 코드에 품질 도구 적용하기

Go 프로젝트에서 기본적으로 갖추면 좋은 코드 품질 도구들(gofmt, goimports, go vet, golangci-lint)을 정리하고, Makefile로 통합하는 방법을 소개합니다.

Go는 언어 차원에서 코드 포맷팅 도구(gofmt)를 제공하고, 표준 도구 체인에 정적 분석(go vet)이 포함되어 있습니다. 이런 도구들을 프로젝트 초기에 설정해두면 코드 리뷰에서 스타일 논쟁을 줄이고, 흔한 실수를 빌드 전에 잡을 수 있습니다.

이 글에서는 Go로 개발하는 프로젝트에서 기본적으로 갖추면 좋은 도구들과 설정 방법을 정리합니다.

1. gofmt: 코드 포맷팅

Go 커뮤니티에서는 gofmt 이 표준 포맷터입니다. 탭/스페이스, 중괄호 위치 같은 스타일 논쟁 없이 모든 Go 코드가 동일한 포맷을 따르게 됩니다.

gofmt -w .

-w 플래그는 파일을 직접 수정합니다. -l 플래그를 쓰면 포맷이 맞지 않는 파일 목록만 출력합니다.

gofmt -l .

포맷이 맞지 않는 파일이 있으면 파일 경로가 출력되고, 모두 맞으면 아무것도 출력되지 않습니다. CI에서는 이 출력을 검사해서 포맷팅되지 않은 코드가 머지되는 것을 막을 수 있습니다.

2. goimports: import 정리

goimports는 `gofmt`의 기능에 더해 import 문을 자동으로 정리합니다.

  • 사용하지 않는 import를 제거

  • 필요한 import를 자동 추가

  • import를 표준 라이브러리 / 그 외 패키지 2그룹으로 정렬 (-local 플래그를 쓰면 로컬 패키지를 별도 그룹으로 분리 가능)

go install golang.org/x/tools/cmd/goimports@latest
goimports -w .

gofmt 대신 `goimports`만 실행해도 포맷팅까지 함께 처리됩니다.

3. go vet: 정적 분석

`go vet`은 Go 표준 도구 체인에 포함된 정적 분석 도구입니다. 컴파일러가 잡지 못하는 의심스러운 코드를 검출합니다.

go vet ./...

대표적으로 잡아주는 문제들은 다음과 같습니다.

  • fmt.Printf`의 포맷 문자열과 인자 불일치 (`printf)

  • 도달할 수 없는 코드 (unreachable)

  • sync.Mutex 등 잠금을 값으로 복사하는 코드 (copylocks)

  • context.WithCancel`의 cancel 함수를 호출하지 않는 경로 (`lostcancel)

별도 설치 없이 바로 사용할 수 있으므로, 모든 Go 프로젝트에서 기본으로 실행하는 것이 좋습니다.

4. golangci-lint: 통합 린터

golangci-lint는 여러 린터를 하나의 도구로 통합해서 실행해주는 린터 러너입니다. 개별 린터를 따로 설치하고 실행할 필요 없이, 설정 파일 하나로 원하는 린터를 선택해서 쓸 수 있습니다.

## 바이너리 설치 (go install은 공식적으로 권장되지 않음)
curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin
golangci-lint run ./...

4.1. 설정 파일

프로젝트 루트에 .golangci.yml 파일을 만들어 설정합니다. 아래는 golangci-lint v2 기준 설정 예시입니다.

.golangci.yml
version: "2"

linters:
  default: standard
  enable:
    - misspell

formatters:
  enable:
    - gofmt
    - goimports

v2에서는 `version: "2"`를 반드시 명시해야 합니다. `default: standard`로 설정하면 errcheck, govet, ineffassign, staticcheck, unused 5개 린터가 기본으로 활성화됩니다. 추가로 필요한 린터만 `enable`에 넣으면 됩니다.

v2의 또 다른 변경점은 gofmt, goimports 같은 포맷터가 linters`가 아닌 `formatters 섹션으로 분리된 것입니다. 기존 v1 설정 파일이 있다면 golangci-lint migrate 명령으로 자동 변환할 수 있습니다.

4.2. 기본 활성화 린터 (default: standard)

린터 설명

errcheck

에러 반환값을 무시하는 코드 검출

govet

`go vet`과 동일한 정적 분석

ineffassign

이후에 사용되지 않는 변수 할당 검출

staticcheck

Go 코드의 버그, 성능 문제, 스타일 문제를 종합적으로 검사 (gosimple, stylecheck 포함)

unused

사용되지 않는 변수, 함수, 타입 검출

이 중 `errcheck`은 특히 중요합니다. Go에서 에러를 반환값으로 처리하는 관례상, 에러를 무시하는 코드는 런타임에 예상치 못한 동작을 일으킬 수 있습니다.

5. Makefile로 통합

위 도구들을 개별로 실행하는 것은 번거로우므로, Makefile에 통합하면 편리합니다.

Makefile
.PHONY: fmt lint test check ci

fmt:
	goimports -w .

lint:
	golangci-lint run ./...

test:
	go test ./...

check: fmt lint test

ci: lint test

check`와 `ci 두 가지 타겟을 나눈 이유가 있습니다. fmt 타겟은 파일을 직접 수정하므로 로컬에서 커밋 전에 실행하기에 편리하지만, CI 환경에서는 파일을 수정하면 안 됩니다. ci 타겟은 `golangci-lint`의 gofmt, goimports 린터가 포맷 위반을 검출만 하고 파일을 수정하지 않으므로, CI에서 안전하게 쓸 수 있습니다.

  • 로컬: make check (포맷 자동 수정 + 린트 + 테스트)

  • CI: make ci (린트 + 테스트, 파일 수정 없음)

`go vet`을 별도 타겟으로 두지 않은 이유는, golangci-lint에 govet 린터가 포함되어 있어서 `make lint`에서 이미 실행되기 때문입니다.

코드 수정 후 커밋하기 전에 `make check`를 실행하는 습관을 들이면, CI에서 실패하는 상황을 줄일 수 있습니다.

6. 참고 자료

(이 글은 개인 라이센스로 구매한 Claude Code의 도움을 받아 작성되었습니다.)

Claude Code Hook으로 quota 전환 자동화하기