웹 서버 개발 고수 팁
Graceful Shutdown
서버를 갑작스럽게 종료하면 진행 중인 요청이 끊길 수 있습니다. http.Server.Shutdown()을 사용하면 새 요청은 받지 않으면서 진행 중인 요청이 완료될 때까지 기다립니다.
package main
import (
"context"
"log"
"net/http"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 느린 요청 시뮬레이션
time.Sleep(2 * time.Second)
w.Write([]byte("완료"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
// OS 시그널 수신 (SIGINT: Ctrl+C, SIGTERM: docker stop)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// 백그라운드에서 서버 시작
go func() {
log.Println("서버 시작: :8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("서버 오류: %v", err)
}
}()
// 시그널 대기
<-ctx.Done()
log.Println("종료 시그널 수신, graceful shutdown 시작...")
// 30초 타임아웃 내에 진행 중인 요청 처리 완료
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("강제 종료: %v", err)
}
log.Println("서버 정상 종료")
}
프레임워크 선택 기준
| 상황 | 추천 |
|---|---|
| 표준 라이브러리만 사용하고 싶다 | net/http |
| 최고 성능이 필요하다 (라우팅 오버헤드 최소화) | Gin |
| 미들웨어 커스터마이징이 많다 | Echo |
| 마이크로서비스, 의존성 최소화 | net/http 또는 Chi |
| REST + gRPC 혼용 | Connect-Go |
// net/http (Go 1.22+) - 패턴 기반 라우팅 내장
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", handleGetUser) // 메서드+경로 패턴
mux.HandleFunc("POST /users", handleCreateUser)
타임아웃 설정
srv := &http.Server{
// 클라이언트가 요청 헤더를 보내는 데 허용되는 최대 시간
ReadHeaderTimeout: 5 * time.Second,
// 요청 전체(바디 포함)를 읽는 최대 시간
ReadTimeout: 10 * time.Second,
// 응답을 쓰는 최대 시간 (스트리밍 API는 길게 설정)
WriteTimeout: 30 * time.Second,
// Keep-Alive 연결의 최대 유휴 시간
IdleTimeout: 120 * time.Second,
// 요청 헤더 최대 크기 (기본 1MB)
MaxHeaderBytes: 1 << 20,
}
요청 크기 제한
func limitBodyMiddleware(maxBytes int64) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 바디 크기 제한 (초과 시 읽기 오류 발생)
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
next.ServeHTTP(w, r)
})
}
}
// 10MB 제한
handler := limitBodyMiddleware(10 << 20)(mux)
보안 헤더 미들웨어
func securityHeadersMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// MIME 타입 스니핑 방지
w.Header().Set("X-Content-Type-Options", "nosniff")
// 클릭재킹 방지
w.Header().Set("X-Frame-Options", "DENY")
// XSS 필터 활성화 (구형 브라우저)
w.Header().Set("X-XSS-Protection", "1; mode=block")
// HTTPS 강제 (1년)
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
// CSP: 같은 출처 스크립트만 허용
w.Header().Set("Content-Security-Policy", "default-src 'self'")
// Referrer 정책
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
next.ServeHTTP(w, r)
})
}
헬스체크 엔드포인트 패턴
package main
import (
"encoding/json"
"net/http"
"time"
)
type HealthStatus struct {
Status string `json:"status"` // "ok", "degraded", "down"
Timestamp time.Time `json:"timestamp"`
Checks map[string]string `json:"checks"`
}
// /health - 기본 생존 확인 (로드밸런서용)
func livenessHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
// /ready - 실제 서비스 가능 여부 (DB, 캐시 연결 포함)
func readinessHandler(w http.ResponseWriter, r *http.Request) {
checks := map[string]string{}
status := "ok"
// DB 연결 확인 (예시)
// if err := db.Ping(); err != nil {
// checks["database"] = "down: " + err.Error()
// status = "down"
// } else {
// checks["database"] = "ok"
// }
checks["database"] = "ok"
code := http.StatusOK
if status == "down" {
code = http.StatusServiceUnavailable
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(HealthStatus{
Status: status,
Timestamp: time.Now(),
Checks: checks,
})
}
// /metrics - Prometheus 형식 메트릭 (prometheus/client_golang 사용)
// promhttp.Handler() 를 등록하면 자동으로 제공됨
GOMAXPROCS 및 커넥션 풀 튜닝
import (
"runtime"
_ "go.uber.org/automaxprocs" // 컨테이너 CPU 쿼터 자동 반영
)
func init() {
// 컨테이너 환경에서는 automaxprocs가 자동으로 설정
// 명시적으로 설정할 경우:
// runtime.GOMAXPROCS(runtime.NumCPU())
_ = runtime.NumCPU()
}
// HTTP 클라이언트 커넥션 풀
transport := &http.Transport{
MaxIdleConns: 100, // 전체 최대 유휴 연결
MaxIdleConnsPerHost: 10, // 호스트당 최대 유휴 연결
MaxConnsPerHost: 50, // 호스트당 최대 연결
IdleConnTimeout: 90 * time.Second,
DisableCompression: false,
}