본문으로 건너뛰기

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를 저장하지 말고 항상 전달
  • 인터페이스 규칙: 구현자가 아닌 소비자(사용하는 쪽)가 인터페이스를 정의