Untitled

개요

2024년 2월 6일 Golang 1.22 버전이 릴리즈 되었습니다. 다양한 기능들이 업데이트되었는데 그중에서 저에게 가장 흥미로운 내용은 HTTP 서버 라우팅 관련 내용이었는데 관련해서 포스팅 해보려고 합니다.

멀티플렉서

Golang에서 HTTP 서버를 구현하는 건 아주 간단합니다. 하지만 실제 현업에서는 기본 HTTP 멀티플렉서로만 구현을 하기가 힘든데 가장 큰 이유중에 하나가 URL 매칭 기능이 너무 제한적이기 때문입니다.

이전에 모종의 이유로 외부 패키지는 사용하지 않고 구현 해야만 했던 일이 있었는데 경로 값을 매칭하거나 할 때 고역을 치렀던 경험이 있습니다. 그래서 기본 패키지는 예제에서만 쓰이는 게 대다수였는데 작년에 아래와 같은 PR이 있었습니다.

https://github.com/golang/go/issues/61410

해당 PR은 아래 2가지 요소에 대해서 기능을 추가하는 것이고 애초에 Golang 개발진도 이런 불편함을 인지는 하고 있었을 것 같지만 Golang 언어 설계의 철학인 단순성을 통해서 확장 가능한 형태의 표준 라이브러리를 유지하고 싶지 않았나 싶습니다.

  • Path Value를 HTTP Request에서 받을 수 있게 추가
  • HTTP Method, Path Wildcard 패턴 매칭 기능 추가

간단한 HTTP 서버 예제

 1package main
 2
 3import (
 4	"fmt"
 5	"log"
 6	"net/http"
 7)
 8
 9func main() {
10	http.HandleFunc("/", func(w http.ResponseWriter, request *http.Request) {
11		fmt.Fprintf(w, "hello world")
12	})
13
14	fmt.Println("Server is running on http://localhost:8080")
15	if err := http.ListenAndServe(":8080", nil); err != nil {
16		log.Panicf("panic listen and server : %v", err)
17	}
18}

루트 Path로 요청하면 무조건 hello world를 출력합니다. 만약에 POST나 다른 Method로 호출을 한다면 해당 라우터에서 request의 메소드를 기반으로 분기를 해서 작성을 해야만 했습니다.

복잡한 HTTP 서버 예제

 1package main
 2
 3import (
 4	"fmt"
 5	"log"
 6	"net/http"
 7	"strings"
 8)
 9
10// "Hello World"를 반환하는 GET 요청 핸들러
11func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
12	if r.URL.Path == "/" && r.Method == "GET" {
13		fmt.Fprintf(w, "Hello World")
14	} else {
15		http.NotFound(w, r)
16	}
17}
18
19// 요청 본문에 포함된 데이터를 그대로 반환하는 POST 요청 핸들러
20func echoPostHandler(w http.ResponseWriter, r *http.Request) {
21	if r.URL.Path == "/echo" && r.Method == "POST" {
22		fmt.Fprintf(w, "Echo")
23	} else {
24		http.NotFound(w, r)
25	}
26}
27
28// 경로 분석을 통해 `id`를 추출하고 "Received ID: [id]"를 반환하는 GET 요청 핸들러
29func getIdHandler(w http.ResponseWriter, r *http.Request) {
30	if strings.HasPrefix(r.URL.Path, "/get/") && r.Method == "GET" {
31		id := strings.TrimPrefix(r.URL.Path, "/get/")
32		fmt.Fprintf(w, "Received ID: %s", id)
33	} else {
34		http.NotFound(w, r)
35	}
36}
37
38func main() {
39	http.HandleFunc("/", helloWorldHandler)   // 루트 경로 핸들러
40	http.HandleFunc("/echo", echoPostHandler) // POST 요청 핸들러
41	http.HandleFunc("/get/", getIdHandler)    // ID를 포함한 GET 요청 핸들러
42
43	fmt.Println("Server is running on http://localhost:8080")
44	if err := http.ListenAndServe(":8080", nil); err != nil {
45		log.Panicf("panic listen and server : %v", err)
46	}
47}

위에 구현한 함수는 아래 정의한 HTTP 요청에 대한 서버의 구현체입니다.

  • GET /get/{_id}
  • POST /echo
  • GET /

위에 언급했듯이 좀 더 복잡해지면 라우터 내부에서는 다양한 작업들을 처리해 줘야 합니다. 실제 API 서버를 만들다 보면 이것보다 훨씬 복잡하고 많은 API들을 다뤄야 하기 때문에 기본 패키지로는 개발을 진행하기가 현실적으로 힘듭니다.

Golang 웹 프레임워크

간단한 echo 예제

 1package main
 2
 3import (
 4	"net/http"
 5	
 6	"github.com/labstack/echo/v4"
 7)
 8
 9func main() {
10	e := echo.New()
11	e.GET("/users/:id", getUser)
12	e.Logger.Fatal(e.Start(":1323"))
13}
14
15// e.GET("/users/:id", getUser)
16func getUser(c echo.Context) error {
17  	// User ID from path `users/:id`
18  	id := c.Param("id")
19	return c.String(http.StatusOK, id)
20}

기본 http 패키지와 비교했을 때 Method나 Path Value들을 구조화된 형태로 손쉽게 활용할 수 있습니다.

1.22 이후 HTTP 서버예제

 1package main
 2
 3import (
 4	"fmt"
 5	"log"
 6	"net/http"
 7)
 8
 9func main() {
10
11	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
12		fmt.Fprintf(w, "Hello World")
13	}) // 루트 경로 핸들러
14	http.HandleFunc("POST /echo", func(w http.ResponseWriter, r *http.Request) {
15		fmt.Fprintf(w, "Echo")
16	}) // POST 요청 핸들러
17	http.HandleFunc("/get/{id}", func(w http.ResponseWriter, r *http.Request) {
18		id := r.PathValue("id")
19		fmt.Fprintf(w, "Received ID: %s", id)
20	}) // ID를 포함한 GET 요청 핸들러
21
22	fmt.Println("Server is running on http://localhost:8080")
23	if err := http.ListenAndServe(":8080", nil); err != nil {
24		log.Panicf("panic listen and server : %v", err)
25	}
26}

위에 예제는 기존에 작성했던 복잡한 HTTP 서버 예제와 결과가 100퍼센트 동일합니다. 대신 코드의 간결함과 가독성이 훨씬 개선된 것을 보실 수 있습니다.

기존에 HandleFunc 의 첫번 째 매개변수는 패턴을 받을 수 있게 변경 되었고, Request 객체에서 PathValue라는 함수를 통해서 손 쉽게 값을 전달 받을 수 있도록 개선되었습니다.

정리

이전에 1.18버전에서 Golang에 Generic 기능이 들어갔을 때 Golang의 철학과 위배된다고 갑론을박이 있었는데 그래도 유저들의 목소리를 듣고 개선해나가는 개발진들이 대단하다고 느끼면서 한편으로 이렇게 열성적으로 의견을 내는 Gopher들도 대단하다고 생각이 들었습니다.

저는 적당한 타협점을 찾았다고 생각이 들고 여기에서 기능이 더 추가되면 그때는 위에서 언급했었던 단순성을 잃어버리지 않을까라고 생각했습니다. 저는 그래도 여전히 현업에서는 외부 패키지를 쓸 것 같지만 일부 경우에서는 표준 패키지로도 충분히 사용 가능할 정도로 많이 개선된 것 같아서 이런 변화가 기분이 좋습니다.