Learning
레슨 9 / 10·4개 토픽

HTTP 서버와 Context

net/http 기본 서버

Go의 net/http 패키지는 HTTP 서버를 쉽게 구축할 수 있게 해줍니다. http.HandleFunc으로 라우트를 등록하고, http.ListenAndServe로 서버를 시작합니다. 외부 프레임워크 없이도 프로덕션급 서버를 만들 수 있는 것이 Go의 강점입니다.

go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type Response struct {
    Message string `json:"message"`
    Status  int    `json:"status"`
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "World"
    }

    resp := Response{
        Message: fmt.Sprintf("Hello, %s!", name),
        Status:  200,
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    fmt.Println("서버 시작: http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

http.Handler 인터페이스와 미들웨어

go
package main

import (
    "log"
    "net/http"
    "time"
)

// 미들웨어 — 요청 로깅
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("[%s] %s 시작", r.Method, r.URL.Path)
        next.ServeHTTP(w, r) // 다음 핸들러 호출
        log.Printf("[%s] %s 완료 (%v)", r.Method, r.URL.Path, time.Since(start))
    })
}

// 미들웨어 — CORS 헤더
func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// http.Handler 인터페이스 직접 구현
type apiHandler struct{}

func (h *apiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("API 응답"))
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/api", &apiHandler{})

    // 미들웨어 체이닝
    handler := loggingMiddleware(corsMiddleware(mux))
    log.Fatal(http.ListenAndServe(":8080", handler))
}

context 패키지

context 패키지는 API 경계와 고루틴 간에 취소 신호, 타임아웃, 요청 범위 값을 전달합니다. HTTP 요청 처리, 데이터베이스 쿼리, 외부 API 호출 등에서 타임아웃과 취소를 관리하는 데 필수적입니다.

go
package main

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

// WithCancel — 수동 취소
func withCancelExample() {
    ctx, cancel := context.WithCancel(context.Background())

    go func() {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("작업 취소됨:", ctx.Err())
                return
            default:
                fmt.Println("작업 중...")
                time.Sleep(500 * time.Millisecond)
            }
        }
    }()

    time.Sleep(2 * time.Second)
    cancel() // 고루틴에 취소 신호 전달
    time.Sleep(100 * time.Millisecond)
}

// WithTimeout — 자동 타임아웃
func fetchData(ctx context.Context, query string) (string, error) {
    select {
    case <-time.After(3 * time.Second): // 느린 작업 시뮬레이션
        return "결과: " + query, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

// WithValue — 요청 범위 값 전달
func processRequest(ctx context.Context) {
    userID := ctx.Value("userID").(string)
    fmt.Printf("사용자 %s의 요청 처리 중\n", userID)
}

func main() {
    // 2초 타임아웃
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    result, err := fetchData(ctx, "SELECT * FROM users")
    if err != nil {
        fmt.Println("타임아웃:", err) // context deadline exceeded
    } else {
        fmt.Println(result)
    }

    // WithValue 사용
    valCtx := context.WithValue(context.Background(), "userID", "user-123")
    processRequest(valCtx)
}

Graceful Shutdown 패턴

go
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "서버 동작 중")
    })

    server := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    // 서버를 고루틴에서 시작
    go func() {
        log.Println("서버 시작: :8080")
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("서버 에러: %v", err)
        }
    }()

    // 종료 시그널 대기 (Ctrl+C, kill)
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("종료 시그널 수신, 정상 종료 시작...")

    // 5초 이내에 진행 중인 요청 완료
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("강제 종료: %v", err)
    }
    log.Println("서버 정상 종료 완료")
}
  • http.HandleFunc() — 경로에 핸들러 함수를 등록
  • http.ListenAndServe() — HTTP 서버를 시작하고 요청 대기
  • http.Handler 인터페이스 — ServeHTTP 메서드를 구현하여 커스텀 핸들러 생성
  • 미들웨어 패턴 — func(http.Handler) http.Handler로 핸들러를 래핑
  • context.WithCancel() — 수동으로 취소 신호를 보낼 수 있는 컨텍스트
  • context.WithTimeout() — 지정 시간 후 자동으로 취소되는 컨텍스트
  • context.WithValue() — 요청 범위 값을 컨텍스트에 첨부
💡

프로덕션 서버에서는 반드시 Graceful Shutdown을 구현하세요. 진행 중인 요청이 갑자기 끊기는 것을 방지합니다. context.WithValue는 요청 ID, 인증 정보 등 요청 범위 메타데이터에만 사용하고, 함수 매개변수를 대체하는 용도로 사용하지 마세요.