GO http 1일차 - http server 실행하기

0

GO http

목록 보기
1/5


오늘도 열심히 농사 공부해보자.

사실 필자는 웹 개발자가 아니다. 시스템 개발자에 가까운데, 최근에 만들고 있는 클라우드 플랫폼에 pod를 하나를 테스트 할 일이 생겼다. CLI를 하나 만들어서 pod에 적재하려고하니, 일이 귀찮아져서 local http server를 열어서 디버깅 테스트를 할 수 있도록 했다. 그 과정에서 그 동안 정리해야지... 하면서 밀어두었던 go http를 정리하고자 한다.

net/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"localhost8888포트로 서버를 구동하겠다는 것이다. 만약 지정된 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)으로 프로그램을 종료하게 된다.

Multiplexing Request Handlers

ListenAndServe 함수를 실행할 때 두번째 매개변수로 nil을 주었는데, 이는 default server multiplexer가 동작한다고 하였다. 그렇다면 만약 custom한 server multiplexer를 동작시키고 싶다면 어떻게 해야할까?? 그리고 custom한 server multiplexer는 무엇일까??

default server multiplexer를 사용하지 않고 custom하게 mux를 만들면, 서버의 urlip,port가 충돌하지않아, 하나의 프로그램으로 여러 개의 서버를 구동시킬 수 있다.

기본적으로 두 번째 매개변수에 들어갈 타입은 http.Handler 인터페이스이다. 해당 인터페이스의 구현체로 http.ServeMux가 있는데, 만들어주는 함수가 있는 net/httphttp.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 handlerurl과 함께 등록시키면 된다. 마지막으로 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.HandlerFuncmux.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.HandlerFuncurl를 맵핑하고 서버에 필요한 configuration을 설정하는 것이 바로 http.Handler 인터페이스가 하는 일인 것이다.

http.Handler 인터페이스의 구현체인 http.ServeMuxHandlerFunc 내부의 동작은 다음과 같다.

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 patternhandler를 연결해주는 것이다. 재밌는 것이 muxEntryhandler를 넣을 때 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를 반환해주고, hServeHTTP를 실행하는 것이다. hhttp.Handler 인터페이스이다. 참조고 mux.Handler부분을 계속 따라가면 우리가 위에서 봤던 mux.m에서 url 요청에 따라 핸들러를 꺼내주는 로직이 있다.

따라서, 순서가 이렇게 되는 것이다.

handlerFunc 구현 --> ServeMux구조체의 HandleFunc에 등록 --> ListenAndServe --서버 시작--> 요청이 들어오면 ServeMuxServeHTTP 실행 --> url에 맞는 handlerServeHTTP 실행 --> 이전에 구현한 handlerFuncServeHTTP 실행 --> handlerFuncServeHTTP는 자기 자신을 실햄하도록 구현됨

뭔가 복잡해보이는 데 정리하면 다음과 같다.

  1. handlerFunc을 구현하자.
  2. mux를 만들어 url과 맵핑하자.
  3. ListenAndServemux를 넣어주자.

0개의 댓글