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