🚀 요약
SUMMARY
Deck을 Go로 시작하면서 프로젝트 기본기부터 박았다. 포맷·린트·보안 린트(gosec)·취약점 스캔(govulncheck)·민감정보 마스킹을 전부
make타깃으로 두고 CI에서 상시 돌린다. 핵심 결정 둘 — 보안 린트 예외는 무작정nolint로 끄지 않고 “위험이 실제로 사는 경계 파일 하나”에만 국소화했고, 취약점은 도달성까지 보는 govulncheck로 노이즈를 줄였다.
기능을 붙이기 전에, 기능이 뭘 잘못해도 걸러지는 그물부터 깔았다.
1. 기본기를 make 타깃 하나로
로컬에서 하는 검사와 CI가 어긋나는 게 싫어서, 검사 종류를 전부 make 타깃으로 뒀다.
make build # 빌드 (버전 ldflags 주입)
make test # go test ./...
make lint # golangci-lint run
make vuln # govulncheck ./...
make generate # 스키마·CLI 문서 생성
make verify-generated # 생성물이 최신인지 (git diff --exit-code)GitHub Actions에선 이걸 그대로 부르기만 한다(run: make lint 식). 그래서 “CI에서만 실패하는” 상황이 거의 없다. 내 노트북에서 make lint && make vuln이면 CI도 같은 결과다.
2. 포맷과 린트: golangci-lint
린트는 golangci-lint로 몬다. 방침은 default: none + 켤 것만 명시다. 기본값에 끌려다니지 않고, 켠 린터가 무엇인지 설정만 봐도 안다.
linters:
default: none
enable:
- gosec # 보안
- errcheck # 에러 무시 금지
- govet
- staticcheck
- bodyclose # HTTP body 안 닫는 것
- nilerr # nil 인데 err 반환 같은 실수
- ineffassign
- misspell
- gocritic
- gochecknoinits # init() 남용 금지
# ...
formatters:
enable: [gci, gofumpt] # 임포트 정렬 + 엄격 포맷포맷은 gofumpt(gofmt보다 엄격)와 gci(임포트를 표준 → 서드파티 → 자기 모듈 순으로 정렬)로 강제한다. 포맷 논쟁이 없어지는 건 Go의 gofmt 문화 그대로다.
3. gosec 예외설정: 위험을 경계 하나에 가둔다
보안 린터 gosec가 잡는 걸 다루는 방식이 이 프로젝트에서 제일 신경 쓴 부분이다. 흔한 실수는 걸릴 때마다 그 줄에 //nolint를 붙이는 건데, 그러면 위험한 패턴이 코드 전체에 흩뿌려지고 예외도 같이 흩어진다.
그래서 반대로 갔다. 위험한 동작을 한 곳(검토된 경계 파일)에 몰아넣고, gosec 예외도 그 파일에만 둔다. .golangci.yml의 exclusions가 그 지도다.
- 외부 명령 실행(G204) →
internal/executil에서만. 명령을 실행하는 코드가 feature 여기저기 흩어지지 않고 executil을 거치게 강제된다. - 동적 파일 접근(G304) →
internal/fsutil/safe_paths.go등 저수준 헬퍼에만. 경로를 받아 파일을 여는 위험은 여기로 모인다. - 호스트 경로 변경(G703) →
internal/hostfs/host_path.go에만. 호스트를 실제로 건드리는 신뢰 경계가 한 파일이다. - 템플릿 응답 sink(G705), 디렉토리 순회(G122)도 같은 식으로 리뷰된 헬퍼 하나에만 예외.
효과가 좋았다. 예외 목록이 곧 “이 프로젝트가 위험한 짓을 하는 지점 목록”이 된다. 새 feature 코드에서 gosec가 뜨면 “그 위험을 경계 헬퍼로 옮기라”는 신호다. (테스트 파일은 shell-out·임시 픽스처가 정상이라 G(122|204|301|302|304|306|703)를 테스트 경계에서만 제외한다.)
4. govulncheck: 도달성까지 보는 취약점 스캔
의존성 취약점은 govulncheck로 본다(make vuln = govulncheck ./...). 이게 일반 스캐너와 다른 점은 “취약한 의존성이 있냐”가 아니라 “그 취약 함수를 실제로 호출하는 경로가 있냐”까지 본다는 것이다. import만 하고 안 부르는 함수의 CVE는 걸러지니, 대응해야 할 목록이 짧고 진짜다. CI의 상시 보안 잡으로 돌려서, 취약한 의존성이 PR 단계에서 막힌다.
(컨테이너 이미지 쪽 CVE는 결이 다른 문제라 따로 정리했다. 여기선 내가 짠 Go 코드와 그 의존성만 다룬다.)
5. 민감정보 노출 막기: secretmask
폐쇄망 도구라 로그·출력에 자격증명이 섞여 나가면 그 자체가 사고다. 그래서 출력 스트림에서 알려진 시크릿 문자열을 ***로 가리는 층(internal/secretmask)을 뒀다. 문자열이 쓰기 청크 경계에 걸쳐 잘려도(token=super- + secret) 이어붙여 잡도록 만들었다.
이건 AI 보조 기능(ask)에서 특히 중요했다. 모델에 컨텍스트를 넘기다 토큰이 같이 새는 걸 막는 마지막 그물이다.
남은 것
이 그물(포맷·린트·gosec·govulncheck·마스킹)을 CI 게이트로 깔아두니, 기능을 붙일 때 “이게 뭘 망가뜨리나”를 사람이 매번 안 봐도 된다. 특히 gosec 예외를 경계 파일에 가둔 방식은, 프로젝트가 커져도 위험 지점 목록이 흩어지지 않게 잡아줬다. 다음은 릴리스 아티팩트에 SBOM·서명을 붙여 공급망 쪽까지 넓히는 것 정도가 숙제다.