고 실전 #2 라우팅 — Go 1.22+ ServeMux
#1 첫 HTTP 서버에서는 단일 /만 처리했습니다. 실전 서버는 — 여러 경로와 메서드를 다룹니다.
Go 1.22 (2024) 이전에는 — 표준 ServeMux가 메서드 매칭이나 패스 파라미터를 지원하지 않아 외부 라우터가 거의 필수였습니다. 1.22부터 — 표준만으로도 보통 충분합니다.
Go 1.22+ ServeMux #
mux := http.NewServeMux()
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("PUT /users/{id}", updateUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
http.ListenAndServe(":8080", mux)- 메서드 + 경로 —
"GET /users" - 패스 파라미터 —
{id}식으로
이전에는 — 전부 if r.Method == "GET" 분기를 수동으로 작성하던 방식입니다. 1.22부터 한 줄로.
패스 파라미터 읽기 #
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// ...
}r.PathValue("id") — 그 부분의 값을 string으로. 정수가 필요하면 직접 strconv.Atoi.
Wildcard와 ...
#
mux.HandleFunc("GET /files/{path...}", serveFile){name}— 한 segment{name...}— 끝까지의 모든 segment
// /files/a/b/c.txt → r.PathValue("path") == "a/b/c.txt"Host 매칭 #
mux.HandleFunc("api.example.com/health", apiHealth)
mux.HandleFunc("admin.example.com/health", adminHealth)같은 경로라도 — Host 헤더에 따라 다른 핸들러. 멀티 도메인 서비스에 유용.
매칭 우선순위 #
여러 패턴이 매치할 때:
- 더 구체적인 패턴이 우선 (호스트 명시 > 미명시, 정확한 segment > wildcard 등)
- 동일 우선순위면 — panic (등록 시점에 충돌 검출)
이전 ServeMux의 모호함이 — 이번 버전에서 명확한 규칙으로 정리됐습니다.
흔한 패턴 — /로 끝나는 경로
#
mux.HandleFunc("GET /api/", apiHandler) // /api/* 모두 (prefix)
mux.HandleFunc("GET /api/{$}", apiRoot) // /api/ 정확히
mux.HandleFunc("GET /api", redirectToSlash) // /api 정확히/api/—/api/...의 모든 경로 (prefix)/api/{$}—/api/만 (정확)/api—/api만 (slash 없는 형태)
이 분리도 1.22의 새 기능. 이전엔 prefix와 정확이 섞여 헷갈렸습니다.
그룹/prefix 처리 #
api := http.NewServeMux()
api.HandleFunc("GET /users", listUsers)
api.HandleFunc("GET /posts", listPosts)
root := http.NewServeMux()
root.Handle("/api/", http.StripPrefix("/api", api))별도 mux를 만들고 — StripPrefix로 prefix 제거. API v1, v2 분리에 어울립니다.
/api/v1 같은 버전 prefix
#
v1 := http.NewServeMux()
v1.HandleFunc("GET /users", listUsers)
v2 := http.NewServeMux()
v2.HandleFunc("GET /users", listUsersV2)
root := http.NewServeMux()
root.Handle("/api/v1/", http.StripPrefix("/api/v1", v1))
root.Handle("/api/v2/", http.StripPrefix("/api/v2", v2))같은 핸들러를 가진 두 mux를 — prefix 별로 등록.
외부 라우터가 어울리는 경우 #
표준이 충분해진 1.22 이후에도 — 외부 라우터가 여전히 의미 있는 경우가 있습니다.
go-chi/chi
#
import "github.com/go-chi/chi/v5"
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Route("/{id}", func(r chi.Router) {
r.Use(loadUser)
r.Get("/", getUser)
r.Delete("/", deleteUser)
})
})
http.ListenAndServe(":8080", r)장점:
- 그룹 단위로 미들웨어 적용
- 중첩 라우터 —
Route/Mount - 표준 net/http 호환 — chi의 라우터도
http.Handler
큰 API에서 — 라우트 트리가 보기 좋고, 그룹별 미들웨어가 자연스러움. 표준 ServeMux만으로는 — 미들웨어를 라우트 단위로 끼우기가 다소 번잡합니다.
gorilla/mux
#
오래된 표준 라우터. Go 1.22 이전 시대의 사실상 디폴트였지만 — 이제는 chi가 더 활발하고, 표준 mux가 가까워졌습니다.
큰 프레임워크 — gin, echo, fiber
#
라우터 + 미들웨어 + 응답 헬퍼 등이 묶인 프레임워크.
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
c.JSON(200, gin.H{"id": c.Param("id")})
})장점:
- 응답 헬퍼(JSON, render)가 풍부
- 벤치마크 빠름 (특히 fiber는 fasthttp 기반)
- 개발 속도 빠름
trade-off:
- 표준 인터페이스에서 멀어짐 — 다른 라이브러리와 섞기 다소 번잡
- 러닝 커브 — 프레임워크별 컨벤션
- fasthttp 기반은 net/http와 호환 안 됨
주관: Go의 디자인 철학(작은 표준 + 명시적)과는 — chi가 가장 잘 맞는 경향. gin/echo는 다른 언어/프레임워크에 익숙한 분께 친근.
어느 쪽을 고를까? #
| 상황 | 추천 |
|---|---|
| 작은 서비스, 라우트 < 30개 | 표준 ServeMux |
| 그룹/중첩이 많고 — 라우트 단위 미들웨어 잦음 | chi |
| 빠른 개발/프로토타입, 응답 헬퍼 풍부 선호 | gin / echo |
| 극한의 성능 (특수한 경우) | fiber (fasthttp) |
기본은 표준에서 시작 → 막히는 부분이 보이면 chi. fasthttp 기반은 — 표준 net/http 호환 안 돼 라이브러리 선택지가 좁아진다는 점 유의.
라우트 테스트 #
net/http/httptest를 — 외부 의존 없이 로컬에서.
func TestGetUser(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUser)
req := httptest.NewRequest("GET", "/users/42", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}#6 테스트에서 자세히.
자주 만나는 함정 #
1) 등록 충돌 → panic #
mux.HandleFunc("GET /a/{id}", h1)
mux.HandleFunc("GET /a/{name}", h2) // ✗ 동일 우선순위 충돌 → panic{id}와 {name}은 같은 패턴입니다. 이름이 다른 와일드카드는 동일한 패턴. 컴파일 에러가 아닌 시작 시 panic.
2) /api vs /api/
#
mux.HandleFunc("GET /api", a) // /api만
mux.HandleFunc("GET /api/", b) // /api/, /api/anything요청이 /api로 와도 — 자동 redirect가 일어나 /api/로 갈 수 있습니다. 클라이언트가 redirect를 따르지 않으면 응답 못 받는 경우입니다.
3) 메서드 매칭 안 한 경우 #
mux.HandleFunc("/users", h)메서드 명시 안 하면 — 모든 메서드가 매치. POST만 받고 싶으면 명시적으로 "POST /users".
Method-not-allowed #
특정 경로에 GET만 등록되어 있고 POST가 오면 — 표준 ServeMux는 405 Method Not Allowed를 자동 반환합니다(1.22+). 이전 버전은 404였습니다. 작은 변화지만 — REST API에 적절한 응답.
마무리 #
이번 글에서 정리한 내용:
- Go 1.22+ ServeMux — 메서드 + 패스 파라미터 + 호스트 매칭
r.PathValue("id")로 파라미터 읽기{...}와일드카드,{$}정확 매칭- prefix 별 mux +
StripPrefix로 그룹화 - chi — 그룹별 미들웨어가 잦으면
- gin / echo / fiber — 응답 헬퍼나 성능이 결정적일 때
- 등록 충돌은 시작 시 panic
/api와/api/는 다른 패턴
다음 글(#3 JSON 입출력)에서는 — encoding/json으로 안전하게 입출력하기, 디코딩 에러 처리, 입력 검증 패턴을 정리합니다.