고 실전 #1 첫 HTTP 서버
Go 실전 시리즈 시작. 지금까지의 기초/중급/고급에서 본 도구들이 — 실제 백엔드를 짤 때 어떻게 조립되는지를 6 편에 걸쳐 정리합니다.
- 첫 HTTP 서버 (이 글) — net/http, ListenAndServe, graceful shutdown
- 라우팅 — Go 1.22+ ServeMux 패턴 매칭
- JSON 입출력과 입력 검증
- DB 연동 —
database/sql, 트랜잭션 - 미들웨어 패턴
- 테스트와 배포 —
httptest, Docker
Go가 백엔드에서 강한 가장 큰 이유 — 표준 net/http만으로 프로덕션 서버가 가능하다는 점. 이 글은 그 시작입니다.
가장 작은 서버 #
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, world!")
})
http.ListenAndServe(":8080", nil)
}go run hello.go
curl http://localhost:8080
# Hello, world!19 줄(공백 제외 7 줄). 외부 의존 0. Express, FastAPI 같은 프레임워크의 도움 없이 — 표준 라이브러리만으로.
세 가지 핵심 타입 #
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
type Request struct {
Method, URL, Header, Body, ...
}Handler— 모든 요청 처리기의 공통 인터페이스ResponseWriter— 응답 작성Request— 요청 정보
이 셋이 — Go HTTP 코드의 기본 구성 요소입니다.
HandleFunc vs Handler
#
// 함수로 등록
http.HandleFunc("/a", func(w, r) { ... })
// Handler 인터페이스로 등록
type myHandler struct{}
func (h *myHandler) ServeHTTP(w, r) { ... }
http.Handle("/b", &myHandler{})대부분은 — HandleFunc가 간결해서 자주 쓰입니다. 상태(필드)가 필요한 핸들러는 — 구조체 + ServeHTTP.
HandleFunc는 — 내부적으로 http.HandlerFunc 어댑터로 함수를 Handler로 만듭니다(어댑터 패턴의 정석 사례).
응답 쓰기 #
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") // 1) 헤더 먼저
w.WriteHeader(http.StatusOK) // 2) 상태 코드
fmt.Fprintln(w, `{"ok":true}`) // 3) 바디
}순서가 중요합니다:
Header().Set은WriteHeader또는Write가 불리기 전에- 그 후 헤더를 바꾸면 효과 없음
WriteHeader를 안 부르면 — 첫 Write 시점에 자동으로 200.
URL/Query 파라미터 #
func handler(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
name := q.Get("name") // ?name=...
page := q.Get("page")
fmt.Fprintf(w, "Hello %s on page %s", name, page)
}curl 'http://localhost:8080/?name=lee&page=2'
# Hello lee on page 2Query()가 — url.Values (map[string][]string)를 돌려줍니다. 같은 키가 여러 번 등장할 수 있는 점에 주의.
메서드 분기 #
func handler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// 조회
case http.MethodPost:
// 생성
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}Go 1.22+ 부터는 — ServeMux 자체가 메서드 매칭을 지원합니다. 다음 글에서 자세히.
요청 바디 읽기 #
func handler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read failed", http.StatusBadRequest)
return
}
defer r.Body.Close()
fmt.Fprintf(w, "got %d bytes", len(body))
}r.Body는 — io.ReadCloser. JSON 같은 구조화된 입력은 #3 JSON에서 자세히.
바디 크기 제한 #
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
body, err := io.ReadAll(r.Body)악의적 큰 요청을 막기 위해 — 항상 한도를 두는 게 좋습니다.
상태 코드 — 표준 상수 #
http.StatusOK // 200
http.StatusCreated // 201
http.StatusBadRequest // 400
http.StatusUnauthorized // 401
http.StatusNotFound // 404
http.StatusInternalServerError // 500매직 넘버 대신 상수로. IDE 자동완성과 가독성 모두 좋아집니다.
http.Error 헬퍼
#
http.Error(w, "not found", http.StatusNotFound)내부적으로 — 헤더 설정 + 상태 코드 + 메시지를 한 번에 정리합니다. 단순한 에러 응답에 자주 씁니다.
Server 구조체로 설정 명시 #
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
log.Fatal(srv.ListenAndServe())http.ListenAndServe(":8080", nil)는 편하지만 — 타임아웃 기본값이 무한. 프로덕션에서는 항상 http.Server를 직접 만들어 타임아웃 설정하는 게 안전합니다(slowloris 공격 방어 등).
Graceful shutdown — 정상 종료 #
서버를 끄려고 SIGINT를 보낼 때 — 진행 중인 요청을 마치고 종료하고 싶을 때 사용합니다.
package main
import (
"context"
"errors"
"log"
"net/http"
"os/signal"
"syscall"
"time"
)
func main() {
srv := &http.Server{Addr: ":8080", Handler: handler()}
go func() {
if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
// SIGINT/SIGTERM 기다림
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
<-ctx.Done()
log.Println("shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatal("shutdown:", err)
}
log.Println("bye")
}흐름:
- 별도 고루틴에서 ListenAndServe — 메인은 시그널 대기
signal.NotifyContext— Go 1.16+ 의 표준 패턴. 컨텍스트로 시그널 처리srv.Shutdown(ctx)— 진행 중 요청이 끝날 때까지 (또는 ctx 타임아웃)
쿠버네티스나 ECS 같은 환경에서 안전한 롤아웃을 위해 필수.
정적 파일 — http.FileServer
#
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public"))))http.Dir("public")— 디스크의 디렉터리http.FileServer— 그 디렉터리를 서빙하는 핸들러http.StripPrefix— URL prefix 제거 (이거 빼면public/static/...로 찾으려 함)
작은 정적 사이트는 — 한 줄로 끝.
http.HandlerFunc 어댑터
#
func myFunc(w http.ResponseWriter, r *http.Request) { ... }
var h http.Handler = http.HandlerFunc(myFunc)함수가 — 인터페이스 메서드 한 개를 만족하도록 변환. 미들웨어 (#5)의 핵심 기법입니다.
함정 — 핸들러는 동시 실행 #
같은 핸들러가 — 여러 요청에서 동시에 호출됩니다. 핸들러가 공유 상태를 건드린다면 — 고급 #2 sync의 도구가 필요.
var counter int // 공유 변수
func handler(w http.ResponseWriter, r *http.Request) {
counter++ // ✗ race
}해결: atomic.Int64 또는 Mutex.
함정 — 응답 작성 후 return 잊기 #
if err != nil {
http.Error(w, "...", 400)
// return 빠뜨림 → 아래 코드도 실행되어 헤더 충돌
}
fmt.Fprintln(w, "...")에러 응답 후에는 항상 **return**으로 빠져나오기.
마무리 #
이번 글에서 정리한 내용:
net/http만으로 프로덕션 서버 가능- 핵심 세 타입 —
Handler,ResponseWriter,Request HandleFunc가 흔함, 상태 필요하면 구조체 + ServeHTTP- 응답 작성 순서 — Header → WriteHeader → Write
http.Server직접 만들고 타임아웃 항상 설정- graceful shutdown —
signal.NotifyContext+srv.Shutdown(ctx) - 핸들러는 동시 실행 — 공유 상태는 동기화
다음 글(#2 라우팅)에서는 — Go 1.22+ 의 ServeMux가 어떻게 패턴/메서드 매칭을 지원하는지, 그리고 별도 라우터(chi, gorilla/mux 등)가 필요한 경우를 정리합니다.