본문으로 건너뛰기

Go 실전 팁 — 프로덕션 Go 애플리케이션

Go 시니어 개발자들이 실제 프로덕션 환경에서 터득한 패턴, 안티패턴, 그리고 Go답게 코드를 작성하는 핵심 원칙을 정리합니다.


에러 처리 고급 패턴

센티넬 에러 vs 커스텀 에러 타입

package main

import (
"errors"
"fmt"
)

// 1. 센티넬 에러 — 비교 가능한 에러 값
var (
ErrNotFound = errors.New("리소스를 찾을 수 없음")
ErrUnauthorized = errors.New("권한 없음")
ErrInvalidInput = errors.New("잘못된 입력")
)

// 2. 커스텀 에러 타입 — 추가 컨텍스트 포함
type ValidationError struct {
Field string
Value interface{}
Message string
}

func (e *ValidationError) Error() string {
return fmt.Sprintf("유효성 검사 실패: 필드=%s, 값=%v, 메시지=%s",
e.Field, e.Value, e.Message)
}

// 3. 에러 래핑 — 컨텍스트 보존
func getUser(id int) error {
if id <= 0 {
return fmt.Errorf("getUser: %w", &ValidationError{
Field: "id",
Value: id,
Message: "양수여야 합니다",
})
}
// DB 조회 실패 시뮬레이션
return fmt.Errorf("getUser: DB 조회 실패 (id=%d): %w", id, ErrNotFound)
}

func processUser(id int) error {
if err := getUser(id); err != nil {
return fmt.Errorf("processUser: %w", err) // 스택처럼 래핑
}
return nil
}

func main() {
err := processUser(-1)
if err != nil {
fmt.Println("에러:", err)

// errors.Is: 에러 체인에서 센티넬 에러 탐색
if errors.Is(err, ErrNotFound) {
fmt.Println("→ 리소스 없음")
}

// errors.As: 에러 체인에서 타입 탐색
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("→ 유효성 오류: 필드=%s\n", valErr.Field)
}
}
}

에러 그룹 — 여러 고루틴의 에러 수집

package main

import (
"context"
"fmt"

"golang.org/x/sync/errgroup"
)

func fetchData(ctx context.Context, url string) (string, error) {
// HTTP 요청 시뮬레이션
if url == "" {
return "", fmt.Errorf("빈 URL")
}
return "data from " + url, nil
}

func main() {
ctx := context.Background()

// errgroup: 여러 고루틴 에러를 한 번에 처리
g, ctx := errgroup.WithContext(ctx)

urls := []string{
"https://api1.example.com",
"https://api2.example.com",
"", // 에러 발생
}

results := make([]string, len(urls))

for i, url := range urls {
i, url := i, url // 루프 변수 캡처
g.Go(func() error {
data, err := fetchData(ctx, url)
if err != nil {
return fmt.Errorf("URL[%d] 실패: %w", i, err)
}
results[i] = data
return nil
})
}

// 모든 고루틴 완료 대기 — 첫 번째 에러 반환
if err := g.Wait(); err != nil {
fmt.Println("에러 발생:", err)
return
}

fmt.Println("모든 결과:", results)
}

컨텍스트 전파 패턴

package main

import (
"context"
"fmt"
"time"
)

// 컨텍스트 키 타입 — 충돌 방지
type contextKey string

const (
requestIDKey contextKey = "requestID"
userIDKey contextKey = "userID"
)

// 미들웨어: 컨텍스트에 값 주입
func withRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}

func withUserID(ctx context.Context, id int64) context.Context {
return context.WithValue(ctx, userIDKey, id)
}

// 헬퍼 함수로 타입 안전하게 추출
func getRequestID(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey).(string); ok {
return id
}
return "unknown"
}

func getUserID(ctx context.Context) int64 {
if id, ok := ctx.Value(userIDKey).(int64); ok {
return id
}
return 0
}

// 컨텍스트 취소 전파
func longRunningTask(ctx context.Context) error {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled 또는 context.DeadlineExceeded
default:
fmt.Printf("[reqID=%s] 진행 중: %d/10\n",
getRequestID(ctx), i+1)
time.Sleep(100 * time.Millisecond)
}
}
return nil
}

func main() {
// 요청 컨텍스트 구성
ctx := context.Background()
ctx = withRequestID(ctx, "req-12345")
ctx = withUserID(ctx, 42)

// 타임아웃 추가
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()

if err := longRunningTask(ctx); err != nil {
fmt.Printf("작업 중단: %v\n", err) // context deadline exceeded
}
}

인터페이스 설계 원칙

package main

import (
"fmt"
"io"
"os"
"strings"
)

// Go 인터페이스 황금률: 작게, 필요할 때만

// 나쁜 예: 과도하게 큰 인터페이스
type BadStorage interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
Delete(key string) error
List() ([]string, error)
Flush() error
Stats() map[string]int
Close() error
Ping() error
}

// 좋은 예: 작은 인터페이스 조합
type Getter interface {
Get(key string) ([]byte, error)
}

type Setter interface {
Set(key string, val []byte) error
}

type ReadWriter interface {
Getter
Setter
}

// 표준 라이브러리처럼 io.Reader 활용
func processInput(r io.Reader) (int, error) {
buf := make([]byte, 1024)
total := 0
for {
n, err := r.Read(buf)
total += n
if err == io.EOF {
break
}
if err != nil {
return total, err
}
}
return total, nil
}

func main() {
// 파일, 문자열, 네트워크 등 모두 io.Reader로 통일
fileReader, _ := os.Open("go.mod")
defer fileReader.Close()
n1, _ := processInput(fileReader)
fmt.Printf("파일에서 %d 바이트 읽음\n", n1)

stringReader := strings.NewReader("hello world")
n2, _ := processInput(stringReader)
fmt.Printf("문자열에서 %d 바이트 읽음\n", n2)
}

동시성 패턴 심화

Fan-Out / Fan-In 파이프라인

package main

import (
"fmt"
"sync"
)

// 제너레이터 — 채널 소스
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for _, n := range nums {
out <- n
}
}()
return out
}

// 스테이지 — 변환 처리
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
out <- n * n
}
}()
return out
}

// Fan-Out — 여러 워커에 분산
func fanOut(in <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = square(in)
}
return channels
}

// Fan-In — 여러 채널 합치기
func fanIn(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup

for _, ch := range channels {
ch := ch
wg.Add(1)
go func() {
defer wg.Done()
for v := range ch {
out <- v
}
}()
}

go func() {
wg.Wait()
close(out)
}()
return out
}

func main() {
// 파이프라인: 생성 → Fan-Out(4워커) → Fan-In → 출력
nums := generate(1, 2, 3, 4, 5, 6, 7, 8)
results := fanIn(fanOut(nums, 4)...)

for v := range results {
fmt.Printf("%d ", v)
}
fmt.Println()
}

Done 채널 패턴 (취소)

package main

import (
"fmt"
"time"
)

// 취소 가능한 작업자
func worker(done <-chan struct{}, id int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := 0; ; i++ {
select {
case <-done:
fmt.Printf("워커 %d 종료\n", id)
return
case out <- i:
time.Sleep(10 * time.Millisecond)
}
}
}()
return out
}

func main() {
done := make(chan struct{})

w1 := worker(done, 1)
w2 := worker(done, 2)

// 300ms 동안 결과 수집
timeout := time.After(300 * time.Millisecond)
for {
select {
case v := <-w1:
fmt.Printf("w1: %d\n", v)
case v := <-w2:
fmt.Printf("w2: %d\n", v)
case <-timeout:
close(done) // 모든 워커 취소
time.Sleep(50 * time.Millisecond) // 정리 대기
fmt.Println("완료")
return
}
}
}

설정 관리 모범 사례

// config/config.go
package config

import (
"fmt"
"os"
"strconv"
"time"
)

type Config struct {
Server ServerConfig
Database DatabaseConfig
Cache CacheConfig
}

type ServerConfig struct {
Host string
Port int
ReadTimeout time.Duration
WriteTimeout time.Duration
}

type DatabaseConfig struct {
DSN string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
}

type CacheConfig struct {
RedisURL string
TTL time.Duration
}

// 환경변수에서 로드 (기본값 포함)
func Load() (*Config, error) {
cfg := &Config{
Server: ServerConfig{
Host: getEnv("SERVER_HOST", "0.0.0.0"),
Port: getEnvInt("SERVER_PORT", 8080),
ReadTimeout: getEnvDuration("SERVER_READ_TIMEOUT", 15*time.Second),
WriteTimeout: getEnvDuration("SERVER_WRITE_TIMEOUT", 15*time.Second),
},
Database: DatabaseConfig{
DSN: mustGetEnv("DATABASE_URL"),
MaxOpenConns: getEnvInt("DB_MAX_OPEN_CONNS", 25),
MaxIdleConns: getEnvInt("DB_MAX_IDLE_CONNS", 5),
ConnMaxLifetime: getEnvDuration("DB_CONN_MAX_LIFETIME", 5*time.Minute),
},
Cache: CacheConfig{
RedisURL: getEnv("REDIS_URL", "redis://localhost:6379"),
TTL: getEnvDuration("CACHE_TTL", 5*time.Minute),
},
}
return cfg, nil
}

func getEnv(key, defaultVal string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultVal
}

func mustGetEnv(key string) string {
val := os.Getenv(key)
if val == "" {
panic(fmt.Sprintf("필수 환경변수 없음: %s", key))
}
return val
}

func getEnvInt(key string, defaultVal int) int {
if val := os.Getenv(key); val != "" {
if n, err := strconv.Atoi(val); err == nil {
return n
}
}
return defaultVal
}

func getEnvDuration(key string, defaultVal time.Duration) time.Duration {
if val := os.Getenv(key); val != "" {
if d, err := time.ParseDuration(val); err == nil {
return d
}
}
return defaultVal
}

구조화된 로깅 — slog (Go 1.21+)

package main

import (
"context"
"log/slog"
"os"
"time"
)

func setupLogger(env string) *slog.Logger {
var handler slog.Handler

if env == "production" {
// JSON 형식 (로그 수집기 친화적)
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
} else {
// 텍스트 형식 (개발 중 가독성)
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
AddSource: true, // 파일:라인 추가
})
}

return slog.New(handler)
}

func main() {
logger := setupLogger("development")

// 전역 로거 설정
slog.SetDefault(logger)

// 기본 로깅
slog.Info("서버 시작", "port", 8080, "env", "development")
slog.Debug("디버그 정보", "goroutines", 10)
slog.Warn("느린 쿼리", "duration_ms", 250, "query", "SELECT *")
slog.Error("DB 연결 실패", "error", "connection refused", "retry", 3)

// 구조화된 그룹
slog.Info("요청 처리",
slog.Group("request",
slog.String("method", "GET"),
slog.String("path", "/users/123"),
slog.String("ip", "192.168.1.1"),
),
slog.Group("response",
slog.Int("status", 200),
slog.Duration("duration", 45*time.Millisecond),
),
)

// 컨텍스트와 함께 (요청 ID 전파)
ctx := context.WithValue(context.Background(), "reqID", "abc-123")
logger.InfoContext(ctx, "사용자 조회", "user_id", 42)

// 공통 필드가 있는 로거 파생
requestLogger := logger.With(
"request_id", "req-789",
"user_id", 100,
)
requestLogger.Info("쿼리 시작")
requestLogger.Info("쿼리 완료", "rows", 5)
}

코드 품질 도구 체인

# go vet — 공식 정적 분석
go vet ./...

# golangci-lint — 다수의 린터 통합 (권장)
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run ./...

# staticcheck — 고급 정적 분석
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./...

# govulncheck — 보안 취약점 검사
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

# gofumpt — gofmt의 엄격한 버전
go install mvdan.cc/gofumpt@latest
gofumpt -l -w .

# deadcode — 미사용 코드 탐지
go install golang.org/x/tools/cmd/deadcode@latest
deadcode -test ./...
# .golangci.yml
run:
timeout: 5m

linters:
enable:
- errcheck # 에러 처리 누락
- gosimple # 단순화 가능한 코드
- govet # go vet
- ineffassign # 비효율적 할당
- staticcheck # 정적 분석
- unused # 미사용 코드
- gofmt # 포맷 검사
- goimports # import 정렬
- misspell # 오타 검사
- exhaustive # switch 완전성 검사

linters-settings:
errcheck:
check-type-assertions: true
govet:
enable-all: true

모듈과 의존성 관리

# 모듈 초기화
go mod init github.com/myorg/myapp

# 의존성 추가
go get github.com/gin-gonic/gin@v1.9.1
go get golang.org/x/sync@latest

# 사용하지 않는 의존성 제거
go mod tidy

# 벤더링 (오프라인/재현 가능 빌드)
go mod vendor

# 의존성 보안 감사
go list -m all | govulncheck -

# 의존성 트리 확인
go mod graph | head -20

# 특정 패키지 왜 필요한지 확인
go mod why github.com/some/package

# 의존성 업그레이드
go get -u ./... # 모든 의존성 업그레이드
go get -u=patch ./... # 패치 버전만 업그레이드
go get github.com/gin-gonic/gin # 최신 버전으로 업그레이드

프로덕션 체크리스트

배포 전 필수 확인 항목:

빌드 & 테스트
✅ go test -race ./... — 레이스 컨디션 탐지
✅ go vet ./... — 정적 분석
✅ golangci-lint run ./... — 린트 통과
✅ govulncheck ./... — 보안 취약점 없음
✅ go test -cover (≥80%) — 테스트 커버리지

코드 품질
✅ 에러 처리: errors.Is/As 사용, fmt.Errorf("%w") 래핑
✅ 컨텍스트 전파: 모든 API 함수 첫 번째 매개변수 context.Context
✅ 리소스 해제: defer로 Close/Cancel 보장
✅ 취소 가능: 모든 장기 실행 작업 ctx.Done() 처리

설정 & 보안
✅ 시크릿 환경변수 처리 (하드코딩 없음)
✅ 타임아웃 설정 (HTTP 클라이언트/서버 모두)
✅ rate limiting 적용
✅ 입력 유효성 검사

운영
✅ 구조화 로깅 (JSON, slog)
✅ 헬스체크 엔드포인트 (/health, /ready)
✅ 그레이스풀 셧다운 (SIGTERM 처리)
✅ 메트릭 노출 (Prometheus /metrics)
✅ 리소스 제한 설정 (GOMEMLIMIT)

핵심 정리

영역권장 패턴
에러fmt.Errorf("%w") 래핑, errors.Is/As 검사
컨텍스트첫 번째 매개변수, 타임아웃/취소 전파
인터페이스작고 집중된 인터페이스, 소비자가 정의
설정환경변수 + 기본값, 시작 시 검증
로깅log/slog JSON 핸들러 (Go 1.21+)
동시성errgroup, 워커 풀, done 채널 패턴
코드 품질golangci-lint, govulncheck, -race 테스트
  • Go 격언: "Clear is better than clever" — 영리한 코드보다 명확한 코드
  • 컨텍스트 규칙: 함수 시그니처에 ctx를 저장하지 말고 항상 전달
  • 인터페이스 규칙: 구현자가 아닌 소비자(사용하는 쪽)가 인터페이스를 정의