Gin: Go언어 스타일랑의 선도 프레임워크
Min-jun Kim
Dev Intern · Leapcell

소개

Gin은 Go(Golang)로 작성된 HTTP 웹 프레임워크입니다. Martini와 유사한 API를 제공하지만, Martini보다 최대 40배 빠른 성능을 자랑합니다. 엄청난 성능이 필요하다면 Gin을 사용해 보세요.
Gin의 공식 웹사이트에서는 Gin을 "고성능"과 "뛰어난 생산성"을 갖춘 웹 프레임워크로 소개합니다. 또한 Martini라는 다른 라이브러리도 언급하고 있는데, 이는 웹 프레임워크이자 술의 이름이기도 합니다. Gin은 Martini의 API를 사용하지만 40배 더 빠르다고 말합니다. httprouter를 사용하는 것이 Martini보다 40배 더 빠른 중요한 이유입니다.
공식 웹사이트의 "기능" 중에서 8가지 주요 기능이 나열되어 있으며, 이러한 기능의 구현을 점차적으로 살펴볼 것입니다.
- 빠른 속도
- 미들웨어 지원
- 충돌 방지
- JSON 유효성 검사
- 라우트 그룹화
- 오류 관리
- 렌더링 내장/확장 가능
간단한 예제로 시작하기
공식 문서에 제공된 가장 작은 예제를 살펴보겠습니다.
package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // 0.0.0.0:8080에서 수신 대기 및 서비스 제공 }
이 예제를 실행한 다음 브라우저를 사용하여 http://localhost:8080/ping에 접속하면 "pong"을 얻을 수 있습니다.
이 예제는 매우 간단합니다. 단 세 단계로 나눌 수 있습니다.
gin.Default()를 사용하여 기본 구성으로Engine객체를 만듭니다.Engine의GET메서드에서 "/ping" 주소에 대한 콜백 함수를 등록합니다. 이 함수는 "pong"을 반환합니다.Engine을 시작하여 포트 수신을 시작하고 서비스를 제공합니다.
HTTP 메서드
위의 작은 예제의 GET 메서드에서 볼 수 있듯이 Gin에서는 HTTP 메서드의 처리 방법을 동일한 이름의 해당 함수를 사용하여 등록해야 합니다.
9개의 HTTP 메서드가 있으며, 가장 일반적으로 사용되는 4가지 메서드는 각각 쿼리, 삽입, 업데이트 및 삭제의 4가지 기능을 나타내는 GET, POST, PUT 및 DELETE입니다. Gin은 또한 모든 HTTP 메서드 처리 방법을 하나의 주소에 직접 바인딩할 수 있는 Any 인터페이스를 제공합니다.
반환된 결과는 일반적으로 두 개 또는 세 개의 부분으로 구성됩니다. code와 message는 항상 존재하며, data는 일반적으로 추가 데이터를 나타내는 데 사용됩니다. 반환할 추가 데이터가 없는 경우 생략할 수 있습니다. 이 예에서 200은 code 필드의 값이고, "pong"은 message 필드의 값입니다.
엔진 변수 생성
위의 예에서는 gin.Default()를 사용하여 Engine을 만들었습니다. 그러나 이 함수는 New의 래퍼입니다. 실제로 Engine은 New 인터페이스를 통해 생성됩니다.
func New() *Engine { debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ //... RouterGroup의 필드 초기화 }, //... 나머지 필드 초기화 } engine.RouterGroup.engine = engine // RouterGroup에 엔진 포인터 저장 engine.pool.New = func() any { return engine.allocateContext() } return engine }
지금은 생성 프로세스를 간단히 살펴보고 Engine 구조체의 다양한 멤버 변수의 의미에 집중하지 마세요. Engine 유형의 engine 변수를 생성하고 초기화하는 것 외에도 New는 engine.pool.New를 engine.allocateContext()를 호출하는 익명 함수로 설정합니다. 이 함수의 기능은 나중에 설명합니다.
라우트 콜백 함수 등록
Engine에는 내장된 struct RouterGroup이 있습니다. Engine의 HTTP 메서드와 관련된 인터페이스는 모두 RouterGroup에서 상속됩니다. 공식 웹사이트에 언급된 기능 포인트의 "라우트 그룹화"는 RouterGroup struct를 통해 구현됩니다.
type RouterGroup struct { Handlers HandlersChain // 그룹 자체의 처리 함수 basePath string // 연결된 기본 경로 engine *Engine // 연결된 엔진 객체 저장 root bool // 루트 플래그, Engine에서 기본적으로 생성된 것만 true }
각 RouterGroup은 기본 경로 basePath와 연결되어 있습니다. Engine에 내장된 RouterGroup의 basePath는 "/"입니다.
또한 일련의 처리 함수 Handlers가 있습니다. 이 그룹과 연결된 경로에 대한 모든 요청은 추가적으로 이 그룹의 처리 함수를 실행하며, 이는 주로 미들웨어 호출에 사용됩니다. Handlers는 Engine이 생성될 때 nil이며, Use 메서드를 통해 일련의 함수를 가져올 수 있습니다. 이 사용법은 나중에 살펴볼 것입니다.
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() }
RouterGroup의 handle 메서드는 모든 HTTP 메서드 콜백 함수를 등록하기 위한 최종 진입점입니다. 초기 예제에서 호출된 GET 메서드 및 HTTP 메서드와 관련된 다른 메서드는 모두 handle 메서드의 래퍼일 뿐입니다.
handle 메서드는 RouterGroup의 basePath와 상대 경로 매개변수에 따라 절대 경로를 계산하고, 동시에 combineHandlers 메서드를 호출하여 최종 handlers 배열을 가져옵니다. 이러한 결과는 Engine의 addRoute 메서드에 매개변수로 전달되어 처리 함수를 등록합니다.
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) assert1(finalSize < int(abortIndex), "too many handlers") mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers }
combineHandlers 메서드가 하는 일은 슬라이스 mergedHandlers를 만들고, 그런 다음 RouterGroup 자체의 Handlers를 복사하고, 그런 다음 매개변수의 handlers를 복사하고, 마지막으로 mergedHandlers를 반환하는 것입니다. 즉, handle을 사용하여 메서드를 등록할 때 실제 결과에는 RouterGroup 자체의 Handlers가 포함됩니다.
Radix 트리를 사용하여 라우트 검색 가속화
공식 웹사이트에 언급된 "빠른 속도" 기능 포인트에서 네트워크 요청의 라우팅은 radix 트리(Radix Tree)를 기반으로 구현된다고 언급되어 있습니다. 이 부분은 Gin에서 구현된 것이 아니라 처음에 Gin의 소개에서 언급된 httprouter에서 구현됩니다. Gin은 httprouter를 사용하여 이 부분의 기능을 구현합니다. radix 트리의 구현은 여기서는 언급하지 않겠습니다. 지금은 사용법에만 집중하겠습니다. 아마도 나중에 radix 트리 구현에 대한 별도의 기사를 쓸 것입니다.
Engine에는 methodTree 구조체의 슬라이스인 trees 변수가 있습니다. 이 변수는 모든 radix 트리에 대한 참조를 보유합니다.
type methodTree struct { method string // 메서드 이름 root *node // 연결 목록의 루트 노드에 대한 포인터 }
Engine은 각 HTTP 메서드에 대한 radix 트리를 유지 관리합니다. 이 트리의 루트 노드와 메서드 이름은 methodTree 변수에 함께 저장되고, 모든 methodTree 변수는 trees에 있습니다.
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { //... 일부 코드 생략 root := engine.trees.get(method) if root == nil { root = new(node) root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root}) } root.addRoute(path, handlers) //... 일부 코드 생략 }
Engine의 addRoute 메서드는 먼저 trees의 get 메서드를 사용하여 method에 해당하는 radix 트리의 루트 노드를 가져옵니다. radix 트리의 루트 노드를 얻지 못하면 이전에 이 method에 등록된 메서드가 없음을 의미하며, 트리 노드를 트리의 루트 노드로 생성하고 trees에 추가합니다.
루트 노드를 가져온 후 루트 노드의 addRoute 메서드를 사용하여 경로 path에 대한 일련의 처리 함수 handlers를 등록합니다. 이 단계는 path 및 handlers에 대한 노드를 만들고 radix 트리에 저장하는 것입니다. 이미 등록된 주소를 등록하려고 하면 addRoute는 직접 panic 오류를 발생시킵니다.
HTTP 요청을 처리할 때 path를 통해 해당 노드의 값을 찾아야 합니다. 루트 노드에는 쿼리 작업을 처리하는 getValue 메서드가 있습니다. Gin에서 HTTP 요청을 처리할 때 언급하겠습니다.
미들웨어 처리 함수 가져오기
RouterGroup의 Use 메서드는 일련의 미들웨어 처리 함수를 가져올 수 있습니다. 공식 웹사이트에 언급된 기능 포인트의 "미들웨어 지원"은 Use 메서드를 통해 구현됩니다.
초기 예제에서 Engine struct 변수를 만들 때 New가 사용되지 않고 Default가 사용되었습니다. Default가 추가로 수행하는 작업을 살펴보겠습니다.
func Default() *Engine { debugPrintWARNINGDefault() // 로그 출력 engine := New() // 객체 생성 engine.Use(Logger(), Recovery()) // 미들웨어 처리 함수 가져오기 return engine }
매우 간단한 함수입니다. New를 호출하여 Engine 객체를 만드는 것 외에도 Use를 호출하여 두 개의 미들웨어 함수 Logger와 Recovery의 반환 값을 가져옵니다. Logger의 반환 값은 로깅을 위한 함수이고, Recovery의 반환 값은 panic을 처리하기 위한 함수입니다. 지금은 건너뛰고 나중에 이 두 함수를 살펴보겠습니다.
Engine은 RouterGroup을 내장하지만 Use 메서드도 구현하지만 RouterGroup의 Use 메서드와 일부 보조 작업을 호출하는 것일 뿐입니다.
func (engine *Engine) Use(middleware...HandlerFunc) IRoutes { engine.RouterGroup.Use(middleware...) engine.rebuild404Handlers() engine.rebuild405Handlers() return engine } func (group *RouterGroup) Use(middleware...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() }
RouterGroup의 Use 메서드도 매우 간단합니다. 매개변수의 미들웨어 처리 함수를 append를 통해 자체 Handlers에 추가합니다.
실행 시작
작은 예제에서 마지막 단계는 매개변수 없이 Engine의 Run 메서드를 호출하는 것입니다. 호출 후 전체 프레임워크가 실행을 시작하고 브라우저로 등록된 주소를 방문하면 콜백이 올바르게 트리거될 수 있습니다.
func (engine *Engine) Run(addr...string) (err error) { //... 일부 코드 생략 address := resolveAddress(addr) // 주소 구문 분석, 기본 주소는 0.0.0.0:8080 debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine.Handler()) return }
Run 메서드는 주소 구문 분석 및 서비스 시작이라는 두 가지 작업만 수행합니다. 여기서 주소는 실제로 문자열만 전달하면 되지만 전달하거나 전달하지 않을 수 있는 효과를 얻기 위해 가변 매개변수가 사용됩니다. resolveAddress 메서드는 addr의 다양한 상황 결과를 처리합니다.
서비스 시작은 표준 라이브러리의 net/http 패키지에서 ListenAndServe 메서드를 사용합니다. 이 메서드는 수신 대기 주소와 Handler 인터페이스의 변수를 허용합니다. Handler 인터페이스의 정의는 매우 간단하며 하나의 ServeHTTP 메서드만 있습니다.
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } type Handler interface { ServeHTTP(ResponseWriter, *Request) }
Engine은 ServeHTTP를 구현하므로 Engine 자체가 여기에서 ListenAndServe 메서드에 전달됩니다. 모니터링된 포트에 대한 새로운 연결이 있으면 ListenAndServe는 연결을 수락하고 설정하는 역할을 하며, 연결에 데이터가 있으면 handler의 ServeHTTP 메서드를 호출하여 처리합니다.
메시지 처리
Engine의 ServeHTTP는 메시지 처리를 위한 콜백 함수입니다. 그 내용을 살펴보겠습니다.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.reset() engine.handleHTTPRequest(c) engine.pool.Put(c) }
콜백 함수에는 두 개의 매개변수가 있습니다. 첫 번째는 요청 응답을 수신하는 데 사용되는 w입니다. 응답 데이터를 w에 씁니다. 다른 하나는 이 요청의 데이터를 보유하는 req입니다. 후속 처리에 필요한 모든 데이터는 req에서 읽을 수 있습니다.
ServeHTTP 메서드는 네 가지 작업을 수행합니다. 먼저 pool 풀에서 Context를 가져오고, 그런 다음 Context를 콜백 함수의 매개변수에 바인딩하고, 그런 다음 Context를 매개변수로 사용하여 handleHTTPRequest 메서드를 호출하여 이 네트워크 요청을 처리하고, 마지막으로 Context를 풀에 다시 넣습니다.
먼저 handleHTTPRequest 메서드의 핵심 부분만 살펴보겠습니다.
func (engine *Engine) handleHTTPRequest(c *Context) { //... 일부 코드 생략 t := engine.trees for i, tl := 0, len(t); i < tl; i++ { if t[i].method!= httpMethod { continue } root := t[i].root // 트리에서 경로 찾기 value := root.getValue(rPath, c.params, c.skippedNodes, unescape) //... 일부 코드 생략 if value.handlers!= nil { c.handlers = value.handlers c.fullPath = value.fullPath c.Next() c.writermem.WriteHeaderNow() return } //... 일부 코드 생략 } //... 일부 코드 생략 }
handleHTTPRequest 메서드는 주로 두 가지 작업을 수행합니다. 먼저 요청 주소에 따라 이전에 등록된 메서드를 radix 트리에서 가져옵니다. 여기서 handlers는 이 처리를 위해 Context에 할당되고, 그런 다음 Context의 Next 함수를 호출하여 handlers의 메서드를 실행합니다. 마지막으로 이 요청의 반환 데이터를 Context의 responseWriter 유형 객체에 씁니다.
컨텍스트
HTTP 요청을 처리할 때 모든 컨텍스트 관련 데이터는 Context 변수에 있습니다. 작성자는 또한 Context struct의 주석에 "Context는 gin에서 가장 중요한 부분입니다."라고 작성하여 중요성을 보여줍니다.
위의 Engine의 ServeHTTP 메서드에 대해 이야기할 때 Context가 직접 생성되지 않고 Engine의 pool 변수의 Get 메서드를 통해 얻는 것을 알 수 있습니다. 꺼낸 후에는 사용하기 전에 상태가 재설정되고 사용 후에는 다시 풀에 넣어집니다.
Engine의 pool 변수는 sync.Pool 유형입니다. 지금은 Go에서 제공하는 동시 사용을 지원하는 객체 풀이라고만 알아두세요. Get 메서드를 통해 풀에서 객체를 가져올 수 있으며, Put 메서드를 사용하여 풀에 객체를 넣을 수도 있습니다. 풀이 비어 있고 Get 메서드가 사용되면 자체 New 메서드를 통해 객체를 생성하여 반환합니다.
이 New 메서드는 Engine의 New 메서드에 정의되어 있습니다. Engine의 New 메서드를 다시 살펴보겠습니다.
func New() *Engine { //... 다른 코드 생략 engine.pool.New = func() any { return engine.allocateContext() } return engine }
코드에서 Context의 생성 메서드는 Engine의 allocateContext 메서드라는 것을 알 수 있습니다. allocateContext 메서드에는 비밀이 없습니다. 슬라이스 길이를 두 단계로 미리 할당한 다음 객체를 만들고 반환합니다.
func (engine *Engine) allocateContext() *Context { v := make(Params, 0, engine.maxParams) skippedNodes := make([]skippedNode, 0, engine.maxSections) return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes} }
위에서 언급한 Context의 Next 메서드는 handlers의 모든 메서드를 실행합니다. 구현을 살펴보겠습니다.
func (c *Context) Next() { c.index++ for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } }
handlers는 슬라이스이지만 Next 메서드는 단순히 handlers를 순회하는 것으로 구현되지 않고 처리 진행 기록 index를 도입합니다. index는 0으로 초기화되고 메서드의 시작 부분에서 증가하고 메서드 실행이 완료된 후 다시 증가합니다.
Next의 디자인은 사용법과 큰 관련이 있으며 주로 일부 미들웨어 함수와 협력하기 위한 것입니다. 예를 들어 특정 handler 실행 중에 panic이 트리거되면 미들웨어에서 recover를 사용하여 오류를 포착한 다음 Next를 다시 호출하여 하나의 handler 문제로 인해 전체 handlers 배열에 영향을 주지 않고 후속 handlers를 계속 실행할 수 있습니다.
패닉 처리
Gin에서 특정 요청의 처리 함수가 panic을 트리거하면 전체 프레임워크가 직접 충돌하지 않습니다. 대신 오류 메시지가 throw되고 서비스가 계속 제공됩니다. Lua 프레임워크가 일반적으로 xpcall을 사용하여 메시지 처리 함수를 실행하는 방법과 다소 유사합니다. 이 작업은 공식 문서에 언급된 "충돌 방지" 기능 포인트입니다.
위에서 언급했듯이 gin.Default를 사용하여 Engine을 만들 때 Engine의 Use 메서드가 실행되어 두 개의 함수를 가져옵니다. 그 중 하나는 다른 함수의 래퍼인 Recovery 함수의 반환 값입니다. 최종적으로 호출되는 함수는 CustomRecoveryWithWriter입니다. 이 함수의 구현을 살펴보겠습니다.
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc { //... 다른 코드 생략 return func(c *Context) { defer func() { if err := recover(); err!= nil { //... 오류 처리 코드 } }() c.Next() // 다음 핸들러 실행 } }
여기서 오류 처리의 세부 사항에 집중하지 않고 수행하는 작업만 살펴보겠습니다. 이 함수는 익명 함수를 반환합니다. 이 익명 함수에서는 defer를 사용하여 다른 익명 함수를 등록합니다. 이 내부 익명 함수에서는 recover를 사용하여 panic을 포착한 다음 오류 처리를 수행합니다. 처리가 완료된 후 Context의 Next 메서드가 호출되므로 원래 순차적으로 실행되던 Context의 handlers가 계속 실행될 수 있습니다.
Leapcell: 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼
마지막으로 Gin 서비스를 배포하기 위한 최고의 플랫폼인 Leapcell을 소개합니다.

1. 다중 언어 지원
- JavaScript, Python, Go 또는 Rust로 개발하세요.
2. 무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하세요. 요청, 요금이 없습니다.
3. 최고의 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하세요.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
4. 간소화된 개발자 경험
- 손쉬운 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
5. 손쉬운 확장성 및 고성능
- 높은 동시성을 쉽게 처리하도록 자동 확장됩니다.
- 운영 오버헤드가 없습니다. 구축에만 집중하세요.
문서!에서 자세히 알아보세요.
Leapcell 트위터: https://x.com/LeapcellHQ

