์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿ” Go ์–ธ์–ด ์ฝ”๋“œ ํ’ˆ์งˆ ๊ด€๋ฆฌ์™€ golangci-lint๋ฅผ ํ™œ์šฉํ•œ ์ •์  ๋ถ„์„ ์‹ค์ „ ๊ฐ€์ด๋“œ
ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ ยท Go

๐Ÿ” Go ์–ธ์–ด ์ฝ”๋“œ ํ’ˆ์งˆ ๊ด€๋ฆฌ์™€ golangci-lint๋ฅผ ํ™œ์šฉํ•œ ์ •์  ๋ถ„์„ ์‹ค์ „ ๊ฐ€์ด๋“œ

์ฝ”๋“œ ํ€„๋ฆฌํ‹ฐ ์ฑ™๊ธฐ๋Š” ๊ฑฐ ๊ท€์ฐฎ๋‹ค๊ณ ? ์ด๊ฑฐ ์ฝ๊ณ  ๋‚˜๋ฉด ์ƒ๊ฐ ๋ฐ”๋€”๊ฑธ์š” ใ…‹ใ…‹ ์ง„์งœ๋กœ์š”

์ฝ”๋“œ ์ž‘์„ฑ .go ํŒŒ์ผ ์ •์  ๋ถ„์„ golangci-lint ์˜ค๋ฅ˜ ์ˆ˜์ • lint ๊ฒฐ๊ณผ ๋ฐ˜์˜ CI/CD ํ†ตํ•ฉ ์ž๋™ํ™” ํŒŒ์ดํ”„๋ผ์ธ ๊ณ ํ’ˆ์งˆ ์ฝ”๋“œ ์™„์„ฑ ๐ŸŽ‰ Go ์ฝ”๋“œ ํ’ˆ์งˆ ๊ด€๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ golangci-lint ๊ธฐ๋ฐ˜ ์ •์  ๋ถ„์„ ์›Œํฌํ”Œ๋กœ์šฐ ๐Ÿ” Lint โ†’ ๐Ÿ›  Fix โ†’ ๐Ÿš€ Deploy ์‚ฌ์ดํด๋กœ ์ฝ”๋“œ ํ’ˆ์งˆ์„ ์ง€์†์ ์œผ๋กœ ํ–ฅ์ƒ 50๊ฐœ ์ด์ƒ์˜ linter๋ฅผ ํ•˜๋‚˜์˜ ๋„๊ตฌ๋กœ ํ†ตํ•ฉ ๊ด€๋ฆฌ
๐Ÿค” ์ž ๊น, ์ฝ”๋“œ ํ’ˆ์งˆ์ด ์™œ ์ค‘์š”ํ•œ๋ฐ์š”?

์†”์งํžˆ ๋งํ•ด์„œ, ์ฒ˜์Œ Go ๋ฐฐ์šธ ๋•Œ "๊ทธ๋ƒฅ ๋Œ์•„๊ฐ€๋ฉด ๋˜๋Š” ๊ฑฐ ์•„๋ƒ?" ํ•˜๋Š” ์ƒ๊ฐ ํ•œ ๋ฒˆ์ฏค์€ ํ•ด๋ดค์„ ๊ฑฐ์˜ˆ์š” ใ…‹ใ…‹ ๊ทผ๋ฐ ์‹ค๋ฌด ๋“ค์–ด๊ฐ€๋ฉด ์–˜๊ธฐ๊ฐ€ ์™„์ „ ๋‹ฌ๋ผ์ง€๊ฑฐ๋“ ์š”. ํŒ€์›์ด ๋‚ด ์ฝ”๋“œ ๋ณด๊ณ  "์ด๊ฒŒ ๋ญ์•ผ..." ํ•˜๋Š” ํ‘œ์ • ์ง“๋Š” ๊ฑฐ ์ƒ์ƒํ•ด๋ด์š”. ์ง„์งœ ์†Œ๋ฆ„ ๋‹์ž–์•„์š”.

์ฝ”๋“œ ํ’ˆ์งˆ(Code Quality)์ด๋ž€ ๋‹จ์ˆœํžˆ "๋ฒ„๊ทธ ์—†์ด ์‹คํ–‰๋œ๋‹ค"๋Š” ์ˆ˜์ค€์„ ๋„˜์–ด์„œ, ๊ฐ€๋…์„ฑยท์œ ์ง€๋ณด์ˆ˜์„ฑยท์„ฑ๋Šฅยท๋ณด์•ˆ๊นŒ์ง€ ์•„์šฐ๋ฅด๋Š” ๊ฐœ๋…์ด์—์š”. Go ์–ธ์–ด๋Š” ํƒœ์ƒ์ ์œผ๋กœ ์‹ฌํ”Œํ•˜๊ณ  ๋ช…ํ™•ํ•œ ์ฝ”๋“œ๋ฅผ ์ง€ํ–ฅํ•˜๋Š” ์–ธ์–ด์ธ๋ฐ, ๊ทธ ์ฒ ํ•™์„ ์ œ๋Œ€๋กœ ์‚ด๋ฆฌ๋ ค๋ฉด ์ •์  ๋ถ„์„ ๋„๊ตฌ๊ฐ€ ํ•„์ˆ˜์˜ˆ์š”.

๐Ÿ’ก ์ •์  ๋ถ„์„(Static Analysis)์ด๋ž€? ์ฝ”๋“œ๋ฅผ ์‹ค์ œ๋กœ ์‹คํ–‰ํ•˜์ง€ ์•Š๊ณ , ์†Œ์Šค์ฝ”๋“œ ์ž์ฒด๋ฅผ ๋ถ„์„ํ•ด์„œ ์ž ์žฌ์  ๋ฒ„๊ทธยท์Šคํƒ€์ผ ์œ„๋ฐ˜ยท๋ณด์•ˆ ์ทจ์•ฝ์  ๋“ฑ์„ ์ฐพ์•„๋‚ด๋Š” ๊ธฐ๋ฒ•์ด์—์š”. ๋Ÿฐํƒ€์ž„ ์—๋Ÿฌ๋ฅผ ์‚ฌ์ „์— ์žก์•„๋‚ผ ์ˆ˜ ์žˆ์–ด์„œ ๊ฐœ๋ฐœ ๋น„์šฉ์„ ์—„์ฒญ๋‚˜๊ฒŒ ์ค„์—ฌ์ค˜์š”.

Go ์ƒํƒœ๊ณ„์—์„œ ์ •์  ๋ถ„์„ ๋„๊ตฌ๋Š” ๊ต‰์žฅํžˆ ํ’๋ถ€ํ•œ๋ฐ์š”, ๋ฌธ์ œ๋Š” ๋„๊ตฌ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ๋‹ค๋Š” ๊ฒƒ์ด์—์š” ใ…‹ใ…‹ golint, staticcheck, errcheck, govet, ineffassign... ์ด๊ฑธ ๋‹ค ๋”ฐ๋กœ๋”ฐ๋กœ ์„ค์น˜ํ•˜๊ณ  ์‹คํ–‰ํ•˜๋ฉด ์ง„์งœ ๋จธ๋ฆฌ ์•„ํŒŒ์š”. ๊ทธ๋ž˜์„œ ๋“ฑ์žฅํ•œ ๊ฒŒ ๋ฐ”๋กœ golangci-lint์˜ˆ์š”!


โšก golangci-lint๊ฐ€ ๋ญ”์ง€ ์ œ๋Œ€๋กœ ์•Œ์•„๋ณด์ž

golangci-lint๋Š” Go ์–ธ์–ด๋ฅผ ์œ„ํ•œ ํ†ตํ•ฉ ๋ฆฐํ„ฐ ๋Ÿฌ๋„ˆ(Aggregated Linter Runner)์˜ˆ์š”. ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด ์ˆ˜์‹ญ ๊ฐœ์˜ lint ๋„๊ตฌ๋ฅผ ํ•˜๋‚˜๋กœ ๋ฌถ์–ด์„œ ๋น ๋ฅด๊ฒŒ ์‹คํ–‰ํ•ด์ฃผ๋Š” ๋ฉ”ํƒ€ ๋„๊ตฌ์˜ˆ์š”. 2018๋…„์— ์ฒ˜์Œ ๋“ฑ์žฅํ–ˆ๊ณ , ์ง€๊ธˆ์€ Go ์ƒํƒœ๊ณ„์—์„œ ์‚ฌ์‹ค์ƒ ํ‘œ์ค€ ์ •์  ๋ถ„์„ ๋„๊ตฌ๋กœ ์ž๋ฆฌ์žก์•˜์–ด์š”.

๐Ÿš€ ์†๋„
๋ณ‘๋ ฌ ์‹คํ–‰ + ์บ์‹ฑ์œผ๋กœ ๊ธฐ์กด ๋„๊ตฌ ๋Œ€๋น„ 5~10๋ฐฐ ๋น ๋ฆ„. ๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ๋„ ๊ฑฐ๋œฌํ•ด์š”.
๐Ÿ”ง ํ†ตํ•ฉ์„ฑ
50๊ฐœ ์ด์ƒ์˜ linter๋ฅผ ๋‹จ์ผ ๋ช…๋ น์–ด๋กœ ์‹คํ–‰. ์„ค์ • ํŒŒ์ผ ํ•˜๋‚˜๋กœ ์ „๋ถ€ ๊ด€๋ฆฌํ•ด์š”.
โš™๏ธ ์œ ์—ฐ์„ฑ
YAML ์„ค์ •์œผ๋กœ linter๋ณ„ ์„ธ๋ฐ€ํ•œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๊ฐ€๋Šฅ. ํ”„๋กœ์ ํŠธ ํŠน์„ฑ์— ๋งž๊ฒŒ ์กฐ์ ˆํ•ด์š”.
๐Ÿ“Š ์ฃผ์š” ๋‚ด์žฅ Linter ๋ชฉ๋ก
Linter ์ด๋ฆ„ ์—ญํ•  ๊ธฐ๋ณธ ํ™œ์„ฑํ™”
errcheck ์—๋Ÿฌ ๋ฐ˜ํ™˜๊ฐ’ ๋ฌด์‹œ ์—ฌ๋ถ€ ๊ฒ€์‚ฌ โœ… ๊ธฐ๋ณธ ํ™œ์„ฑ
gosimple ์ฝ”๋“œ ๋‹จ์ˆœํ™” ๊ฐ€๋Šฅ ๋ถ€๋ถ„ ์ œ์•ˆ โœ… ๊ธฐ๋ณธ ํ™œ์„ฑ
govet Go ๊ณต์‹ vet ๋„๊ตฌ (์˜์‹ฌ์Šค๋Ÿฌ์šด ์ฝ”๋“œ ํŒจํ„ด) โœ… ๊ธฐ๋ณธ ํ™œ์„ฑ
ineffassign ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜ ํ• ๋‹น ๊ฐ์ง€ โœ… ๊ธฐ๋ณธ ํ™œ์„ฑ
staticcheck ๊ณ ๊ธ‰ ์ •์  ๋ถ„์„ (deprecated API, ๋ฒ„๊ทธ ํŒจํ„ด ๋“ฑ) โœ… ๊ธฐ๋ณธ ํ™œ์„ฑ
unused ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ์ฝ”๋“œ ๊ฐ์ง€ โœ… ๊ธฐ๋ณธ ํ™œ์„ฑ
gofmt Go ๊ณต์‹ ํฌ๋งทํŒ… ๊ทœ์น™ ์ค€์ˆ˜ ์—ฌ๋ถ€ โŒ ์„ ํƒ ํ™œ์„ฑ
goimports import ์ •๋ ฌ ๋ฐ ๋ฏธ์‚ฌ์šฉ import ๊ฒ€์‚ฌ โŒ ์„ ํƒ ํ™œ์„ฑ
gosec ๋ณด์•ˆ ์ทจ์•ฝ์  ํŒจํ„ด ๊ฒ€์‚ฌ โŒ ์„ ํƒ ํ™œ์„ฑ
cyclop ์ˆœํ™˜ ๋ณต์žก๋„(Cyclomatic Complexity) ์ธก์ • โŒ ์„ ํƒ ํ™œ์„ฑ
dupl ์ค‘๋ณต ์ฝ”๋“œ ๊ฐ์ง€ โŒ ์„ ํƒ ํ™œ์„ฑ
funlen ํ•จ์ˆ˜ ๊ธธ์ด ์ œํ•œ ๊ฒ€์‚ฌ โŒ ์„ ํƒ ํ™œ์„ฑ
โš ๏ธ golangci-lint v1.54 ์ดํ›„๋ถ€ํ„ฐ ์ผ๋ถ€ linter์˜ ๊ธฐ๋ณธ ํ™œ์„ฑํ™” ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋์–ด์š”. ๋ฒ„์ „ ์—…๊ทธ๋ ˆ์ด๋“œ ์‹œ ๋ฐ˜๋“œ์‹œ ๋ฆด๋ฆฌ์ฆˆ ๋…ธํŠธ ํ™•์ธํ•˜๋Š” ๊ฑฐ ์žŠ์ง€ ๋งˆ์„ธ์š”!

๐Ÿ›  ์„ค์น˜๋ถ€ํ„ฐ ์ฒซ ์‹คํ–‰๊นŒ์ง€ (์ง„์งœ ์‰ฌ์›Œ์š”)
๐Ÿ“ฆ ์„ค์น˜ ๋ฐฉ๋ฒ•

golangci-lint ์„ค์น˜ ๋ฐฉ๋ฒ•์€ ์—ฌ๋Ÿฌ ๊ฐ€์ง€์ธ๋ฐ, ๊ณต์‹ ๊ถŒ์žฅ ๋ฐฉ๋ฒ•์€ ๋ฐ”์ด๋„ˆ๋ฆฌ ์ง์ ‘ ์„ค์น˜์˜ˆ์š”. go install๋กœ ์„ค์น˜ํ•˜๋Š” ๊ฑด ๊ณต์‹์ ์œผ๋กœ ๋น„๊ถŒ์žฅ์ด์—์š” (๋ฒ„์ „ ๋ถˆ์ผ์น˜ ๋ฌธ์ œ ๋•Œ๋ฌธ).

macOS (Homebrew)

shell
brew install golangci-lint
brew upgrade golangci-lint

Linux / macOS (๊ณต์‹ ์Šคํฌ๋ฆฝํŠธ)

shell
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \
  | sh -s -- -b $(go env GOPATH)/bin v1.57.2

Windows (Scoop)

shell
scoop install golangci-lint

Docker

shell
docker run --rm -v $(pwd):/app -w /app \
  golangci/golangci-lint:v1.57.2 golangci-lint run
๐Ÿš€ ์ฒซ ์‹คํ–‰

์„ค์น˜ ์™„๋ฃŒ ํ›„ ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์—์„œ ๊ทธ๋ƒฅ ์ด๋ ‡๊ฒŒ ์น˜๋ฉด ๋ผ์š”:

shell
golangci-lint run

๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ์˜ ๋ชจ๋“  Go ํŒŒ์ผ์„ ๋ถ„์„ํ•ด์š”. ํŠน์ • ๋””๋ ‰ํ† ๋ฆฌ๋งŒ ๋ถ„์„ํ•˜๊ณ  ์‹ถ์œผ๋ฉด:

shell
# ํŠน์ • ํŒจํ‚ค์ง€๋งŒ ๋ถ„์„
golangci-lint run ./internal/...

# ํŠน์ • ํŒŒ์ผ๋งŒ ๋ถ„์„
golangci-lint run ./cmd/main.go

# ํƒ€์ž„์•„์›ƒ ์„ค์ • (๋Œ€ํ˜• ํ”„๋กœ์ ํŠธ์šฉ)
golangci-lint run --timeout 5m ./...
๐Ÿ”ต ํŒ! ์ฒ˜์Œ ์‹คํ–‰ํ•˜๋ฉด ๊ฒฝ๊ณ ๊ฐ€ ์—„์ฒญ ์Ÿ์•„์งˆ ์ˆ˜ ์žˆ์–ด์š” ใ…‹ใ…‹ ๋‹นํ™ฉํ•˜์ง€ ๋ง๊ณ , ํ•˜๋‚˜์”ฉ ๊ณ ์ณ๋‚˜๊ฐ€๋ฉด ๋ผ์š”. ๋ ˆ๊ฑฐ์‹œ ํ”„๋กœ์ ํŠธ์— ์ฒ˜์Œ ์ ์šฉํ•  ๋•Œ๋Š” --new-from-rev ์˜ต์…˜์œผ๋กœ ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ฝ”๋“œ๋งŒ ๊ฒ€์‚ฌํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ์ „๋žต์ด์—์š”.

โš™๏ธ .golangci.yml ์„ค์ • ํŒŒ์ผ ์™„์ „ ํ•ด๋ถ€

golangci-lint์˜ ์ง„์งœ ํŒŒ์›Œ๋Š” ์„ค์ • ํŒŒ์ผ์—์„œ ๋‚˜์™€์š”. ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์— .golangci.yml ํŒŒ์ผ์„ ๋งŒ๋“ค๋ฉด ํŒ€ ์ „์ฒด๊ฐ€ ๋™์ผํ•œ lint ๊ทœ์น™์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์–ด์š”. ์ด๊ฒŒ ์ง„์งœ ์ค‘์š”ํ•œ ๊ฑฐ์˜ˆ์š” - ๊ฐœ์ธ ์ทจํ–ฅ์ด ์•„๋‹ˆ๋ผ ํŒ€ ์ปจ๋ฒค์…˜์„ ์ฝ”๋“œ๋กœ ๊ฐ•์ œํ•˜๋Š” ๊ฑฐ๊ฑฐ๋“ ์š”.

๐Ÿ“ ๊ธฐ๋ณธ ์„ค์ • ๊ตฌ์กฐ
.golangci.yml
run:
  timeout: 5m
  go: '1.21'
  tests: true
  skip-dirs:
    - vendor
    - third_party

linters:
  enable-all: false
  enable:
    - errcheck
    - gosimple
    - govet
    - ineffassign
    - staticcheck
    - unused
    - gofmt
    - goimports
    - gosec
    - cyclop
    - funlen
    - gocognit
    - misspell
    - revive
  disable:
    - deadcode   # unused๋กœ ๋Œ€์ฒด๋จ

linters-settings:
  cyclop:
    max-complexity: 10
  funlen:
    lines: 80
    statements: 50
  gocognit:
    min-complexity: 15
  gosec:
    excludes:
      - G104  # ํŠน์ • ๊ทœ์น™ ์ œ์™ธ
  revive:
    rules:
      - name: exported
        severity: warning
  misspell:
    locale: US

issues:
  exclude-rules:
    - path: _test\.go
      linters:
        - funlen
        - gosec
  max-issues-per-linter: 50
  max-same-issues: 10
  new: false
๐Ÿ”‘ ์ฃผ์š” ์„ค์ • ์„น์…˜ ์„ค๋ช…
run ์„น์…˜
์ „์ฒด ์‹คํ–‰ ํ™˜๊ฒฝ์„ ์„ค์ •ํ•ด์š”. timeout์€ ๋ถ„์„ ์ œํ•œ ์‹œ๊ฐ„, go๋Š” Go ๋ฒ„์ „ ์ง€์ •, skip-dirs๋Š” ๋ถ„์„ ์ œ์™ธ ๋””๋ ‰ํ† ๋ฆฌ์˜ˆ์š”. vendor ๋””๋ ‰ํ† ๋ฆฌ๋Š” ๊ฑฐ์˜ ํ•ญ์ƒ ์ œ์™ธํ•˜๋Š” ๊ฒŒ ๊ตญ๋ฃฐ์ด์—์š” ใ…‹ใ…‹
linters ์„น์…˜
์–ด๋–ค linter๋ฅผ ํ™œ์„ฑํ™”/๋น„ํ™œ์„ฑํ™”ํ• ์ง€ ๊ฒฐ์ •ํ•ด์š”. enable-all: true๋กœ ์ „๋ถ€ ์ผœ๊ณ  ์ผ๋ถ€๋งŒ ๋„๋Š” ๋ฐฉ์‹๋„ ์žˆ์ง€๋งŒ, ์ฒ˜์Œ์—” ํ•„์š”ํ•œ ๊ฒƒ๋งŒ ์ผœ๋Š” ๊ฒŒ ์ •์‹ ๊ฑด๊ฐ•์— ์ข‹์•„์š” ใ…‹ใ…‹
linters-settings ์„น์…˜
๊ฐ linter์˜ ์„ธ๋ถ€ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์กฐ์ •ํ•ด์š”. ์˜ˆ๋ฅผ ๋“ค์–ด funlen์˜ ์ตœ๋Œ€ ์ค„ ์ˆ˜, cyclop์˜ ์ตœ๋Œ€ ๋ณต์žก๋„ ๋“ฑ์„ ํ”„๋กœ์ ํŠธ ํŠน์„ฑ์— ๋งž๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”.
issues ์„น์…˜
ํŠน์ • ํŒŒ์ผ์ด๋‚˜ ํŒจํ„ด์—์„œ ํŠน์ • linter๋ฅผ ์ œ์™ธํ•˜๋Š” ๊ทœ์น™์„ ์ •์˜ํ•ด์š”. ํ…Œ์ŠคํŠธ ํŒŒ์ผ์—์„œ funlen์„ ๋„๋Š” ๊ฑด ๊ฑฐ์˜ ๋ชจ๋“  ํŒ€์ด ํ•˜๋Š” ์„ค์ •์ด์—์š”.
โš ๏ธ enable-all: true๋Š” ์‹ ์ค‘ํ•˜๊ฒŒ ์จ์•ผ ํ•ด์š”. ์ƒˆ ๋ฒ„์ „์—์„œ linter๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉด ๊ฐ‘์ž๊ธฐ CI๊ฐ€ ๊นจ์งˆ ์ˆ˜ ์žˆ๊ฑฐ๋“ ์š”. ๋ช…์‹œ์ ์œผ๋กœ ํ•„์š”ํ•œ linter๋งŒ enableํ•˜๋Š” ๊ฒŒ ๋” ์•ˆ์ •์ ์ด์—์š”.

๐Ÿ”ฌ ํ•ต์‹ฌ Linter ์‹ค์ „ ์˜ˆ์ œ๋กœ ์ดํ•ดํ•˜๊ธฐ

์ด๋ก ๋งŒ ๋ณด๋ฉด ์ง€๋ฃจํ•˜์ž–์•„์š” ใ…‹ใ…‹ ์‹ค์ œ ์ฝ”๋“œ ์˜ˆ์ œ๋กœ ๊ฐ linter๊ฐ€ ๋ญ˜ ์žก์•„๋‚ด๋Š”์ง€ ๋ด์š”!

1๏ธโƒฃ errcheck - ์—๋Ÿฌ ๋ฌด์‹œํ•˜๋ฉด ์•ˆ ๋ผ์š”!

Go์—์„œ ๊ฐ€์žฅ ํ”ํ•œ ์‹ค์ˆ˜ ์ค‘ ํ•˜๋‚˜๊ฐ€ ์—๋Ÿฌ ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฌด์‹œํ•˜๋Š” ๊ฑฐ์˜ˆ์š”.

bad.go - errcheck๊ฐ€ ์žก์•„๋‚ด๋Š” ์ฝ”๋“œ
package main

import (
    "os"
    "fmt"
)

func main() {
    // โŒ ์—๋Ÿฌ ๋ฌด์‹œ! errcheck๊ฐ€ ๊ฒฝ๊ณ  ๋ฐœ์ƒ
    os.Remove("/tmp/test.txt")
    
    f, _ := os.Open("config.json")  // โŒ ์—๋Ÿฌ๋ฅผ _๋กœ ๋ฌด์‹œ
    fmt.Println(f)
}
good.go - ์˜ฌ๋ฐ”๋ฅธ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    // โœ… ์—๋Ÿฌ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ฒ˜๋ฆฌ
    if err := os.Remove("/tmp/test.txt"); err != nil {
        log.Printf("ํŒŒ์ผ ์‚ญ์ œ ์‹คํŒจ: %v", err)
    }
    
    f, err := os.Open("config.json")
    if err != nil {
        log.Fatalf("ํŒŒ์ผ ์—ด๊ธฐ ์‹คํŒจ: %v", err)
    }
    defer f.Close()
    fmt.Println(f.Name())
}
2๏ธโƒฃ staticcheck - ๊ณ ๊ธ‰ ๋ฒ„๊ทธ ํŒจํ„ด ๊ฐ์ง€
staticcheck ์˜ˆ์ œ
package main

import "sync"

func badExample() {
    var mu sync.Mutex
    
    // โŒ SA2001: sync.Mutex๋ฅผ ๊ฐ’์œผ๋กœ ๋ณต์‚ฌ (Lock ์ƒํƒœ ๋ณต์‚ฌ ์œ„ํ—˜)
    mu2 := mu
    _ = mu2
    
    // โŒ S1039: ๋ถˆํ•„์š”ํ•œ fmt.Sprintf ์‚ฌ์šฉ
    // s := fmt.Sprintf("%s", "hello")  // ๊ทธ๋ƒฅ s := "hello" ์“ฐ๋ฉด ๋จ
    
    // โŒ SA4006: ๋ณ€์ˆ˜ ํ• ๋‹น ํ›„ ์‚ฌ์šฉ ์•ˆ ํ•จ
    x := 42
    x = 100  // ์ด์ „ 42๋Š” ์‚ฌ์šฉ๋œ ์  ์—†์Œ
    _ = x
}
3๏ธโƒฃ gosec - ๋ณด์•ˆ ์ทจ์•ฝ์  ๊ฐ์ง€
gosec ์˜ˆ์ œ
package main

import (
    "fmt"
    "os/exec"
    "net/http"
)

func vulnerableCode(userInput string) {
    // โŒ G204: ์‚ฌ์šฉ์ž ์ž…๋ ฅ์œผ๋กœ ๋ช…๋ น์–ด ์‹คํ–‰ (Command Injection ์œ„ํ—˜)
    cmd := exec.Command("sh", "-c", userInput)
    cmd.Run()
    
    // โŒ G107: ๋ณ€์ˆ˜๋กœ URL ๊ตฌ์„ฑ (SSRF ์œ„ํ—˜)
    resp, _ := http.Get(fmt.Sprintf("http://%s/api", userInput))
    _ = resp
}
4๏ธโƒฃ cyclop - ๋ณต์žก๋„ ๊ด€๋ฆฌ

์ˆœํ™˜ ๋ณต์žก๋„(Cyclomatic Complexity)๋Š” ์ฝ”๋“œ์˜ ๋ถ„๊ธฐ ๊ฒฝ๋กœ ์ˆ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ง€ํ‘œ์˜ˆ์š”. ๋†’์„์ˆ˜๋ก ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์–ด๋ ต๊ณ  ๋ฒ„๊ทธ๊ฐ€ ์ƒ๊ธฐ๊ธฐ ์‰ฌ์›Œ์š”.

๋ณต์žก๋„ ๋†’์€ ํ•จ์ˆ˜ ์˜ˆ์‹œ
// โŒ ๋ณต์žก๋„ 12 - cyclop ๊ฒฝ๊ณ  ๋ฐœ์ƒ (๊ธฐ๋ณธ ์ž„๊ณ„๊ฐ’ 10 ์ดˆ๊ณผ)
func processOrder(order Order) error {
    if order.ID == "" {
        return errors.New("ID ์—†์Œ")
    }
    if order.Amount <= 0 {
        return errors.New("๊ธˆ์•ก ์˜ค๋ฅ˜")
    }
    if order.UserID == "" {
        return errors.New("์‚ฌ์šฉ์ž ์—†์Œ")
    }
    switch order.Status {
    case "pending":
        if order.Amount > 1000000 {
            if order.VIPUser {
                // ์ฒ˜๋ฆฌ ๋กœ์ง...
            } else {
                // ๋‹ค๋ฅธ ์ฒ˜๋ฆฌ...
            }
        }
    case "processing":
        // ...
    case "completed":
        // ...
    default:
        return errors.New("์•Œ ์ˆ˜ ์—†๋Š” ์ƒํƒœ")
    }
    return nil
}

// โœ… ํ•จ์ˆ˜ ๋ถ„๋ฆฌ๋กœ ๋ณต์žก๋„ ๋‚ฎ์ถ”๊ธฐ
func validateOrder(order Order) error {
    if order.ID == "" { return errors.New("ID ์—†์Œ") }
    if order.Amount <= 0 { return errors.New("๊ธˆ์•ก ์˜ค๋ฅ˜") }
    if order.UserID == "" { return errors.New("์‚ฌ์šฉ์ž ์—†์Œ") }
    return nil
}

func processOrderStatus(order Order) error {
    // ์ƒํƒœ๋ณ„ ์ฒ˜๋ฆฌ ๋กœ์ง ๋ถ„๋ฆฌ
    return nil
}
5๏ธโƒฃ revive - golint์˜ ํ˜„๋Œ€์  ๋Œ€์•ˆ

revive๋Š” deprecated๋œ golint๋ฅผ ๋Œ€์ฒดํ•˜๋Š” linter์˜ˆ์š”. Go ์ฝ”๋”ฉ ์ปจ๋ฒค์…˜ ์ค€์ˆ˜ ์—ฌ๋ถ€๋ฅผ ๊ฒ€์‚ฌํ•ด์š”.

revive ์˜ˆ์ œ
// โŒ exported ํ•จ์ˆ˜์— ์ฃผ์„ ์—†์Œ
func ProcessData(data []byte) error {
    return nil
}

// โŒ ์—๋Ÿฌ ํƒ€์ž… ์ด๋ฆ„์ด Error๋กœ ๋๋‚˜์ง€ ์•Š์Œ
type MyErr struct {
    msg string
}

// โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ์‹
// ProcessData๋Š” ๋ฐ”์ดํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
func ProcessData(data []byte) error {
    return nil
}

// โœ… ์—๋Ÿฌ ํƒ€์ž… ๋ช…๋ช… ๊ทœ์น™ ์ค€์ˆ˜
type ProcessError struct {
    msg string
}

func (e *ProcessError) Error() string {
    return e.msg
}

GitHub Actions CI/CD ํ†ตํ•ฉ ํ๋ฆ„ โ‘  ์ฝ”๋“œ Push git push origin โ‘ก Actions ํŠธ๋ฆฌ๊ฑฐ ์›Œํฌํ”Œ๋กœ์šฐ ์‹คํ–‰ โ‘ข golangci-lint ์ •์  ๋ถ„์„ ์‹คํ–‰ โ‘ฃ ๊ฒฐ๊ณผ ํŒ์ • Pass / Fail โœ… ๋จธ์ง€ ํ—ˆ์šฉ PR ์Šน์ธ ๊ฐ€๋Šฅ โŒ ๋จธ์ง€ ์ฐจ๋‹จ lint ์˜ค๋ฅ˜ ์ˆ˜์ • ํ•„์š” ์˜ค๋ฅ˜ ์ˆ˜์ • ํ›„ ์žฌPush GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ ํ•ต์‹ฌ golangci/golangci-lint-action@v4 ์‚ฌ์šฉ PR ์ƒ์„ฑ ์‹œ ์ž๋™์œผ๋กœ lint ๊ฒ€์‚ฌ ์‹คํ–‰ lint ์‹คํŒจ ์‹œ ๋จธ์ง€ ๋ฒ„ํŠผ ๋น„ํ™œ์„ฑํ™”
๐Ÿš€ CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ํ†ตํ•ฉํ•˜๊ธฐ

golangci-lint๋ฅผ ๋กœ์ปฌ์—์„œ๋งŒ ์“ฐ๋ฉด ๋ฐ˜์ชฝ์งœ๋ฆฌ์˜ˆ์š”. ์ง„์งœ ํŒŒ์›Œ๋Š” CI/CD ํŒŒ์ดํ”„๋ผ์ธ์— ํ†ตํ•ฉํ–ˆ์„ ๋•Œ ๋‚˜์™€์š”. ํŒ€์› ๋ˆ„๊ตฐ๊ฐ€๊ฐ€ lint ๊ฒ€์‚ฌ ์•ˆ ํ•˜๊ณ  PR ์˜ฌ๋ ค๋„ ์ž๋™์œผ๋กœ ๋ง‰์•„์ฃผ๊ฑฐ๋“ ์š” ใ…‹ใ…‹ ์ด๊ฒŒ ์ง„์งœ ํŒ€ ์ฝ”๋“œ ํ’ˆ์งˆ ๊ด€๋ฆฌ์˜ ํ•ต์‹ฌ์ด์—์š”.

๐Ÿ™ GitHub Actions ์„ค์ •
.github/workflows/lint.yml
name: golangci-lint

on:
  push:
    branches:
      - main
      - develop
  pull_request:
    branches:
      - main
      - develop

permissions:
  contents: read
  pull-requests: read

jobs:
  golangci:
    name: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-go@v5
        with:
          go-version: '1.21'
          cache: false
      
      - name: golangci-lint ์‹คํ–‰
        uses: golangci/golangci-lint-action@v4
        with:
          version: v1.57.2
          args: --timeout=5m
          # ์„ค์ • ํŒŒ์ผ ๊ฒฝ๋กœ (๊ธฐ๋ณธ๊ฐ’: .golangci.yml)
          # config: .golangci.yml
๐ŸฆŠ GitLab CI ์„ค์ •
.gitlab-ci.yml
lint:
  image: golangci/golangci-lint:v1.57.2
  stage: test
  script:
    - golangci-lint run --timeout 5m ./...
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"
๐Ÿ”ต Bitbucket Pipelines ์„ค์ •
bitbucket-pipelines.yml
pipelines:
  pull-requests:
    '**':
      - step:
          name: Go Lint
          image: golangci/golangci-lint:v1.57.2
          script:
            - golangci-lint run --timeout 5m ./...
๐Ÿ”ต ์„ฑ๋Šฅ ํŒ! GitHub Actions์—์„œ golangci-lint-action์„ ์“ฐ๋ฉด ์ž๋™์œผ๋กœ ์บ์‹ฑ์ด ์ ์šฉ๋ผ์š”. ๋‘ ๋ฒˆ์งธ ์‹คํ–‰๋ถ€ํ„ฐ๋Š” ํ›จ์”ฌ ๋นจ๋ผ์ ธ์š”. ์บ์‹œ ํ‚ค๋Š” Go ๋ฒ„์ „๊ณผ ์„ค์ • ํŒŒ์ผ ํ•ด์‹œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž๋™ ์ƒ์„ฑ๋ผ์š”.

๐ŸŽฏ ์‹ค์ „ ํŒ: ํŒ€์—์„œ golangci-lint ๋„์ž…ํ•˜๊ธฐ

์ด๋ก ์€ ์•Œ๊ฒ ๋Š”๋ฐ ์‹ค์ œ๋กœ ํŒ€์— ๋„์ž…ํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผ ํ• ๊นŒ์š”? ๊ฒฝํ—˜ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹จ๊ณ„๋ณ„ ์ „๋žต์„ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”.

๐Ÿ“‹ ๋‹จ๊ณ„๋ณ„ ๋„์ž… ์ „๋žต
1ํ˜„ํ™ฉ ํŒŒ์•…๋ถ€ํ„ฐ
๊ธฐ์กด ํ”„๋กœ์ ํŠธ์— ์ฒ˜์Œ ์ ์šฉํ•˜๋ฉด ์ˆ˜๋ฐฑ ๊ฐœ์˜ ๊ฒฝ๊ณ ๊ฐ€ ์Ÿ์•„์งˆ ์ˆ˜ ์žˆ์–ด์š”. ๋จผ์ €
golangci-lint run --out-format json | jq '.Issues | group_by(.FromLinter) | map({linter: .[0].FromLinter, count: length})'
๋กœ linter๋ณ„ ์ด์Šˆ ์ˆ˜๋ฅผ ํŒŒ์•…ํ•ด์š”.
2์ ์ง„์  ์ ์šฉ
์ฒ˜์Œ๋ถ€ํ„ฐ ๋ชจ๋“  linter๋ฅผ ์ผœ๋ฉด ํŒ€์›๋“ค์ด ๋ฐ˜๋ฐœํ•  ์ˆ˜ ์žˆ์–ด์š” ใ…‹ใ…‹ ๊ธฐ๋ณธ linter(errcheck, govet, staticcheck)๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ 2์ฃผ๋งˆ๋‹ค ํ•˜๋‚˜์”ฉ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์ด ํ˜„์‹ค์ ์ด์—์š”.
3๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ ์ฒ˜๋ฆฌ
--new-from-rev=HEAD~1 ์˜ต์…˜์œผ๋กœ ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ์ฝ”๋“œ๋งŒ ๊ฒ€์‚ฌํ•˜๊ฑฐ๋‚˜, //nolint:lintername ์ฃผ์„์œผ๋กœ ํŠน์ • ๋ผ์ธ์„ ์ผ์‹œ์ ์œผ๋กœ ์ œ์™ธํ•  ์ˆ˜ ์žˆ์–ด์š”. ๋‹จ, nolint ๋‚จ์šฉ์€ ๊ธˆ๋ฌผ!
4pre-commit hook ์—ฐ๋™
CI์—์„œ๋งŒ ๊ฒ€์‚ฌํ•˜๋ฉด ๋Šฆ์–ด์š”. ๋กœ์ปฌ์—์„œ ์ปค๋ฐ‹ ์ „์— ์ž๋™์œผ๋กœ lint๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํ›จ์”ฌ ๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์–ด์š”.
๐Ÿช pre-commit hook ์„ค์ •
.pre-commit-config.yaml
repos:
  - repo: https://github.com/dnephin/pre-commit-golang
    rev: v0.5.1
    hooks:
      - id: golangci-lint

๋˜๋Š” ์ง์ ‘ git hook์œผ๋กœ ์„ค์ •:

.git/hooks/pre-commit
#!/bin/sh
echo "๐Ÿ” golangci-lint ์‹คํ–‰ ์ค‘..."
golangci-lint run ./...
if [ $? -ne 0 ]; then
    echo "โŒ lint ์˜ค๋ฅ˜๊ฐ€ ์žˆ์–ด์š”. ์ˆ˜์ • ํ›„ ๋‹ค์‹œ ์ปค๋ฐ‹ํ•ด์ฃผ์„ธ์š”."
    exit 1
fi
echo "โœ… lint ํ†ต๊ณผ!"
๐Ÿ’ฌ nolint ์ฃผ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ

์–ด์ฉ” ์ˆ˜ ์—†์ด lint ๊ทœ์น™์„ ๋ฌด์‹œํ•ด์•ผ ํ•  ๋•Œ๋Š” //nolint ์ฃผ์„์„ ์จ์š”. ๊ทผ๋ฐ ์ด๊ฑฐ ๋‚จ์šฉํ•˜๋ฉด lint ์“ฐ๋Š” ์˜๋ฏธ๊ฐ€ ์—†์–ด์ง€๋‹ˆ๊นŒ ์ฃผ์˜ํ•ด์š”!

nolint ์‚ฌ์šฉ ์˜ˆ์‹œ
// โœ… ํŠน์ • linter๋งŒ ์ œ์™ธ (์ด์œ  ๋ช…์‹œ ํ•„์ˆ˜!)
func legacyFunction() error {
    result, _ := riskyOperation() //nolint:errcheck // ๋ ˆ๊ฑฐ์‹œ ์ฝ”๋“œ, ์ถ”ํ›„ ์ˆ˜์ • ์˜ˆ์ • (#123)
    return result
}

// โœ… ์—ฌ๋Ÿฌ linter ๋™์‹œ ์ œ์™ธ
var globalVar = "test" //nolint:gochecknoglobals,revive // ์„ค์ •๊ฐ’์œผ๋กœ ์‚ฌ์šฉ

// โŒ ์ด์œ  ์—†๋Š” nolint (๋‚˜์œ ์˜ˆ)
result, _ := riskyOperation() //nolint
โš ๏ธ //nolint ์ฃผ์„์—๋Š” ๋ฐ˜๋“œ์‹œ ์–ด๋–ค linter๋ฅผ ์ œ์™ธํ•˜๋Š”์ง€์™€ ์ด์œ ๋ฅผ ๋ช…์‹œํ•˜๋Š” ๊ฒŒ ํŒ€ ์ปจ๋ฒค์…˜์œผ๋กœ ์ •ํ•˜๋Š” ๊ฒŒ ์ข‹์•„์š”. ์ด์œ  ์—†๋Š” nolint๋Š” ์ฝ”๋“œ ๋ฆฌ๋ทฐ์—์„œ ๋ฐ˜๋“œ์‹œ ์ง€์ ๋ฐ›์•„์•ผ ํ•ด์š”.

๐Ÿ“ˆ ์ฝ”๋“œ ํ’ˆ์งˆ ์ง€ํ‘œ ์ดํ•ดํ•˜๊ธฐ

golangci-lint๊ฐ€ ์žก์•„๋‚ด๋Š” ์ด์Šˆ๋“ค์„ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ์ดํ•ดํ•˜๋ฉด ๋” ํšจ๊ณผ์ ์œผ๋กœ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.

์ฝ”๋“œ ํ’ˆ์งˆ 5๋Œ€ ์ง€ํ‘œ ๐Ÿ› ๋ฒ„๊ทธ ์˜ˆ๋ฐฉ errcheck staticcheck govet ๐Ÿ”’ ๋ณด์•ˆ ๊ฐ•ํ™” gosec bodyclose sqlclosecheck ๐Ÿ“– ๊ฐ€๋…์„ฑ revive gofmt goimports โšก ์„ฑ๋Šฅ prealloc ineffassign maligned ๐Ÿ— ๋ณต์žก๋„ cyclop funlen gocognit ๊ฐ ์ง€ํ‘œ๋ณ„ ๊ถŒ์žฅ ์ž„๊ณ„๊ฐ’ ์ˆœํ™˜ ๋ณต์žก๋„ ํ•จ์ˆ˜๋‹น ์ตœ๋Œ€ 10 ๊ถŒ์žฅ ํ•จ์ˆ˜ ๊ธธ์ด ์ตœ๋Œ€ 80์ค„ ๊ถŒ์žฅ ์ธ์ง€ ๋ณต์žก๋„ ์ตœ๋Œ€ 15 ๊ถŒ์žฅ ์ค‘๋ณต ์ฝ”๋“œ 150ํ† ํฐ ์ด์ƒ ์ค‘๋ณต ๊ฒฝ๊ณ  ๐Ÿ’ก ์ด ์ˆ˜์น˜๋“ค์€ ์ ˆ๋Œ€์ ์ธ ๊ธฐ์ค€์ด ์•„๋‹ˆ์—์š”. ํŒ€๊ณผ ํ”„๋กœ์ ํŠธ ํŠน์„ฑ์— ๋งž๊ฒŒ ์กฐ์ •ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด์š”! ๋„ˆ๋ฌด ์—„๊ฒฉํ•˜๋ฉด ๊ฐœ๋ฐœ ์†๋„๊ฐ€ ๋–จ์–ด์ง€๊ณ , ๋„ˆ๋ฌด ๋А์Šจํ•˜๋ฉด ํ’ˆ์งˆ ๊ด€๋ฆฌ๊ฐ€ ์•ˆ ๋ผ์š”.
๐Ÿ” ์ž์ฃผ ๋ฐœ์ƒํ•˜๋Š” ์ด์Šˆ ์œ ํ˜•๊ณผ ํ•ด๊ฒฐ๋ฒ•
์ด์Šˆ ์œ ํ˜• ๋ฐœ์ƒ ์›์ธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•
SA1006 Printf์— ํฌ๋งท ์—†์ด ๋ณ€์ˆ˜ ์ง์ ‘ ์ „๋‹ฌ fmt.Println(x) ๋˜๋Š” fmt.Printf("%v", x) ์‚ฌ์šฉ
G401 MD5/SHA1 ๊ฐ™์€ ์•ฝํ•œ ํ•ด์‹œ ์‚ฌ์šฉ SHA-256 ์ด์ƒ ์‚ฌ์šฉ, ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” bcrypt ์‚ฌ์šฉ
bodyclose HTTP ์‘๋‹ต Body๋ฅผ Closeํ•˜์ง€ ์•Š์Œ defer resp.Body.Close() ์ถ”๊ฐ€
contextcheck context.Background() ๋‚จ์šฉ ์ƒ์œ„ ์ปจํ…์ŠคํŠธ๋ฅผ ์ „ํŒŒํ•ด์„œ ์‚ฌ์šฉ
wrapcheck ์™ธ๋ถ€ ํŒจํ‚ค์ง€ ์—๋Ÿฌ๋ฅผ wrapํ•˜์ง€ ์•Š์Œ fmt.Errorf("...: %w", err) ์‚ฌ์šฉ

๐ŸŒŸ ๊ณ ๊ธ‰ ํ™œ์šฉ: ์ปค์Šคํ…€ Linter ๋งŒ๋“ค๊ธฐ

๊ธฐ๋ณธ ์ œ๊ณต linter๋กœ ๋ถ€์กฑํ•˜๋‹ค๋ฉด? ์ง์ ‘ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ์–ด์š”! golangci-lint๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ ์‹œ์Šคํ…œ์„ ์ง€์›ํ•ด์š”. ํšŒ์‚ฌ ๋‚ด๋ถ€ ์ฝ”๋”ฉ ๊ทœ์น™์„ linter๋กœ ๋งŒ๋“ค์–ด์„œ ๊ฐ•์ œํ•  ์ˆ˜ ์žˆ์–ด์š”.

๐Ÿ”Œ ์ปค์Šคํ…€ linter ํ”Œ๋Ÿฌ๊ทธ์ธ ๊ตฌ์กฐ
plugin/myplugin.go
package main

import (
    "go/ast"
    "golang.org/x/tools/go/analysis"
)

// ์ปค์Šคํ…€ ๋ถ„์„๊ธฐ ์ •์˜
var Analyzer = &analysis.Analyzer{
    Name: "mycompanyrule",
    Doc:  "ํšŒ์‚ฌ ๋‚ด๋ถ€ ๊ทœ์น™: ํŠน์ • ํ•จ์ˆ˜ ์‚ฌ์šฉ ๊ธˆ์ง€",
    Run:  run,
}

func run(pass *analysis.Pass) (interface{}, error) {
    for _, file := range pass.Files {
        ast.Inspect(file, func(n ast.Node) bool {
            // ํŠน์ • ํ•จ์ˆ˜ ํ˜ธ์ถœ ํŒจํ„ด ๊ฐ์ง€
            call, ok := n.(*ast.CallExpr)
            if !ok {
                return true
            }
            sel, ok := call.Fun.(*ast.SelectorExpr)
            if !ok {
                return true
            }
            // "deprecated.OldFunction" ํ˜ธ์ถœ ๊ฐ์ง€
            if sel.Sel.Name == "OldFunction" {
                pass.Reportf(call.Pos(), 
                    "OldFunction์€ deprecated์ž…๋‹ˆ๋‹ค. NewFunction์„ ์‚ฌ์šฉํ•˜์„ธ์š”.")
            }
            return true
        })
    }
    return nil, nil
}
๐Ÿ”ต ์ฐธ๊ณ ! ์ปค์Šคํ…€ linter ๊ฐœ๋ฐœ์€ Go์˜ go/analysis ํŒจํ‚ค์ง€๋ฅผ ์ดํ•ดํ•ด์•ผ ํ•ด์š”. ์ฒ˜์Œ์—” ์ง„์ž…์žฅ๋ฒฝ์ด ์žˆ์ง€๋งŒ, ํ•œ ๋ฒˆ ์ตํžˆ๋ฉด ํŒ€ ์ „์ฒด์˜ ์ฝ”๋“œ ํ’ˆ์งˆ์„ ํš๊ธฐ์ ์œผ๋กœ ๋†’์ผ ์ˆ˜ ์žˆ์–ด์š”. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ Go ๊ฐœ๋ฐœ ์ „๋ฌธ๊ฐ€๋ฅผ ์ฐพ์•„ ๋„์›€๋ฐ›๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—์š”!

๐Ÿ›ก ๋ณด์•ˆ ์ค‘์‹ฌ Linter ์‹ฌํ™”: gosec ํ™œ์šฉ

๋ณด์•ˆ์€ ๋‚˜์ค‘์— ์ƒ๊ฐํ•˜๋ฉด ๋Šฆ์–ด์š”. gosec์€ OWASP ๊ฐ€์ด๋“œ๋ผ์ธ ๊ธฐ๋ฐ˜์œผ๋กœ Go ์ฝ”๋“œ์˜ ๋ณด์•ˆ ์ทจ์•ฝ์ ์„ ์ฐพ์•„์ค˜์š”. ์‹ค๋ฌด์—์„œ ์ง„์งœ ์ค‘์š”ํ•œ linter์˜ˆ์š”.

๐Ÿ” gosec ์ฃผ์š” ๊ทœ์น™
๊ทœ์น™ ID ์„ค๋ช… ์œ„ํ—˜๋„
G101 ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ž๊ฒฉ์ฆ๋ช… ๊ฐ์ง€ ๐Ÿ”ด ๋†’์Œ
G201 SQL ์ฟผ๋ฆฌ ํฌ๋งท ๋ฌธ์ž์—ด ์‚ฌ์šฉ (SQL Injection) ๐Ÿ”ด ๋†’์Œ
G204 ์‚ฌ์šฉ์ž ์ž…๋ ฅ์œผ๋กœ ๋ช…๋ น์–ด ์‹คํ–‰ (Command Injection) ๐Ÿ”ด ๋†’์Œ
G304 ํŒŒ์ผ ๊ฒฝ๋กœ์— ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์‚ฌ์šฉ (Path Traversal) ๐ŸŸ  ์ค‘๊ฐ„
G401 ์•ฝํ•œ ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜ ์‚ฌ์šฉ (MD5, SHA1) ๐ŸŸ  ์ค‘๊ฐ„
G501 ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ํ•ด์‹œ ํ•จ์ˆ˜ import ๐ŸŸ  ์ค‘๊ฐ„
G601 ๋ฃจํ”„ ๋ณ€์ˆ˜ ์ฃผ์†Œ ์ฐธ์กฐ (Go 1.22 ์ด์ „) ๐ŸŸก ๋‚ฎ์Œ
gosec์ด ์žก์•„๋‚ด๋Š” ๋ณด์•ˆ ์ทจ์•ฝ์  ์˜ˆ์‹œ
package main

import (
    "crypto/md5"  // G501: ์•ˆ์ „ํ•˜์ง€ ์•Š์€ ํ•ด์‹œ
    "database/sql"
    "fmt"
    "os"
)

const apiKey = "sk-1234567890abcdef"  // G101: ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์ž๊ฒฉ์ฆ๋ช…!

func getUserData(db *sql.DB, userID string) {
    // G201: SQL Injection ์ทจ์•ฝ์ !
    query := fmt.Sprintf("SELECT * FROM users WHERE id = '%s'", userID)
    db.Query(query)
}

func readUserFile(filename string) {
    // G304: Path Traversal ์ทจ์•ฝ์ !
    f, _ := os.Open("/data/" + filename)
    _ = f
}

func hashPassword(password string) []byte {
    // G401: MD5๋Š” ๋ณด์•ˆ์— ์ทจ์•ฝํ•œ ํ•ด์‹œ ์•Œ๊ณ ๋ฆฌ์ฆ˜
    h := md5.New()
    h.Write([]byte(password))
    return h.Sum(nil)
}
๐Ÿ’ก gosec ์„ค์ • ํŒ: ๋ชจ๋“  ๋ณด์•ˆ ๊ทœ์น™์ด ๋ชจ๋“  ํ”„๋กœ์ ํŠธ์— ์ ์šฉ๋˜๋Š” ๊ฑด ์•„๋‹ˆ์—์š”. ์˜ˆ๋ฅผ ๋“ค์–ด ๋‚ด๋ถ€ ๋„๊ตฌ์—์„œ MD5๋ฅผ ์ฒดํฌ์„ฌ ๋ชฉ์ ์œผ๋กœ ์“ด๋‹ค๋ฉด G401์„ ์ œ์™ธํ•  ์ˆ˜ ์žˆ์–ด์š”. ํ•˜์ง€๋งŒ ์ œ์™ธํ•  ๋•Œ๋Š” ๋ฐ˜๋“œ์‹œ ํŒ€ ๋ฆฌ๋ทฐ๋ฅผ ๊ฑฐ์ณ์•ผ ํ•ด์š”.

๐Ÿ“Š golangci-lint ์ถœ๋ ฅ ํ˜•์‹ ํ™œ์šฉํ•˜๊ธฐ

๊ธฐ๋ณธ ํ…์ŠคํŠธ ์ถœ๋ ฅ ์™ธ์—๋„ ๋‹ค์–‘ํ•œ ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์š”. ํŠนํžˆ CI ํ™˜๊ฒฝ์ด๋‚˜ IDE ํ†ตํ•ฉ ์‹œ ์œ ์šฉํ•ด์š”.

๋‹ค์–‘ํ•œ ์ถœ๋ ฅ ํ˜•์‹
# ๊ธฐ๋ณธ ํ…์ŠคํŠธ ์ถœ๋ ฅ
golangci-lint run

# JSON ํ˜•์‹ (ํŒŒ์‹ฑ ์šฉ์ด)
golangci-lint run --out-format json

# GitHub Actions ์–ด๋…ธํ…Œ์ด์…˜ ํ˜•์‹
golangci-lint run --out-format github-actions

# JUnit XML (Jenkins ๋“ฑ CI ๋„๊ตฌ ์—ฐ๋™)
golangci-lint run --out-format junit-xml > lint-report.xml

# ์—ฌ๋Ÿฌ ํ˜•์‹ ๋™์‹œ ์ถœ๋ ฅ
golangci-lint run \
  --out-format "github-actions,json:lint-results.json"

# ์ƒ‰์ƒ ์—†๋Š” ์ถœ๋ ฅ (๋กœ๊ทธ ํŒŒ์ผ ์ €์žฅ์šฉ)
golangci-lint run --no-config --color never
๐Ÿ–ฅ IDE ํ†ตํ•ฉ

VSCode์—์„œ golangci-lint๋ฅผ ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜๋ ค๋ฉด Go ํ™•์žฅ(golang.go)์˜ ์„ค์ •์—์„œ:

VSCode settings.json
{
  "go.lintTool": "golangci-lint",
  "go.lintFlags": [
    "--fast",
    "--config=${workspaceFolder}/.golangci.yml"
  ],
  "go.lintOnSave": "package"
}

GoLand(JetBrains)์—์„œ๋Š” Settings โ†’ Go โ†’ Go Linter์—์„œ golangci-lint๋ฅผ ์„ ํƒํ•˜๋ฉด ๋ผ์š”. ์ €์žฅํ•  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ lint ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ค˜์š” ใ…‹ใ…‹ ์ง„์งœ ํŽธํ•ด์š”.


๐Ÿ† ์‹ค์ „ ํ”„๋กœ์ ํŠธ ์„ค์ • ์˜ˆ์‹œ (์™„์„ฑํ˜•)

์ง€๊ธˆ๊นŒ์ง€ ๋ฐฐ์šด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ด์„œ ์‹ค์ œ ํ”„๋กœ๋•์…˜ ํ”„๋กœ์ ํŠธ์— ์“ธ ์ˆ˜ ์žˆ๋Š” ์™„์„ฑํ˜• ์„ค์ •์„ ๋ณด์—ฌ๋“œ๋ฆด๊ฒŒ์š”. ์žฌ๋Šฅ๋„ท์ฒ˜๋Ÿผ ์‹ค์ œ ์„œ๋น„์Šค๋ฅผ ์šด์˜ํ•˜๋Š” ํŒ€์—์„œ ์“ธ ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ค€์ด์—์š”!

.golangci.yml (ํ”„๋กœ๋•์…˜ ์™„์„ฑํ˜•)
run:
  timeout: 10m
  go: '1.21'
  tests: true
  build-tags:
    - integration
  skip-dirs:
    - vendor
    - third_party
    - generated
  skip-files:
    - ".*\\.pb\\.go$"  # protobuf ์ƒ์„ฑ ํŒŒ์ผ ์ œ์™ธ
    - ".*\\.gen\\.go$"  # ์ž๋™ ์ƒ์„ฑ ํŒŒ์ผ ์ œ์™ธ

output:
  format: colored-line-number
  print-issued-lines: true
  print-linter-name: true
  sort-results: true

linters:
  enable:
    # ๋ฒ„๊ทธ ๊ฐ์ง€
    - errcheck
    - govet
    - staticcheck
    - ineffassign
    - unused
    # ์ฝ”๋“œ ์Šคํƒ€์ผ
    - gofmt
    - goimports
    - revive
    - misspell
    # ๋ณด์•ˆ
    - gosec
    - bodyclose
    - sqlclosecheck
    # ๋ณต์žก๋„
    - cyclop
    - funlen
    - gocognit
    # ์„ฑ๋Šฅ
    - prealloc
    # ์—๋Ÿฌ ์ฒ˜๋ฆฌ
    - wrapcheck
    - errorlint
    # ๊ธฐํƒ€
    - godot
    - whitespace

linters-settings:
  errcheck:
    check-type-assertions: true
    check-blank: true
  
  govet:
    enable-all: true
  
  cyclop:
    max-complexity: 10
    package-average: 0.0
    skip-tests: true
  
  funlen:
    lines: 100
    statements: 60
  
  gocognit:
    min-complexity: 20
  
  gosec:
    severity: medium
    confidence: medium
    excludes:
      - G115  # ํŠน์ • ํ™˜๊ฒฝ์—์„œ ๋ถˆํ•„์š”ํ•œ ๊ทœ์น™
  
  revive:
    min-confidence: 0.8
    rules:
      - name: exported
        severity: warning
      - name: var-naming
        severity: warning
      - name: package-comments
        severity: warning
  
  wrapcheck:
    ignoreSigs:
      - .Errorf(
      - errors.New(
      - errors.Unwrap(
  
  misspell:
    locale: US
    ignore-words:
      - someword
  
  goimports:
    local-prefixes: github.com/mycompany/myproject

issues:
  exclude-rules:
    # ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์˜ˆ์™ธ
    - path: _test\.go
      linters:
        - funlen
        - gocognit
        - cyclop
        - gosec
        - wrapcheck
    
    # main ํŒจํ‚ค์ง€ ์˜ˆ์™ธ
    - path: cmd/
      linters:
        - wrapcheck
    
    # ํŠน์ • ํŒจํ„ด ์ œ์™ธ
    - text: "weak cryptographic primitive"
      linters:
        - gosec
      path: internal/checksum/
  
  exclude-use-default: false
  max-issues-per-linter: 100
  max-same-issues: 20
  new: false
  fix: false
๐Ÿ’ก ์ด ์„ค์ •์€ ์‹œ์ž‘์ ์ด์—์š”. ํŒ€์˜ ์ฝ”๋”ฉ ์Šคํƒ€์ผ๊ณผ ํ”„๋กœ์ ํŠธ ํŠน์„ฑ์— ๋งž๊ฒŒ ์กฐ์ •ํ•˜๋Š” ๊ฒŒ ์ค‘์š”ํ•ด์š”. ์ฒ˜์Œ์—” max-issues-per-linter๋ฅผ ๋†’๊ฒŒ ์„ค์ •ํ•˜๊ณ , ์ ์ง„์ ์œผ๋กœ ์ด์Šˆ๋ฅผ ์ค„์—ฌ๋‚˜๊ฐ€๋Š” ๋ฐฉ์‹์„ ์ถ”์ฒœํ•ด์š”.

๐ŸŽ“ ๋งˆ๋ฌด๋ฆฌ: ์ฝ”๋“œ ํ’ˆ์งˆ์€ ์Šต๊ด€์ด์—์š”

golangci-lint๋Š” ๋„๊ตฌ์ผ ๋ฟ์ด์—์š”. ์ง„์งœ ์ค‘์š”ํ•œ ๊ฑด ์ฝ”๋“œ ํ’ˆ์งˆ์„ ์ง€์†์ ์œผ๋กœ ๊ฐœ์„ ํ•˜๋ ค๋Š” ํŒ€ ๋ฌธํ™”์˜ˆ์š”. lint ๊ฒฝ๊ณ ๋ฅผ ๋ฌด์‹œํ•˜๊ฑฐ๋‚˜ nolint๋กœ ๋„๋ฐฐํ•˜๋ฉด ์•„๋ฌด ์˜๋ฏธ ์—†๊ฑฐ๋“ ์š” ใ…‹ใ…‹

์ฒ˜์Œ์—” lint ๊ฒฝ๊ณ  ๊ณ ์น˜๋Š” ๊ฒŒ ๊ท€์ฐฎ๊ณ  ์‹œ๊ฐ„ ๋‚ญ๋น„์ฒ˜๋Ÿผ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์–ด์š”. ๊ทผ๋ฐ 6๊ฐœ์›”, 1๋…„ ์ง€๋‚˜๊ณ  ๋‚˜๋ฉด ํ™•์‹คํžˆ ๋А๊ปด์ ธ์š”. ๋ฒ„๊ทธ๊ฐ€ ์ค„๊ณ , ์ฝ”๋“œ ๋ฆฌ๋ทฐ๊ฐ€ ๋นจ๋ผ์ง€๊ณ , ์ƒˆ ํŒ€์›์ด ์ฝ”๋“œ ์ดํ•ดํ•˜๋Š” ์‹œ๊ฐ„์ด ์ค„์–ด๋“ค์–ด์š”. ์ด๊ฒŒ ์ง„์งœ ์ƒ์‚ฐ์„ฑ ํ–ฅ์ƒ์ด์—์š”.

โœ… ์˜ค๋Š˜ ๋‹น์žฅ ํ•  ๊ฒƒ

1. golangci-lint ์„ค์น˜
2. ํ”„๋กœ์ ํŠธ์— ๊ธฐ๋ณธ ์„ค์ • ํŒŒ์ผ ์ƒ์„ฑ
3. ๊ธฐ๋ณธ linter๋กœ ํ˜„ํ™ฉ ํŒŒ์•…
4. ๊ฐ€์žฅ ์‹ฌ๊ฐํ•œ ์ด์Šˆ๋ถ€ํ„ฐ ์ˆ˜์ •
๐Ÿ“… ์ด๋ฒˆ ์ฃผ ํ•  ๊ฒƒ

1. CI/CD ํŒŒ์ดํ”„๋ผ์ธ ํ†ตํ•ฉ
2. pre-commit hook ์„ค์ •
3. ํŒ€ ์ปจ๋ฒค์…˜ ๋ฌธ์„œํ™”
4. IDE ํ†ตํ•ฉ ์„ค์ •
๐Ÿš€ ์ด๋ฒˆ ๋‹ฌ ํ•  ๊ฒƒ

1. ๋ณด์•ˆ linter ์ถ”๊ฐ€
2. ๋ณต์žก๋„ ์ง€ํ‘œ ๋ชจ๋‹ˆํ„ฐ๋ง
3. ํŒ€ ๋ฆฌ๋ทฐ ํ”„๋กœ์„ธ์Šค ๊ฐœ์„ 
4. ์ปค์Šคํ…€ linter ๊ฒ€ํ† 

Go ์–ธ์–ด์˜ ์ฒ ํ•™์€ "๋ช…ํ™•ํ•˜๊ณ  ๋‹จ์ˆœํ•œ ์ฝ”๋“œ"์˜ˆ์š”. golangci-lint๋Š” ๊ทธ ์ฒ ํ•™์„ ์‹ค์ฒœํ•˜๋Š” ๊ฐ€์žฅ ํšจ๊ณผ์ ์ธ ๋„๊ตฌ์˜ˆ์š”. ์ง€๊ธˆ ๋ฐ”๋กœ ์‹œ์ž‘ํ•ด๋ด์š”! ์ฒ˜์Œ์—” ๊ฒฝ๊ณ  ํญํƒ„ ๋งž๊ฒ ์ง€๋งŒ ใ…‹ใ…‹ ๊ทธ๊ฒŒ ์„ฑ์žฅ์˜ ์‹œ์ž‘์ด์—์š”.

๐Ÿ”ต ๋งˆ์ง€๋ง‰ ํŒ! golangci-lint ๊ณต์‹ ๋ฌธ์„œ(golangci-lint.run)๋Š” ์ •๋ง ์ž˜ ๋˜์–ด ์žˆ์–ด์š”. ๊ฐ linter์˜ ์ƒ์„ธ ์„ค๋ช…๊ณผ ์˜ˆ์ œ๊ฐ€ ์žˆ์œผ๋‹ˆ ๊ผญ ์ฐธ๊ณ ํ•ด์š”. ๊ทธ๋ฆฌ๊ณ  Go ๊ฐœ๋ฐœ ๊ด€๋ จ ๋„์›€์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์žฌ๋Šฅ๋„ท์—์„œ Go ์ „๋ฌธ ๊ฐœ๋ฐœ์ž๋ฅผ ์ฐพ์•„๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—์š”!
๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0