don't stop believing

Handler와 Multiplexer 이해하기 본문

Golang/Web App

Handler와 Multiplexer 이해하기

Tongchun 2019. 2. 7. 19:03

Multiplexer를 검색하다가 정리가 잘된 글을 찾았습니다.

https://www.integralist.co.uk/posts/understanding-golangs-func-type/


확인차 코드를 정리하여 기록해 보겠습니다.


Golang으로 Web app 개발을 하면 처음 만나는 Hello World 코드는 아래와 같습니다.

package main

import (
	"fmt"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello %s", r.URL.Path[1:])
}

func main() {
	http.HandleFunc("/World", handler)
	http.ListenAndServe(":8080", nil)
}

server.go 파일에 위 코드를 넣고 바로 실행해 봅니다.

$ go run server.go

그리고 브라우저를 열고 localhost:8080/World를 호출해 봅니다.

처음 Golang을 배우고 기본 코드를 따라하게 될때 HandleFunc가 무엇인지, ListenAndServe의 두 번째 파라메타에 왜 nil이 들어가는지 모릅니다.

그렇다고 그걸 모른체 따라해서는 안되며 계속 궁금증을 가져야 합니다.

여러 프로그램책에서 카고 컬트 프로그래밍이라는 것을 언급합니다.


카고 컬트 프로그래밍

2차 세계대전 중에 연합군은 전쟁을 돕기 위해 태평양 섬에 공군 기지를 지었습니다. 많은 양의 보급품과 군 장비 지원으로 인해 군인과 원주민의 삶을 크게 바꾸었습니다. 주민들은 비행기가 제조된 옷과 통조림 식품 그리고 기타 물품을 싣고 오는 것들을 처음 보았습니다. 전쟁이 끝나자, 기지는 버려졌고 화물(cargo)이 더는 도착하지 않았습니다. 그래서 섬 주민들은 군인들을 흉내내기 위해 항공 교통 관제사나 군인, 그리고 선원처럼 복장을 차려입고 활주로 비슷한 곳을 만들어 놓고 막대기를 이용해 착륙 신호를 항공기에 보냈습니다. 항공기에서 화물이 낙하산으로 떨어지도록 하기 위한 행진을 계속했습니다. 그러나 그들이 기다리는 비행기는 오지 않았습니다.

카고 예찬자들은 이와 비슷한 관행에 대해 카고 컬트 프로그래밍(cargo cult programming)이라는 이름을 부여했습니다. 카고 컬트 프로그래머는 착륙 신호가 무엇을 의미하는지 정확히 모른 채 착륙 신호를 흔듭니다. 이들은 인터넷에서 찾은 코드가 어떻게 동작하는지 이해하지 않고 (스택오버플로우와 같은 사이트 등에서) 복사하여 붙여넣기를 합니다. 결과적으로 코드를 변경하거나 확장할 수 없게 됩니다. 또한, 카고 컬트 프로그래머는 종종 프레임워크가 사용하는 특정 패턴이나 관례를 사용하는 이유를 이해하지 못한 채 웹 프레임워크를 사용하기도 합니다.


Go로 Web Server를 만드는 4개지 방법을 설명합니다.


1. No request parsing (serve same content regardless of request)

2. Manual request parsing

3. Multiplexer

4. Global multiplexer


사실상 3가지 방법입니다. 첫 번째와 두 번째는 실질적으로 같습니다.


1. No request parsing

가장 기본적인 방법 입니다.

package main

import (
	"fmt"
	"log"
	"net/http"
)

type pounds float32

func (p pounds) String() string {
	return fmt.Sprintf("£%.2f", p)
}

type database map[string]pounds

func (d database) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	for item, price := range d {
		fmt.Fprintf(w, "%s: %s\n", item, price)
	}
}

func main() {
	db := database{
		"foo": 1,
		"bar": 2,
	}

	log.Fatal(http.ListenAndServe("localhost:8000", db))
}

먼저 db라는 변수에 임시로 사용할 map 타입의 database를 정의합니다.

그리고 pounds라는 타입을 만드는데 이건 float32를 참조합니다. String() 함수는 pounds (float32)를 상속받아 string으로 변환해 리턴합니다. 마찬가지로 ServeHTTP 함수도 database를 상속받아 처리합니다.


ListenAndServe는 두 번째 인자로 ServeHTTP를 받습니다.

godoc으로 ListenAndServe를 확인하면 두 번째 인자로 Handler 타입을 받는 것을 확인할 수 있습니다.

$ godoc net/http ListenAndServe | less

func ListenAndServe(addr string, handler Handler) error

    ListenAndServe listens on the TCP network address addr and then calls

    Serve with handler to handle requests on incoming connections. Accepted

    connections are configured to enable TCP keep-alives.


    The handler is typically nil, in which case the DefaultServeMux is used.


    ListenAndServe always returns a non-nil error.

그럼 Handler 타입도 확인해 보겠습니다.

$ godoc net/http Handler | less

type Handler interface {

    ServeHTTP(ResponseWriter, *Request)

}

    A Handler responds to an HTTP request.


    ServeHTTP should write reply headers and data to the ResponseWriter and

    then return. Returning signals that the request is finished; it is not

    valid to use the ResponseWriter or read from the Request.Body after or

    concurrently with the completion of the ServeHTTP call.

Handler는 interface이며 ServeHTTP를 가지고 있습니다. 따라서, 위 코드에서 ServeHTTP 함수를 가지고 있는 database를 ListenAdnServe의 두 번째 인자로 넣어주게 됩니다.


위 코드를 바로 실행하고 브라우저에서 아래와 같이 호출해 보겠습니다.

  • http://localhost:8000/
  • http://localhost:8000/abc
  • http://localhost:8000/xyz

모두 동일한 내용이 출력됩니다.


2. Manual request parsing

이번에는 위 코드를 좀 더 추가해 보겠습니다. ServeHTTP 함수 안에서 요청되는 경로를 구분해 처리하겠습니다.

package main

import (
	"fmt"
	"log"
	"net/http"
)

type pounds float32

func (p pounds) String() string {
	return fmt.Sprintf("£%.2f", p)
}

type database map[string]pounds

func (d database) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch r.URL.Path {
	case "/foo":
		fmt.Fprintf(w, "foo: %s\n", d["foo"])
	case "/bar":
		fmt.Fprintf(w, "bar: %s\n", d["bar"])
	default:
		w.WriteHeader(http.StatusNotFound)
		fmt.Fprintf(w, "No page found for: %s\n", r.URL)
	}
}

func main() {
	db := database{
		"foo": 1,
		"bar": 2,
	}

	log.Fatal(http.ListenAndServe("localhost:8000", db))
}

ServeHTTP 함수 안에 switch 문을 넣어 request되는 경로에 따라 다르게 출력했습니다. 

그리고 지정된 경로가 아닐 경우 404 메시지를 보냅니다. 404 메시지를 지정해 보내려면 WriteHeader()를 사용합니다.

참고로 WriteHeader의 source를 확인하고 싶다면 godoc -src 로 확인할 수 있습니다.

$ godoc -src net/http WriteHeader | less

코드를 실행하고 지정된 경로를 호출해 봅니다.

다른 경로를 호출했을 경우 아래와 같이 호출된 경로를 출력하고 header에는 404 메시지를 보냅니다.


3. Multiplexer

위 코드에서 switch로 분기된 부분을 함수로 만들어 주겠습니다. 그리고 이번에는 ServeHTTP 대신 ServeMux를 사용하겠습니다.

ServeMux는 HandleFunc를 제공하며 개발 함수를 바로 사용할 수 있습니다.

package main

import (
	"fmt"
	"log"
	"net/http"
)

type pounds float32

func (p pounds) String() string {
	return fmt.Sprintf("£%.2f", p)
}

type database map[string]pounds

func (d database) foo(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "foo: %s\n", d["foo"])
}

func (d database) bar(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "bar: %s\n", d["bar"])
}

func (d database) baz(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "baz: %s\n", d["baz"])
}

func main() {
	db := database{
		"foo": 1,
		"bar": 2,
		"baz": 3,
	}

	mux := http.NewServeMux()

	mux.Handle("/foo", http.HandlerFunc(db.foo))
	mux.Handle("/bar", http.HandlerFunc(db.bar))

	// Convenience method for longer form mux.Handle
	mux.HandleFunc("/baz", db.baz)

	log.Fatal(http.ListenAndServe("localhost:8000", mux))
}

ServeMux 인스턴스는 Multiplexer이며 ListenAdnServe의 두 번째 인자로 들어갈 수 있습니다. ServeMux를 사용하려면 http.NewServeMux를 선언해야 합니다.


HandlerFunc()과 HandleFunc()의 차이를 알고 싶다면 godoc을 확인해 보세요.

$ godoc net/http HandlerFunc | less

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)

    ServeHTTP calls f(w, r).


$ godoc net/http HandleFunc | less

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

    HandleFunc registers the handler function for the given pattern in the

    DefaultServeMux. The documentation for ServeMux explains how patterns

    are matched.


type ServeMux struct {

    // contains filtered or unexported fields

}


    ServeMux is an HTTP request multiplexer. It matches the URL of each

    incoming request against a list of registered patterns and calls the

    handler for the pattern that most closely matches the URL.


4. Global multiplexer

Web server를 만들다 보면 기능별로 여러개의 package를 만들어 사용하게 됩니다. ServeMux를 각 package별로 만들어야 되는데 그 대신에 Go의 Global DefaultServeMux를 사용하는 방법이 있습니다. DefaultServeMux를 사용하면 ListenAndServe의 두 번째 인자에 nil이 들어가게 됩니다.

package main

import (
	"fmt"
	"log"
	"net/http"
)

type pounds float32

func (p pounds) String() string {
	return fmt.Sprintf("£%.2f", p)
}

type database map[string]pounds

func (d database) foo(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "foo: %s\n", d["foo"])
}

func (d database) bar(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "bar: %s\n", d["bar"])
}

func (d database) baz(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "baz: %s\n", d["baz"])
}

func main() {
	db := database{
		"foo": 1,
		"bar": 2,
		"baz": 3,
	}

	http.HandleFunc("/foo", db.foo)
	http.HandleFunc("/bar", db.bar)
	http.HandleFunc("/baz", db.baz)

	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

여까지 코드로 확인한 Handler와 Multiplexer에 대한 설명이었습니다.



'Golang > Web App' 카테고리의 다른 글

Header와 body 확인하기  (1) 2019.02.09
체이닝 함수  (0) 2019.02.06
핸들링 요청과 핸들링 함수  (0) 2019.02.02
첫 번째 웹 서버(Hello Tongchun!)  (0) 2019.01.31
Comments