오늘도 열심히 농사 공부해보자.
사실 필자는 웹 개발자가 아니다. 시스템 개발자에 가까운데, 최근에 만들고 있는 클라우드 플랫폼에 pod를 하나를 테스트 할 일이 생겼다. CLI를 하나 만들어서 pod에 적재하려고하니, 일이 귀찮아져서 local http server를 열어서 디버깅 테스트를 할 수 있도록 했다. 그 과정에서 그 동안 정리해야지... 하면서 밀어두었던 go http를 정리하고자 한다.
go에서 대부분의 http 기능은 standard library에 있는 net/http
패키지를 통해 제공된다. 반면 나머지 network communication들은 net
패키지를 통해 제공된다. net/http
패키지는 http request들을 만드는 능력을 포함하며, 이러한 요청들을 처리하기위해 사용하는 http server를 제공한다.
http.ListenAndServe
함수는 각 path
별로 요청들의 응답을 처리하는 http server를 시작하는 기능을 한다. 또한, 같은 프로그램에서 여러 개의 http server들을 구동하기위해 프로그램을 확장할 수 있다.
http.ListenAndServe()
함수를 사용하면 서버를 시작할 수 있다. 두 가지 매개변수가 필요한데, 첫 번째는 ip, port
정보이고, 두 번째는 Handler
타입인데, Handler
타입은 들어오는 request를 처리하는 response handler를 묶어놓은 타입이라고 생각하면 된다.
만약, nil
로 넣어주면 기본적으로 동작하는 default server multiplexer가 동작한다. 이 후에 custom multiplexer를 만들어 하나의 프로그램에 여러 서버를 구동할 수 있다.
func main() {
http.ListenAndServe(":8888", nil)
}
":8888"
은 localhost
의 8888
포트로 서버를 구동하겠다는 것이다. 만약 지정된 ip
가 있다면 ip:port
형식으로 만들면 된다. 가령 localhost
로만 서버를 구동시키겠다면 "127.0.0.1:8888"
이라고 써주면 된다.
그러나 이렇게만 해서는 request
를 처리할 수 없다. request
를 처리하고 response
를 보내는 handler
를 추가해보도록 하자.
HandleFunc
으로 request를 받고, response보내기go http server는 두 개의 중요한 컴포넌트들을 포함하는데, 하나는 서버가 http client로부터 request들을 listen하고, 다른 하나는 그런 request에 맞게 response를 처리할 수 있는 기능이다. 이것들을 가능하게 해주는 것이 바로 http.HandleFunc
이다.
http.HandleFunc
은 두 개의 파라미터를 받는데, 첫번째로는 path
로 request가 들어오는 url경로를 말한다. 두번째는 request가 왔을 때 이를 해결할 handler
가 되는데 정의는 다음과 같다.
func(ResponseWriter, *Request)
다음의 함수 시그니처를 지키기만하면 되는 것이다. 참고로 이 함수는 http.HandlerFunc
타입으로 정의되어 있는데 이를 알아두도록 하자.
ResponseWriter
타입은 말 그대로 request에 대한 응답을 전달하는 매개변수로 여기에 응답을 써서 보내거나, http status code
를 전달할 수 있다. Request
는 http request 정보가 들어오며, url parameter, http method, header 등의 정보를 가져올 수 있다. 자세한 방법은 직접 사용하면서 분석하는 것이 좋다.
간단하게, "/"
라는 path
로 요청이 오게되면 응답으로 "hello"
를 보내기로 하자.
func HelloHandler(res http.ResponseWriter, req *http.Request) {
res.Write([]byte("hello world!!\n"))
}
func main() {
fmt.Println("start server!")
http.HandleFunc("/", HelloHandler)
http.ListenAndServe(":8888", nil)
}
localhost:8888
에 접속하면 "hello world!!"
메시지가 온 것을 확인할 수 있다.
이번엔 추가로 /go
라는 url로 접속하면 "go"
라는 메시지를 보여주도록 하자.
func HelloHandler(res http.ResponseWriter, req *http.Request) {
res.Write([]byte("hello world!!\n"))
}
func goHandler(res http.ResponseWriter, req *http.Request) {
res.Write([]byte("GO\n"))
}
func main() {
fmt.Println("start server!")
http.HandleFunc("/", HelloHandler)
http.HandleFunc("/go", goHandler)
http.ListenAndServe(":8888", nil)
}
/go
라는 url에 접속하면 GO
라는 메시지가 나오게된다.
일단 handlers가 set up이 된 후, http.ListenAndServe
함수를 호출하며 global HTTP server가 특정 port를 통해 들어오는 요청을 처리한다.
http.ListenAndServe
는 blocking call이다. 이는 우리의 프로그램이 http.ListenAndServe
후에 더 이상 진행하지 않고 http.ListenAndServe
가 종료되기 까지 기다린다는 것이다. 그러나 http.ListenAndServe
는 우리의 프로그램이 끝나기 전이나 HTTP 서버가 shut down되기 전까지 동작을 멈추지 않는다. 비록 ListenAndServe
이 blocking되고, 우리의 프로그램이 서버를 shut down하는 방법이 없다하여도, ListenAndServe
에 대한 error handling을 포함하는 것은 여전히 매우 중요하다. 왜냐하면 ListenAndServe
을 호출하는 몇가지 방법이 실패할 수 있기 때문이다. 때문에 ListenAndServe
에 대한 error handling을 추가하는 것이 좋다.
func main() {
...
err := http.ListenAndServe(":8888", nil)
if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("Server closed\n")
} else if err != nil {
fmt.Printf("error starting server: %s\n", err)
os.Exit(1)
}
}
http.ErrServerClosed
는 서버가 shutdown 또는 close되었을 때 발생한는 error으로 어떤 error로 서버가 종료되었는 지를 알려준다. 만약, 다른 error가 발생했다면 else if
문으로 빠지게 되어 os.Exit(1)
으로 프로그램을 종료하게 된다.
ListenAndServe
함수를 실행할 때 두번째 매개변수로 nil
을 주었는데, 이는 default server multiplexer
가 동작한다고 하였다. 그렇다면 만약 custom한 server multiplexer를 동작시키고 싶다면 어떻게 해야할까?? 그리고 custom한 server multiplexer는 무엇일까??
default server multiplexer
를 사용하지 않고 custom하게 mux를 만들면, 서버의 url
과 ip,port
가 충돌하지않아, 하나의 프로그램으로 여러 개의 서버를 구동시킬 수 있다.
기본적으로 두 번째 매개변수에 들어갈 타입은 http.Handler
인터페이스이다. 해당 인터페이스의 구현체로 http.ServeMux
가 있는데, 만들어주는 함수가 있는 net/http
의 http.NewServeMux
함수가 있다.
...
func main() {
fmt.Println("start server!")
mux := http.NewServeMux()
mux.HandleFunc("/", HelloHandler)
mux.HandleFunc("/go", goHandler)
err := http.ListenAndServe(":8888", mux)
...
}
다음과 같이 http.NewServeMux()
라는 함수를 통해서 mux
를 만들고 여기에 http handler
를 url
과 함께 등록시키면 된다. 마지막으로 http.ListenAndServe
의 두번째 argument에 넣어주면 된다.
http.ServeMux
구조체는 default server multiplexer
와 동일한 configuration을 가지므로 코드가 변경될 사항이나 사용방법이 달라지는 경우는 거의 없다.
http.ServeMux
구조체가 구현하고 있는 interface인 http.Handler
를 살펴보자.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ServeHTTP(ResponseWriter, *Request)
만 구현하면 되는데, 해당 함수의 시그니처가 무언가 닮은 것이 있었다. 바로 http.HandlerFunc
이다. http.HandlerFunc
는 mux.HandleFunc
에 두번째 파라미터로 url
과 함께 등록되는 함수인데, 다음의 구조를 갖는다.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
재밌게도 ServeHTTP
를 구현하고 있는데, 함수 타입에서 자체적으로 메서드를 구현하고 있고 내부에서는 자신을 호출한다. 따라서 ListenAndServe
의 두 번째 매개변수에 http.ServeMux
가 아닌 http.HandlerFunc
을 줄 수도 있다.
...
func main() {
fmt.Println("start server!")
mux := http.NewServeMux()
mux.HandleFunc("/", HelloHandler)
mux.HandleFunc("/go", goHandler)
err := http.ListenAndServe(":8888", http.HandlerFunc(goHandler))
..
}
이렇게 만들어놓고, 실행하면 localhost:8888
로 오는 모든 요청은 goHandler
로 실행된다.
http.HandlerFunc
는 순수하게 핸들러 함수 그 자체이다. 따라서 ServeHTTP
내부에서 자신을 호출하고 있는 것이다. http.HandlerFunc
과 url
를 맵핑하고 서버에 필요한 configuration을 설정하는 것이 바로 http.Handler
인터페이스가 하는 일인 것이다.
http.Handler
인터페이스의 구현체인 http.ServeMux
의 HandlerFunc
내부의 동작은 다음과 같다.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
별다른 내용없이 Handle
을 수행하는데 Handle
은 다음과 같다.
func (mux *ServeMux) Handle(pattern string, handler Handler) {
...
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
가장 중요한 부분은 mux.m[pattern] = e
이 부분으로 url
pattern
과 handler
를 연결해주는 것이다. 재밌는 것이 muxEntry
에 handler
를 넣을 때 http.Handler
인터페이스로 넣어준다. 이렇게 한 이유를 대충 알 수 있을 것이다. 실제로 인터페이스에 넣어진 구현체는 우리가 만든 핸들러로 http.HandlerFunc
이다.
따라서, http.Handler
인터페이스의 ServeHTTP
를 실행할 수 있게 된다. http.ListenAndServe
메서드에서 실행되는 http.ServeMux
구조체의 http.ServeHTTP
메서드를 보도록 하자.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
요청 r
에 따라 mux.Handler(r)
로 핸들러인 h
를 반환해주고, h
의 ServeHTTP
를 실행하는 것이다. h
는 http.Handler
인터페이스이다. 참조고 mux.Handler
부분을 계속 따라가면 우리가 위에서 봤던 mux.m
에서 url 요청에 따라 핸들러를 꺼내주는 로직이 있다.
따라서, 순서가 이렇게 되는 것이다.
handlerFunc 구현
-->ServeMux
구조체의HandleFunc
에 등록 -->ListenAndServe
--서버 시작--> 요청이 들어오면ServeMux
의ServeHTTP
실행 --> url에 맞는handler
의ServeHTTP
실행 --> 이전에 구현한handlerFunc
의ServeHTTP
실행 -->handlerFunc
의ServeHTTP
는 자기 자신을 실햄하도록 구현됨
뭔가 복잡해보이는 데 정리하면 다음과 같다.
handlerFunc
을 구현하자.mux
를 만들어 url
과 맵핑하자.ListenAndServe
에 mux
를 넣어주자.