Golang HTTP 서버

멀티플렉서

ServeMux는 멀티플렉서로 HTTP 요청 라우팅을 처리한다. 여러 URL을 적절한 동작으로 연결해 주는 역할이다.

func ListenAndServe(addr string, handler Handler) error

ListenAndServe는 서버를 실행하고 요청을 기다린다. 첫번째 인자로 포트를, 두 번째 인자로 Handler를 받는다.

func main() {
	// http://127.0.0.1:8080/
	http.ListenAndServe(":8080", nil)
}

Handler를 nil로 작성하면 DefaultServeMux를 사용한다. 이 경우 Handler 함수를 등록할 때 바로 http에 등록한다. (자세한 건 아래에서)

func main() {
	mux := http.NewServeMux()
	http.ListenAndServe(":8080", mux)
}

직접 Handler를 생성해 등록할 수도 있다. 이때 NewServeMux를 사용한다. 일반적으로 권장하는 방법이다.

NewServeMux는 ServeMux를 반환하는데 왜 ListenAndServe 2번째 인자로 쓰이는지 혼란이 올 수 있다. 이는 ServeMux가 Handler 인터페이스의 구현체이기 때문이다. 다시 말해 ListenAndServe는 Handler 인터페이스를 받는데, 멀티플렉서인 ServeMux도 Handler 인터페이스를 만족한다. Handler 인터페이스에 대해서는 아래에서 자세히 다룬다.


Handler

Handler는 URL로 요청이 들어왔을 때 수행할 동작이다. 

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Welcome")
	})
	http.ListenAndServe(":8080", mux)
}

HandleFunc는 Handler 메서드를 등록한다. 위 코드는 "/"로 접근했을 때 "Welcome"을 출력해 준다. 구체적으로 Handler 인터페이스는 ServeHTTP 메서드를 가진다. 

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

위에서 살펴봤던 HandleFunc는 두번째 인자로 ServeHTTP를 받은 것이다.

Handler 객체를 직접 구현하고 등록하는 방법도 있다.

type userHandler struct{}

func (h *userHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint("Hello")
}

func main() {
	mux := http.NewServeMux()
	mux.Handle("/", &userHandler{})
	http.ListenAndServe(":8080", mux)
}

Handle을 사용하면 구현한 객체를 바로 전달할 수 있다.

참고로 DefaultServeMux를 사용하면 바로 http에 등록한다.

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Welcome")
	})
	http.ListenAndServe(":8080", nil)
}

정리

  • ServeMux는 요청에 따라 적절한 Handler를 연결해 준다.
  • Handler는 요청을 받아 작업을 수행한다.
  • 요청에 대한 함수를 등록하고 싶다면 HandleFunc, 객체를 등록하고 싶다면 Handle을 사용하면 된다.
package main

import (
	"fmt"
	"net/http"
)

func main() {
	mux := http.NewServeMux() // -> http.ServeMux (Handler)
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello") // -> Handler.ServeHTTP
	})
	mux.HandleFunc("/new", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "new page")
	})
	http.ListenAndServe(":3000", mux)
}

이제 go를 실행하고 "http://127.0.0.1 + 포트"로 접속하면 화면을 볼 수 있다.

$ go run main.go


URL로 정보 받아오기

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		name := r.URL.Query().Get("name") // URL에서 name 가져오기
		if name == "" {
			name = "Unknown"
		}
		fmt.Fprintf(w, "Hello %s!", name)
	})
	http.ListenAndServe(":8080", nil)
}

정보는 URL?key= 형태로 요청된다. 만약 "127.0.0.1:8080/hello?name=Jin"라는 요청이 들어왔다면 "Hello Jin!"이 출력된다. name이 없으면 "Hello Unknown!"이 출력된다.


index.html 보여주기

앞선 예제는 Print를 통해 출력했다면, 이제는 index.html을 서빙하는 코드다. 파일 구조는 아래와 같다.

ROOT
 |-- main.go
 |-- static
      |-- index.html
func main() {
	http.Handle("/", http.FileServer(http.Dir("static")))
	http.ListenAndServe(":8080", nil)
}

FileServer를 통해 "static" 폴더를 공유한다. 폴더에 index.html이 있다면 자동으로 보여준다.


파일 업로드

반대로 FileServer를 이용해 파일을 업로드해보겠다.

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	// 파일 업로드
	uploadFile, header, err := r.FormFile("file")
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}
	// 빈 파일 생성
	filePath := fmt.Sprintf("%s/%s", "./uploads", header.Filename)
	file, err := os.Create(filePath)
	defer file.Close()
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprint(w, err)
		return
	}
	// 업로드된 파일 저장
	io.Copy(file, uploadFile)
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, "Uploaded successfully!")
}

func main() {
	http.Handle("/", http.FileServer(http.Dir("static")))
	http.HandleFunc("/uploads", uploadHandler)
	http.ListenAndServe(":8080", nil)
}

POST 요청이 발생했을 때 f.FormFile과 key(name 속성)를 이용해 파일을 불러온다. 불러온 파일은 /uploads 폴더에 저장된다. Go에서 바로 파일을 저장할 수 없어 빈 파일을 생성하고 덮어쓰기 한 모습이다.

w.WriteHeader는 헤더 정보를 작성하는 함수로 상태 코드를 전달할 수 있다.

참고로 POST 요청을 보내는 폼은 아래와 같은 형태다.

<!-- index.html -->
<form action="/uploads" method="POST" enctype="multipart/form-data">
    <input type="file" name="file" />
    <input type="submit" value="제출" />
</form>

참고: Tucker Programming