ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Go 언어의 net/http 패키지의 http.HandleFunc이 실행되는 방식 이해하기
    Go 2024. 3. 3. 16:32

     

    안녕하세요?

     

    오늘은 Go 기본 문법을 알고 계시는 분 혹은 Request 요청이 들어왔을 때 함수가 어떻게 실행되는지 궁금하신 분, 또는 Go의 웹 응용 프레임워크를 능숙하게 활용하고 싶은 분들을 위해 조금은 지루한 글을 쓸까 합니다.

     

    웹 응용 프로그램을 개발할 때, 요청이 들어오면 해당 경로에 따라 함수를 실행해야 합니다.

     

    그런데 웹 응용 프레임워크를 사용하면 내부에서 어떤 처리가 이루어지는지 몰라도 동작시킬 수 있습니다.

     

    하지만 웹 응용 프레임워크를 올바르게 활용하려면 내부 구현에 대한 이해가 필요합니다.

     

    그래서 이번 기회에 Go의 net/http 패키지 문서를 참조하여 요청이 들어왔을 때 함수가 어떻게 실행되는지 조사해보았습니다.

     

    특히 http.Handlehttp.HandleFunc과 같은 유사한 이름 때문에 처음에는 혼란스러울 수 있지만, 계속 읽다보면 이해할 수 있을 거라 보고 글 진행하겠습니다.

     

    ** 목 차 **


    기본

    다음과 같은 웹 응용 프레임워크를 사용하지 않았을 때 일반적인 HTTP 핸들러가 어떻게 호출되는지 조사했습니다.

     

    웹 응용 프레임워크를 사용하면 func(w http.ResponseWriter, req *http.Request)와 같은 http.HandleFunc 인터페이스를 직접 구현할 필요는 없지만, 결국 내부적으로는 Go의 net/http 패키지를 사용하여 이러한 HTTP 핸들러를 생성하고 있습니다.

    package main
    
    import (
        "net/http"
    )
    
    func main() {
    
      http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
      })
    
      http.ListenAndServe(":8080", nil)
    }

     

    위 코드들이 실제 이글의 모든 것이라고 말해도 과언이 아닙니다.

     

    그래도 조금은 상세하게 살펴 볼까 합니다.


    자주 사용되는 유형

    먼저 설명하기 전에 자주 사용되는 두 가지 유형에 대해 이해해야 합니다.

    Handler 인터페이스

    요청에 응답하는 호출되는 핸들러가 충족해야 하는 인터페이스입니다.

     

    이후에 자세히 살펴보겠지만, 등록된 핸들러가 실행될 때는 ServeHTTP 메서드가 호출됩니다.

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

    HandlerFunc 함수 유형

    func(ResponseWriter, *Request)를 다른 유형으로 정의한 것으로, HTTP 핸들러와 같이 사용할 수 있도록 하는 함수 유형입니다.

     

    또한 이 함수 유형은 ServeHTTP를 가지고 있습니다.

    // https://golang.org/src/net/http/server.go?s=59707:59754#L1950
    type HandlerFunc func(ResponseWriter, *Request)
    
    // ServeHTTP calls f(w, r).
    func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
        f(w, r)
    }

     

    자주 사용되는 방법 중 하나는 다음과 같은 함수를 HandlerFunc 유형으로 캐스팅하여 HandlerFunc 유형의 ServeHTTP 메서드를 추가하는 것입니다.

    HandlerFunc(func(ResponseWriter, *http.Request))

     

    이로써 위의 Handler 인터페이스를 함수가 충족시킬 수 있습니다.


    요청되면 핸들러가 호출되기까지

    http.ListenAndServe

    처음 코드에서는 마지막에 http.ListenAndServe에 포트와 nil을 전달하고 있습니다.

    package main
    
    import (
        "net/http"
    )
    
    func main() {
        ...
        http.ListenAndServe(":8080", nil)
    }

    이는 주소와 Handler 인터페이스 유형을 구조체에 가지고 있으며, http.Server.ListenAndServe를 호출하고 있습니다.

     

    이로써 HTTP 서버가 지정된 주소에서 수신 대기하고, 요청이 있으면 등록한 핸들러(Handler 인터페이스 유형)를 호출합니다.

     

    즉, ServeHTTP가 호출됩니다.

     

    이 핸들러가 nil인 경우 DefaultServeMux가 사용됩니다.

     

    DefaultServeMux는 나중에 자세히 살펴보겠습니다.

    ListenAndServe starts an HTTP server with a given address and handler. The handler is usually nil, which means to use DefaultServeMux. Handle and HandleFunc add handlers to DefaultServeMux:

     

    아래는 번역입니다.

    ListenAndServe는 주어진 주소와 핸들러로 HTTP 서버를 시작합니다. 핸들러는 일반적으로 nil이며, 이는 DefaultServeMux를 사용한다는 의미입니다. Handle 및 HandleFunc은 DefaultServeMux에 핸들러를 추가합니다.

    // https://golang.org/src/net/http/server.go?s=93229:93284#L2992
    func ListenAndServe(addr string, handler Handler) error {
        server := &Server{Addr: addr, Handler: handler}
        return server.ListenAndServe()
    }

     

    그리고 이 http.Server.ListenAndServe는 그 이름 그대로 지정된 포트를 수신 대기하고, 요청이 있으면 http.Server.Serve를 호출합니다.

    // https://golang.org/src/net/http/server.go?s=85391:85432#L2742
    func (srv *Server) ListenAndServe() error {
            ...
        addr := srv.Addr
            ...
        ln, err := net.Listen("tcp", addr)
            ...
        return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
    }

    http.Serve

    그럼, 지정된 포트로 연결 요청이 들어오면 호출되는 http.Serve가 무엇을 하는지 살펴보겠습니다.

     

    이 함수는 연결을 수락하고 요청에 따라 핸들러(Handler 인터페이스 유형을 충족시키는 ServeHTTP를 구현한)를 호출하기 위한 고루틴을 생성합니다.

     

    내부적으로는 http.conn.serve를 호출하고, 이는 미리 ListenAndServe에서 등록한 DefaultServeMux의 ServeHTTP를 호출합니다.

    // https://golang.org/src/net/http/server.go?s=87455:87501#L2795
    func (srv *Server) Serve(l net.Listener) error {
            ....
        baseCtx := context.Background() // base is always background, per Issue 16220
        ctx := context.WithValue(baseCtx, ServerContextKey, srv)
        for {
            rw, e := l.Accept()
                    ...
            c := srv.newConn(rw)
            c.setState(c.rwc, StateNew) // before Serve can return
            go c.serve(ctx) // 여기
        }
    }
    
    // src/net/http/server.go#L1739
    // Serve a new connection.
    func (c *conn) serve(ctx context.Context) {
            ...
        for {
            w, err := c.readRequest(ctx)
                    ...
            // HTTP cannot have multiple simultaneous active requests.[*]
            // Until the server replies to this request, it can't read another,
            // so we might as well run the handler in this goroutine.
            // [*] Not strictly true: HTTP pipelining. We could let them all process
            // in parallel even if their responses need to be serialized.
            // But we're not going to implement HTTP pipelining because it
            // was never deployed in the wild and the answer is HTTP/2.
            serverHandler{c.server}.ServeHTTP(w, w.req)
                    ...
        }
    }

     

    요약하면, ListenAndServe에서 지정된 포트를 바인드하고 수신 대기하며 요청이 있으면 고루틴을 생성하여 DefaultServeMux의 ServeHTTP를 호출합니다.

    http.ServeMux 구조체

    그럼, DefaultServeMux는 무엇일까요?

     

    이는 ServeMux 구조체의 포인터가 저장된 변수입니다.

     

    기본적으로 위에서 생성한 http.Serve의 고루틴에서 호출됩니다.

     

    Mux는 Multiplexer의 약자입니다.

     

    Multiplexer는 두 개 이상의 입력을 하나의 신호로 출력하는 메커니즘을 의미합니다.

    // src/net/http/server.go#L2164
    // DefaultServeMux is the default ServeMux used by Serve.
    var DefaultServeMux = &defaultServeMux
    
    var defaultServeMux ServeMux

     

    그렇다면 이 ServeMux 구조체는 무엇일까요?

     

    URL 패턴을 매칭하여 가장 가까운 경로에 등록된 핸들러를 호출합니다.

     

    경로 패턴 매칭에는 다양한 규칙이 있으며, 다양한 URL 패턴과 핸들러 호출 로직을 정의하고 있는 것으로 보입니다.

     

    예를 들어, 긴 경로가 우선되고 해당 경로에 해당하는 핸들러가 호출되거나, 하위 트리 경로가 "/"로 끝나는 경우 우선되는 등의 규칙이 있습니다.

    http.ServeMux.ServeHTTP

    그리고, http.ServeMux는 Handler 인터페이스(ServeHTTP를 구현)를 만족합니다.

     

    이는 가장 가까운 경로에 있는 Handler 인터페이스를 구현한 함수를 호출합니다.

    // https://golang.org/src/net/http/server.go?s=71923:71983#L2342
    // ServeHTTP dispatches the request to the handler whose
    // pattern most closely matches the request URL.
    func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
      ...
      // https://golang.org/pkg/net/http/#ServeMux.Handler
      // http.ServeMux.Handler에 의해 요청에 맞는 등록된 핸들러를 선택합니다.
        h, _ := mux.Handler(r)
      // 그리고 해당 핸들러의 ServeHTTP를 호출합니다.
      // 즉, 이 핸들러는 Handler 인터페이스를 구현하고 있습니다.
        h.ServeHTTP(w, r)
    }

     

    경로에 맞는 Handler에 대해 ServeHTTP를 호출하고 있습니다.

     

    내부적으로는 http.ServeMux.match를 사용하여 경로에서 Handler를 찾고 있습니다.

    // src/net/http/server.go##L2342
    // handler is the main implementation of Handler.
    // The path is known to be in canonical form, except for CONNECT methods.
    func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
        mux.mu.RLock()
        defer mux.mu.RUnlock()
    
        // Host-specific pattern takes precedence over generic ones
        if mux.hosts {
            h, pattern = mux.match(host + path)
        }
        if h == nil {
            h, pattern = mux.match(path)
        }
        if h == nil {
            h, pattern = NotFoundHandler(), ""
        }
        return
    }

     

    다음과 같이 http.ServeMux 구조체에 등록된 경로와 핸들러를 반환합니다.

    // src/net/http/server.go##L2218
    // Find a handler on a handler map given a path string.
    // Most-specific (longest) pattern wins.
    func (mux *ServeMux) match(path string) (h Handler, pattern string) {
        // Check for exact match first.
        v, ok := mux.m[path]
        if ok {
            return v.h, v.pattern
        }
      ...
    }

     

    요약하면, http.ServeMux.Handler를 호출하고 등록된 핸들러의 ServeHTTP 메서드를 실행합니다.

     

    다음으로, 해당 핸들러가 어떻게 http.HandleFunc에서 등록되었는지 살펴보겠습니다.


    핸들러를 등록하는 과정

    package main
    
    import (
        "net/http"
    )
    
    func main() {
      http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
      })
      ...
    }

    http.HandleFunc

    HandlerFunc 유형의 핸들러와 경로를 DefaultServeMux에 등록합니다.

     

    이는 내부적으로 http.ServeMux.HandleFunc를 호출하는 단축키일 뿐입니다.

    // https://golang.org/src/net/http/server.go?s=73427:73498#L2396
    func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
        DefaultServeMux.HandleFunc(pattern, handler)
    }

    http.ServeMux.HandleFunc

    받은 함수를 HandlerFunc 유형으로 캐스팅하여 Handler 인터페이스를 만족하도록 하고,

     

    http.ServeMux.Handle을 호출합니다.

    // https://golang.org/src/net/http/server.go?s=72834:72921#L2381
    // HandleFunc registers the handler function for the given pattern.
    func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
        if handler == nil {
            panic("http: nil handler")
        }
           // handler를 HandlerFunc 함수 유형으로 캐스팅하여 ServeHTTP를 추가합니다(Handler 인터페이스를 만족).
        mux.Handle(pattern, HandlerFunc(handler))
    }

    http.ServeMux.Handle

    받은 Handler를 경로와 매핑하여 map[string]muxEntry 유형의 값에 등록합니다.

    // https://golang.org/src/net/http/server.go?s=72291:72351#L2356
    mux.m[pattern] = muxEntry{h: handler, pattern: pattern}

     

    내부적으로 map[string]muxEntry 유형의 값에 경로(string)와 Handler 유형의 함수를 muxEntry 구조체로 래핑하여 매핑하고 있습니다.

    // https://golang.org/src/net/http/server.go?s=66347:66472#L2139
    type ServeMux struct {
        mu    sync.RWMutex
        m     map[string]muxEntry
        hosts bool // whether any patterns contain hostnames
    }
    
    type muxEntry struct {
        h       Handler
        pattern string
    }

    http.Handle

    참고로, http.Handle도 있지만, 이는 내부적으로 http.ServeMux.Handle을 호출하는 단축키일 뿐입니다.

     

    Handler 유형의 핸들러와 경로를 DefaultServeMux에 등록합니다.

    // https://golang.org/src/net/http/server.go?s=73173:73217#L2391
    func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

    소감

    • 이 Go의 HTTP 서버 메커니즘 설명은 HTTP 요청과 핸들러 간의 관계를 이해하는 데 도움이 됩니다.
    • 인터페이스를 활용하고 있어 유연하고 확장 가능한 구현입니다.
    • 다른 프로그램에서도 HTTP 서버를 구축할 때 이 메커니즘을 참고할 수 있을 것 같습니다.
Designed by Tistory.