Go

Go 1.22 버전, `http.ServeMux` 하나면 충분할까요?

드리프트2 2025. 4. 25. 20:21

Go 1.22 버전, http.ServeMux 하나면 충분할까요?

안녕하세요, 오늘은 Go 언어 웹 개발에서 중요한 역할을 하는 http.ServeMux에 대한 이야기를 해볼까 하는데요.

 

특히 Go 1.22 버전에서 http.ServeMux가 얼마나 강력해졌는지, 그리고 써드파티 라이브러리 없이도 충분한지 함께 알아보도록 하겠습니다.

 

Go 1.22, 달라진 점이 뭘까요?

Go 웹 개발을 하다 보면, 좀 더 효율적이고 유연한 라우팅 기능이 필요할 때가 있는데요.

 

그래서 많은 개발자들이 httproutergorilla/mux 같은 써드파티 라이브러리를 사용해왔습니다.

 

하지만 Go 1.22 버전에서는 표준 라이브러리에 있는 http.ServeMux가 엄청나게 업그레이드됐다는 사실!

 

알고 계셨나요? 이제는 굳이 써드파티 라이브러리에 의존하지 않아도 될 정도로 기능이 풍부해졌는데요.

 

Go 1.22 버전에서는 표준 라이브러리인 net/http 패키지의 기본 HTTP 서비스 멀티플렉서의 패턴 매칭 능력을 강화하는 기능이 추가됐습니다.

 

기존의 http.ServeMux는 기본적인 경로 매칭 기능만 제공해서 아쉬운 점이 많았는데요.

 

이번 업데이트로 써드파티 라이브러리와의 기능 차이를 많이 좁혔다고 합니다.

 

그럼 새로운 멀티플렉서(mux)는 어떻게 사용하는지, REST 서버 예제를 통해 살펴보고, 성능은 어떤지 gorilla/mux와 비교해볼까요?

새로운 mux, 이렇게 사용하면 돼요!

gorilla/mux 같은 써드파티 mux/router를 사용해본 경험이 있다면, 새로운 표준 mux 사용법은 아주 익숙하고 쉬울 겁니다.

 

먼저 공식 문서를 읽어보는 걸 추천하는데요, 설명이 간결하고 명확해서 이해하기 쉬울 거예요.

 

(I) 기본 사용법 예시

아래 코드는 mux의 새로운 패턴 매칭 기능 몇 가지를 보여줍니다.

package main

import (
        "fmt"
        "net/http"
)

func main() {
        mux := http.NewServeMux()
        mux.HandleFunc("GET /path/", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprint(w, "got path\n")
        })
        mux.HandleFunc("/task/{id}/", func(w http.ResponseWriter, r *http.Request) {
                id := r.PathValue("id")
                fmt.Fprintf(w, "handling task with id=%v\n", id)
        })
        http.ListenAndServe("localhost:8090", mux)
}

 

Go 개발 경험이 있는 분들은 아마 두 가지 새로운 기능을 바로 알아차리셨을 텐데요.

  • 첫 번째 핸들러에서는 HTTP 메서드(예: GET)가 패턴의 일부로 명시적으로 사용됩니다. 즉, 이 핸들러는 /path/로 시작하는 경로에 대한 GET 요청에만 응답하고, 다른 HTTP 메서드 요청은 처리하지 않습니다.
  • 두 번째 핸들러에서는 두 번째 경로 구성 요소인 {id}에 와일드카드가 포함되어 있습니다. 이전 버전에서는 지원하지 않았던 기능인데요. 이 와일드카드는 단일 경로 구성 요소와 일치할 수 있으며, 핸들러는 요청의 PathValue 메서드를 통해 일치하는 값을 가져올 수 있습니다.

다음은 curl 명령어를 사용하여 이 서버를 테스트하는 예시입니다.

$ gotip run sample.go
# 다른 터미널에서 테스트
$ curl localhost:8090/what/
404 page not found
$ curl localhost:8090/path/
got path
$ curl -X POST localhost:8090/path/
Method Not Allowed
$ curl localhost:8090/task/leapcell/
handling task with id=leapcell

 

테스트 결과에서 볼 수 있듯이, 서버는 /path/에 대한 POST 요청을 거부하고 GET 요청만 허용합니다(curl은 기본적으로 GET 요청을 사용합니다).

 

동시에, 요청이 일치하면 id 와일드카드에 해당 값이 할당됩니다. 새로운 ServeMux의 더 많은 기능 (예: 후행 경로 규칙, {id}를 사용한 와일드카드 매칭, {$}로 끝나는 경로의 엄격한 매칭)을 자세히 알아보려면 문서를 참고하는 것이 좋습니다.

 

(II) 패턴 충돌 처리

이번 업데이트에서는 서로 다른 패턴 간의 충돌 문제에 특히 주목하고 있습니다. 다음은 예시인데요.

mux := http.NewServeMux()
mux.HandleFunc("/task/{id}/status/", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")
        fmt.Fprintf(w, "handling task status with id=%v\n", id)
})
mux.HandleFunc("/task/0/{action}/", func(w http.ResponseWriter, r *http.Request) {
        action := r.PathValue("action")
        fmt.Fprintf(w, "handling task 0 with action=%v\n", action)
})

 

서버가 /task/0/status/에 대한 요청을 받으면, 두 핸들러 모두 이 요청과 일치할 수 있습니다.

 

새로운 ServeMux 문서에서는 패턴 우선순위 규칙과 잠재적인 충돌을 처리하는 방법을 자세히 설명합니다.

 

충돌이 발생하면 등록 프로세스에서 panic이 발생합니다. 위 예제의 경우, 다음과 같은 오류 메시지가 표시됩니다.

panic: pattern "/task/0/{action}/" (registered at sample - conflict.go:14) conflicts with pattern "/task/{id}/status/" (registered at sample - conflict.go:10):
/task/0/{action}/ and /task/{id}/status/ both match some paths, like "/task/0/status/".
But neither is more specific than the other.
/task/0/{action}/ matches "/task/0/action/", but /task/{id}/status/ doesn't.
/task/{id}/status/ matches "/task/id/status/", but /task/0/{action}/ doesn't.

 

이 오류 메시지는 자세하고 실용적인데요. 복잡한 등록 시나리오(특히 소스 코드의 여러 위치에서 패턴이 등록될 때)에서 이러한 세부 정보는 개발자가 충돌 문제를 신속하게 찾고 해결하는 데 도움이 될 수 있습니다.

새로운 mux로 서버를 만들어볼까요?

Go REST 서버 시리즈에서는 Go에서 여러 가지 방법으로 작업/할 일 목록 애플리케이션을 위한 간단한 서버를 구현했는데요.

 

첫 번째 부분은 표준 라이브러리를 기반으로 구현했고, 두 번째 부분은 gorilla/mux 라우터를 사용하여 동일한 서버를 다시 구현했습니다.

 

이제 Go 1.22의 향상된 mux로 이 서버를 다시 구현하는 것은 매우 의미 있는 일이며, gorilla/mux를 사용한 솔루션과 비교하는 것도 흥미롭습니다.

 

(I) 패턴 등록 예시

다음은 몇 가지 대표적인 패턴 등록 코드입니다.

mux := http.NewServeMux()
server := NewTaskServer()
mux.HandleFunc("POST /task/", server.createTaskHandler)
mux.HandleFunc("GET /task/", server.getAllTasksHandler)
mux.HandleFunc("DELETE /task/", server.deleteAllTasksHandler)
mux.HandleFunc("GET /task/{id}/", server.getTaskHandler)
mux.HandleFunc("DELETE /task/{id}/", server.deleteTaskHandler)
mux.HandleFunc("GET /tag/{tag}/", server.tagHandler)
mux.HandleFunc("GET /due/{year}/{month}/{day}/", server.dueHandler)

 

gorilla/mux 예제와 마찬가지로, 여기서는 동일한 경로를 가진 요청이 특정 HTTP 메서드를 사용하여 서로 다른 핸들러로 라우팅됩니다.

이전의 http.ServeMux를 사용하면 이러한 매처는 요청을 동일한 핸들러로 전달한 다음 핸들러는 요청 메서드를 기반으로 후속 작업을 결정했을 겁니다.

 

(II) 핸들러 예시

다음은 핸들러의 코드 예시입니다.

func (ts *taskServer) getTaskHandler(w http.ResponseWriter, req *http.Request) {
        log.Printf("handling get task at %s\n", req.URL.Path)
        id, err := strconv.Atoi(req.PathValue("id"))
        if err != nil {
                http.Error(w, "invalid id", http.StatusBadRequest)
                return
        }
        task, err := ts.store.GetTask(id)
        if err != nil {
                http.Error(w, err.Error(), http.StatusNotFound)
                return
        }
        renderJSON(w, task)
}

 

이 핸들러는 req.PathValue("id")에서 ID 값을 추출하는데요. gorilla/mux와 비슷한 방식입니다.

 

하지만 정규식을 사용하여 {id}가 정수만 일치하도록 지정하지 않았기 때문에 strconv.Atoi에서 반환되는 오류에 주의해야 합니다.

 

전반적으로 최종 결과는 gorilla/mux를 사용한 솔루션과 매우 유사합니다.

 

기존의 표준 라이브러리 메서드와 비교할 때, 새로운 mux는 더 복잡한 라우팅 작업을 수행할 수 있어서 라우팅 결정을 핸들러 자체에 맡길 필요성을 줄여주고 개발 효율성과 코드 유지 관리성을 향상시켜줍니다.

결론: 이제 어떤 라우터를 선택해야 할까요?

"어떤 라우터 라이브러리를 선택해야 할까요?"는 Go 초보자들이 흔히 묻는 질문인데요.

 

Go 1.22 출시 후, 이 질문에 대한 답이 바뀔 수도 있을 것 같습니다.

 

많은 개발자들이 새로운 표준 라이브러리 mux가 자신의 요구 사항을 충족시키기에 충분하다는 것을 알게 될 것이고, 써드파티 패키지에 의존할 필요가 없어질 수도 있습니다.

 

물론, 일부 개발자들은 익숙한 써드파티 라이브러리를 계속 선택할 텐데요.

 

그것도 합리적인 선택입니다.

 

gorilla/mux와 같은 라우터는 여전히 표준 라이브러리보다 더 많은 기능을 가지고 있습니다.

 

또한 많은 Go 프로그래머들이 라우터뿐만 아니라 웹 백엔드를 구축하는 데 필요한 추가 도구를 제공하는 Gin과 같은 가벼운 프레임워크를 선택할 겁니다.

 

결론적으로, Go 1.22에서 표준 라이브러리 http.ServeMux를 최적화한 것은 의심할 여지 없이 긍정적인 변화입니다.