본문 바로가기
Golang

gin은 어떻게 요청을 받는가

by Limm_jk 2023. 4. 23.

gin-gonic

golang에서 웹 어플리케이션을 만들기 위하여 다양한 라이브러리를 사용합니다. 그중 우리 팀은 Gin을 가장 많이 사용하고 있는데요.

문득 Gin은 자세히 어떻게 handler로 요청을 넘겨주는지 궁금해졌습니다. golang은 매지컬하게 뭔가를 끼워 넣는 것이 불가능하니, 추적하는 것이 크게 어려울 것 같지도 않아서 한번 시도해 보았습니다.

 

들어가기 전에 TL;DR

글 자체가 장황해져서, 간단하게 읽고 가고 싶으신 분을 위한 요약입니다.

요약은 글을 정리하면서 새로 알게 된 부분만 간단하게 정리합니다.

 

1. Server 생성 시, engine을 핸들러로 사용하여 request를 파싱한 후 넘겨준다.

2. gin의 context는 요청마다 생성되는 것이 아닌, pool이 있다. 이 pool에는 engine 생성 시, context를 생성하여 넣어주며, 요청이 들어왔을 때, 이 context를 꺼내서 잘 reset하여 사용한다.

3. handler를 찾는 방법은 method마다 tree 구조를 이루며 root는 / 부터 시작하여 path 구분마다 한 뎁스 씩 내려간다.

4. tree에서 못 찾으면 설정에 따라서 method not allowed(405) 혹은 not fount(404)를 주며, 이 response body는 변경할 수 없다. 


Engine.Run

Gin에서 서버를 켜기 위하여는 Engine을 생성하고, Run을 수행해야 합니다. Gin은 이런 간단한 함수 호출로 웹 어플리케이션 서버를 켤 수 있도록 도와주고 있습니다. 하지만, 요청을 받고 잘 라우팅 해주는 일련의 과정은 이렇게 간단할 리가 없겠죠? 조금 더 파고 들어보겠습니다.

그전에, Run에 간단한 주석이 적혀있는데, 한번 확인해 볼까요?

Run attaches the router to a http.Server and starts listening and serving HTTP requests. It is a shortcut for http.ListenAndServe(addr, router) Note: this method will block the calling goroutine indefinitely unless an error happens.

위 주석을 통하여 Run을 호출함으로 인하여 일어나는 일을 다음과 같이 정리할 수 있겠습니다.

  1. http.Server에 router를 연결한다.
  2. http.ListenAndServe를 이용하여 HTTP 요청을 listen하고 serve한다.
  3. calling goroutine을 오류가 발생하기 전까지 블락한다.

위의 일들이 어떻게 일어나는지 조금 더 탐색해 보겠습니다.

http.ListenAndServe

이 함수는 address와 handler를 파라미터로 받아서 수행됩니다.

코드를 보았을 때는 http.server struct를 생성(server := &Server{Addr: addr, Handler: handler})하고 server가 가지고 있는 메서드 ListenAndServe를 실행시켜 주는 간단한 역할을 수행하고 있는 것 같은데, 주석은 어떻게 설명하고 있는지 확인해 볼까요?

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.

간단히 정리해 보면 요청을 처리하기 위하여 serve를 호출하고, 허용된 연결은 TCP keep-alive를 지원하는 것 같네요. 그리고 handler에 대한 설명도 적혀있는데, gin의 경우는 따로 설정을 하지 않는다면 handler에 engine을 넘겨주고 있어서 nil인 경우는 없을 것 같네요.

그리고 해당 함수는 return 시, err 객체에 nil이 들어가는 경우는 없다고 합니다. 위의 engine.Run에서 3번에 언급된 ’오류가 발생하기 전까지 블락한다.’라는 속성과 관련이 있어 보이네요.

 

이 함수도 릴레잉의 느낌이 강하네요. 한 뎁스 더 들어가 볼게요.

server.ListenAndServe

ListenAndServe listens on the TCP network address srv.Addr and then calls Serve to handle requests on incoming connections. Accepted connections are configured to enable TCP keep-alives. If srv.Addr is blank, ":http" is used. ListenAndServe always returns a non-nil error. After Shutdown or Close, the returned error is ErrServerClosed.

메서드 명이 같은 만큼 하는 일도 거의 유사한 것 같습니다. 오류 명세가 조금 더 구체적 이어졌네요. 코드를 살펴보면 net.Listen과 같은 함수가 존재합니다.

 

뭔가 net.Listen에서 요청을 수신하기 위하여 대기할 줄 알았는데, listener만 넘겨주고 넘어가네요. 이 listener는 server.Serve 메서드로 넘겨집니다. 여기에서 사용하기 위한 listener를 생성하는 역할이라고 봐도 무방하겠네요. Serve 메서드로 넘어가 보겠습니다.

server.Serve

Serve accepts incoming connections on the Listener l, creating a new service goroutine for each. The service goroutines read requests and then call srv.Handler to reply to them. HTTP/2 support is only enabled if the Listener returns *tls.Conn connections and they were configured with "h2" in the TLS Config.NextProtos. Serve always returns a non-nil error and closes l. After Shutdown or Close, the returned error is ErrServerClosed.

여기가 요청을 받고 처리하는 중추적인 로직인 것 같습니다.

여기서 block이 걸림을 알 수 있는 부분이 존재하는데요.

다음과 같이 조건이 없는 for문이 걸려있어서 내부에서 return을 해주지 않으면 탈출할 수 없게 만들어두었네요.

 

그리고 이 for문의 최상단에는 listener를 이용한 Accept라는 메서드를 사용하고 있는 것을 볼 수 있습니다. 디버깅해서 보니까 이 시점에 block이 걸리는 것을 확인할 수 있었습니다. 내부를 타고 들어가 보았더니 TCPListener까지 이어지는 것을 볼 수 있었는데요. 어떻게 요청을 받는지까지 확인하는 것은 이번 글의 목적이 아니므로 여기까지만 보도록 하겠습니다.

 

이 시점까지 왔을 때 우리는 서버가 켜졌다고 말을 할 수 있습니다. 이제 accept라는 함수는 요청을 받을 때까지 waiting에 들어갔는데요. 이 waiting을 풀어주기 위하여 요청을 넣어보겠습니다.

 

그렇다면 아래 코드에서 block이 풀린다고 볼 수 있는데요.

rw, err := l.Accept()

이 waiting이 풀리는 시점에 rw라는 변수에 net.TCPConn을 전달하고, 오류가 발생하면 err에 값을 채워줍니다.

 

그리고 err가 nil이라는 요청을 수행하기 위하여 넘어가는데요. 이 과정에서 connection에 사용되는 context를 지정해주기도 하고, 새로운 connection을 현재 서버 객체와 accept를 통하여 받아온 rw(TCPConn)을 통하여 생성합니다. 최초의 engine에 대한 정보를 가진 server와 요청 정보를 가진 rw. 정보는 충분히 받았으니, 이로 인하여 생성된 connection을 이용하여 요청을 처리할 수 있겠네요.

 

그리고 이 이후의 과정은 go conn.serve(connCtx)를 통하여 새로운 goroutine으로 넘어가게 됩니다. 그리고 여기까지 마쳤다면, 위에서 이야기했던 것과 같이 for문으로 인하여 다시 Accept 메서드를 호출하게 되고 다음 요청을 받기 위하여 대기하게 됩니다.

conn.serve

여기까지 들어왔다면 상당히 복잡한 코드 블럭이 기다리고 있는데요.

TLS요청에 대한 핸들링 등을 처리해 주는 로직들인데, 이번 관심사는 아니기 때문에 넘어가고,

1916 line의 w, err := c.readRequest(ctx) 로 넘어와보겠습니다.

 

이 과정을 통하여 request를 읽어서 객체화시키고, response를 표현하는 객체를 만들어서 반환합니다.

 

request와 response를 책임지는 객체들인만큼 앞으로 요청주기 내내 따라다닐 친구라고 보시면 될 것 같고,

그에 걸맞게 1991 line에서 serverHandler{c.server}.ServeHTTP(w, w.req) 를 통하여 response 객체와 request 객체를 전해주게 됩니다.

ServeHTTP

해당 메서드로 들어오면 최초에 handler를 변수에 할당하는 작업을 진행하는데요. (handler := sh.srv.Handler)

앗. server의 handler. 앞에서 이야기하지 않았었나요? ListenAndServe에서 설정한 것과 같이 handler는 즉 gin.engine입니다. 이제 여기부터 gin의 handler처리로 넘어온다고 보면 좋을 것 같네요.

해당 메서드에서 handler.ServeHTTP(rw, req)를 호출하는데, rw는 response니까 요청의 생명주기를 위하여 넘겨준다고 인지하면 되겠습니다.

engine.ServeHTTP

gin의 내부로 들어왔습니다. 최초에 gin을 위한 context를 가져오는 일을 하네요. gin context는 생성되는 것이 아닌 최초 생성 시점에 pool에 할당 가능한 context를 넣어두고, 이를 할당받아서 진행됨을 알 수 있습니다. 어떻게 pool에 최초에 할당되는지 알고 싶으시다면 engine.allocateContext를 참고해 주세요.

다시 돌아와서 위 과정을 마치게 되면, response writer와 request를 가지고 있는 gin.context가 생기게 됩니다. 이 context를 넘겨가면서 요청을 처리하게 되겠네요.

engine.handleHTTPRequest

이제 어떤 핸들러가 이 요청을 처리할지 정하는 단계입니다.

사실 이 글을 시작하면서 gin이 어떤 식으로 해당하는 핸들러를 찾는지도 궁금했는데요. 아래 코드로 정리할 수 있을 것 같습니다.

위와 같이 tree로 관리하는 것을 알 수 있습니다. 그리고 tree는 method를 따라서 나뉘고, root는 / path로 이루어져 있습니다. 그리고 이후의 tree 구조는 path에 따라서 동일한 path 구분을 가지는 친구들로 나뉘게 됩니다.

 

예를 들면 get /user/follow/count라고 한다면 get tree의 root에서 시작하여 /user 서브트리, /follow 서브트리를 거쳐 count에 해당하는 핸들러 찾게 됩니다.

이 과정을 통하여 handler를 찾게 된다면 handler를 지정하고 c.Next를 걸어서 middleware를 순회하고 handler까지 도달하게 되겠네요.

댓글