프로그래밍 언어/Golang

Go: 웹 서버 구축하기 (net/http, html/template)

WAS(Web Application Server)

Node.js 서버처럼 Go 언어로 만든 서버도 동적 컨텐츠를 처리하는 WAS(Web Application Server)로써 동작하게 만들 수 있다. 보통 앞에 NginX, Apache 와 같은 범용적인 http 서버를 앞에다가 두고 뒤에 별도로 WAS 를 실행시켜두는 것이 일반적인 구성일 것이다. 물론 Go 로 만든 서버를 직접 80 포트에 바인딩하는 것도 가능하지만 요청이 많아질 경우 과부하가 걸릴 가능성도 있어서 많은 트래픽이 발생하는 경우에는 권장하는 방법은 아니다.

 

이 포스트에서 말하는 웹 서버는 Go 언어로 만든 서버를 의미할 것이며 이러한 서버를 만드는 행위는 Node.js http 모듈을 사용하여 만드는 것만큼 상당히 단순하다고 볼 수 있다. 개인적으로 사용 편의성은 Node.js 보다 훨씬 더 좋은 듯하다.

net/http

http 패키지는 웹 서버를 구축하기 위한 도구들이 담겨있는 패키지다. http 패키지 문서에서 꽤나 많은 함수와 타입 목록들을 볼 수 있지만, 간단히 소스코드를 작성하여 살펴보면 다음과 같다.

package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, Go!"))
	})
	if err := http.ListenAndServe(":3000", nil); err != nil {
		log.Fatal(err)
	}
}
http.ResponseWriter 타입은 io.Writer 인터페이스를 충족하기 때문에 해당 인터페이스를 받는 함수나 메서드에 사용할 수 있다.

논리적 경로인 URL 의 루트인 / 경로에 대한 라우팅과 그에 대한 로직을 설정한다. http.ListenAndServe() 함수를 쓰면 서버를 시작할 수 있으며 시작하는 순간 블러킹되기 때문에 서버를 시작할 때 어떤 문제가 발생하지 않는 이상은 error 가 반환되지 않는다.

http.HandleFunc(), http.Handle()

요청을 핸들링 하기 위한 함수로는 http.HandleFunc() 도 있겠지만, http.Handle() 도 있다. http.HandleFunc() 의 경우 파라매터로 함수를 받지만, http.Handle() 의 경우 Handler 타입을 받는다. Handle인터페이스이며 해당 인터페이스를 만족하기만 하면 요청을 핸들링할 수 있다.

http.Handler

$ go doc http.Handler
package http // import "net/http"

type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

http.HandlerFunc

http.HandlerFunc 는 인터페이스가 아닌 타입이며 http.Handler 인터페이스를 만족한다.

$ go doc http.HandlerFunc
package http // import "net/http"

type HandlerFunc func(ResponseWriter, *Request)
    The HandlerFunc type is an adapter to allow the use of ordinary functions as
    HTTP handlers. If f is a function with the appropriate signature,
    HandlerFunc(f) is a Handler that calls f.

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)

따라서 해당 타입을 사용하면 http.Handle() 을 사용해서도 핸들러를 넘길 수 있게된다.

http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hello, Go!"))
}))

html/template

html/template 를 사용하면 Content-Type: text/html 을 응답으로 반환할 수 있다. html/template 의 기반은 text/template 이며 둘은 패키지 이름은 같지만 패키지 경로가 다르므로 서로 다른 패키지임을 주의하자. html/template 패키지는 text/template 패키지에 비해 웹에서 사용할 수 있도록 이스케이프와 같은 보안요소를 제공한다.

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	tmpl, err := template.New("index").Parse("<h1>Hello, Go!</h1>")
	if err != nil {
		http.Error(
			w,
			http.StatusText(http.StatusInternalServerError),
			http.StatusInternalServerError)
	}
	if err = tmpl.Execute(w, nil); err != nil {
		http.Error(
			w,
			http.StatusText(http.StatusInternalServerError),
			http.StatusInternalServerError)
	}
})
error 처리가 다소 지저분해 보이긴 한다만, Go 의 에러처리 방식은 어플리케이션 관점에서 보면 더욱 견고하게 구성할 수 있도록 만들어준다.

템플릿에 데이터 넘기기

template.New() 함수를 호출하여 새로운 템플릿을 생성하고, HTML String 을 파싱하거나 template.ParseFiles() 메서드를 사용하여 html 파일도 파싱할 수 있다. template.Execute() 메서드의 두번째 파라매터로는 템플릿에 넘길 데이터를 줄 수 있다. 예를 들면 다음과 같이 템플릿을 만들고 데이터를 넘길 수 있다.

tmpl, _ := template.New("index").Parse("<h1>{{.}}</h1>")
tmpl.Execute(w, "Hello, Go!")

{{.}} 와 같이 사용하면 . 이 템플릿에 넘겨준 데이터로 변환된다. 조건문과 반복문도 사용이 가능하며 일반적인 사용법으로는 템플릿에 데이터를 넘길때 구조체의 객체를 넘기는 것이다. 이러한 템플릿의 사용법은 html/template 패키지의 문서에 가보면 자세히 알 수 있다.