Is net/http로 충분한가, 아니면 Gin이 필요한가?
Takashi Yamamoto
Infrastructure Engineer · Leapcell

net/http로 충분한가, 아니면 Gin이 필요한가?
I. 서론
Go 언어 생태계에서 표준 HTTP 라이브러리인 net/http 패키지는 강력하고 유연한 기능을 가지고 있습니다. 웹 애플리케이션을 구축하고 HTTP 요청을 처리하는 데 사용할 수 있습니다. 이 패키지는 Go 언어 표준 라이브러리에 속하므로 모든 Go 프로그램에서 직접 호출할 수 있습니다. 그러나 net/http와 같이 강력하고 유연한 표준 라이브러리가 이미 있는데도 불구하고 웹 애플리케이션 구축을 지원하는 Gin과 같은 타사 라이브러리가 여전히 등장하는 이유는 무엇일까요?
사실 이것은 net/http의 위치와 밀접한 관련이 있습니다. net/http 패키지는 기본적인 HTTP 기능을 제공하며, 고급 기능과 편리한 개발 경험을 제공하기보다는 단순성과 일반성에 중점을 두고 설계되었습니다. HTTP 요청을 처리하고 웹 애플리케이션을 구축하는 과정에서 개발자는 일련의 문제에 직면할 수 있으며, 이것이 바로 Gin과 같은 타사 라이브러리가 탄생한 이유입니다.
다음에서는 일련의 시나리오를 자세히 설명하고 이러한 시나리오에서 net/http와 Gin의 다양한 구현 방법을 비교하여 Gin 프레임워크의 필요성을 설명합니다.
II. 복잡한 라우팅 시나리오 처리
웹 애플리케이션의 실제 개발 과정에서 동일한 라우팅 접두사를 사용하는 것은 매우 일반적입니다. 다음은 비교적 일반적인 두 가지 예입니다.
API를 설계할 때 API는 시간이 지남에 따라 업데이트되고 개선되어야 하는 경우가 많습니다. 이전 버전과의 호환성을 유지하고 여러 API 버전이 공존할 수 있도록 하기 위해 일반적으로 /v1, /v2 등과 같은 라우팅 접두사를 사용하여 API의 여러 버전을 구별합니다.
또 다른 시나리오는 대규모 웹 애플리케이션이 일반적으로 여러 모듈로 구성되며 각 모듈은 서로 다른 기능을 담당합니다. 코드를 보다 효과적으로 구성하고 서로 다른 모듈의 경로를 구별하기 위해 모듈 이름이 라우팅 접두사로 자주 사용됩니다.
위의 두 시나리오에서는 동일한 라우팅 접두사를 사용할 가능성이 높습니다. net/http를 사용하여 웹 애플리케이션을 구축하는 경우 구현은 대략 다음과 같습니다.
package main import ( "fmt" "net/http" ) func handleUsersV1(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "v1의 사용자 목록") } func handlePostsV1(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "v1의 게시물 목록") } func main() { http.HandleFunc("/v1/users", handleUsersV1) http.HandleFunc("/v1/posts", handlePostsV1) http.ListenAndServe(":8080", nil) }
위의 예제에서는 http.HandleFunc를 수동으로 호출하여 서로 다른 라우팅 처리 함수를 정의합니다. 이 코드 예제를 보면 명백한 문제가 없는 것 같지만, 현재 라우팅 그룹이 두 개뿐이기 때문입니다. 경로 수가 계속 증가하면 처리 함수 수도 증가하고 코드는 점점 더 복잡하고 길어집니다. 또한 각 라우팅 규칙에는 예제의 v1 접두사와 같이 라우팅 접두사를 수동으로 설정해야 합니다. 접두사가 /v1/v2/...와 같이 복잡한 형태인 경우 설정 프로세스는 코드 아키텍처를 불분명하게 만들 뿐만 아니라 매우 번거롭고 오류가 발생하기 쉽습니다.
이와는 대조적으로 Gin 프레임워크는 라우팅 그룹 기능을 구현합니다. 다음은 Gin 프레임워크에서 이 기능의 구현 코드입니다.
package main import ( "fmt" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() // 라우팅 그룹 생성 v1 := router.Group("/v1") { v1.GET("/users", func(c *gin.Context) { c.String(200, "v1의 사용자 목록") }) v1.GET("/posts", func(c *gin.Context) { c.String(200, "v1의 게시물 목록") }) } router.Run(":8080") }
위의 예에서는 router.Group을 사용하여 v1 라우팅 접두사가 있는 라우팅 그룹을 만듭니다. 라우팅 규칙을 설정할 때 라우팅 접두사를 다시 설정할 필요가 없으며 프레임워크에서 자동으로 어셈블합니다. 동시에 동일한 라우팅 접두사를 가진 규칙은 모두 동일한 코드 블록에 유지 관리됩니다. net/http 코드 라이브러리와 비교할 때 Gin은 코드 구조를 더 명확하고 관리하기 쉽게 만듭니다.
III. 미들웨어 처리
웹 애플리케이션 요청을 처리하는 과정에서 특정 비즈니스 로직을 실행하는 것 외에도 일반적으로 인증 작업, 오류 처리 또는 로그 인쇄 기능 등과 같은 몇 가지 일반적인 로직을 미리 실행해야 합니다. 이러한 로직을 총칭하여 미들웨어 처리 로직이라고 하며 실제 응용 프로그램에서 없어서는 안 될 경우가 많습니다.
먼저 오류 처리에 대해 살펴보겠습니다. 응용 프로그램을 실행하는 동안 데이터베이스 연결 실패, 파일 읽기 오류 등과 같은 일부 내부 오류가 발생할 수 있습니다. 합리적인 오류 처리는 이러한 오류로 인해 전체 응용 프로그램이 충돌하는 것을 방지하고 대신 적절한 오류 응답을 통해 클라이언트에 알릴 수 있습니다.
인증 작업의 경우 많은 웹 처리 시나리오에서 사용자는 특정 제한된 리소스에 액세스하거나 특정 작업을 수행하기 전에 인증을 받아야 하는 경우가 많습니다. 동시에 인증 작업은 사용자 권한을 제한하고 사용자의 무단 액세스를 방지하여 프로그램의 보안을 향상시킬 수도 있습니다.
따라서 전체 HTTP 요청 처리 로직에는 이러한 미들웨어 처리 로직이 필요할 가능성이 매우 높습니다. 이론적으로 프레임워크 또는 라이브러리는 미들웨어 로직에 대한 지원을 제공해야 합니다. 먼저 net/http가 어떻게 구현하는지 살펴보겠습니다.
package main import ( "fmt" "log" "net/http" ) // 오류 처리 미들웨어 func errorHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { http.Error(w, "내부 서버 오류", http.StatusInternalServerError) log.Printf("Panic: %v", err) } }() next.ServeHTTP(w, r) }) } // 인증 미들웨어 func authMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 인증 시뮬레이션 if r.Header.Get("Authorization") != "secret" { http.Error(w, "인증되지 않음", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } // 비즈니스 로직 처리 func helloHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "안녕하세요, 세계!") } // 추가적으로 func anotherHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "다른 엔드포인트") } func main() { // 경로 핸들러 생성 router := http.NewServeMux() // 미들웨어 적용 및 핸들러 등록 handler := errorHandler(authMiddleware(http.HandlerFunc(helloHandler))) router.Handle("/", handler) // 미들웨어 적용 및 다른 요청에 대한 핸들러 등록 another := errorHandler(authMiddleware(http.HandlerFunc(anotherHandler))) router.Handle("/another", another) // 서버 시작 http.ListenAndServe(":8080", router) }
위의 예에서 net/http에서 오류 처리 및 인증 기능은 두 개의 미들웨어 errorHandler 및 authMiddleware를 통해 구현됩니다. 예제 코드의 49행을 보면 코드는 데코레이터 패턴을 사용하여 오류 처리 및 인증 작업 기능을 원래 핸들러에 추가합니다. 이 코드 구현의 장점은 데코레이터 패턴을 통해 여러 처리 함수를 결합하여 핸들러 체인을 형성함으로써 각 처리 함수 핸들러에 이 로직 부분을 추가하지 않고 오류 처리 및 인증 기능을 달성하여 코드의 가독성과 유지 관리성을 향상시키는 것입니다.
그러나 여기에는 중요한 단점도 있습니다. 즉, 이 함수는 프레임워크에서 직접 제공하지 않고 개발자가 직접 구현합니다. 새 처리 함수 핸들러를 추가할 때마다 데코레이터를 추가하고 오류 처리 및 인증 작업을 추가해야 하므로 개발자에게 부담을 줄 뿐만 아니라 오류가 발생하기 쉽습니다. 또한 요구 사항이 계속 변경됨에 따라 일부 요청은 오류 처리만 필요하고 일부 요청은 인증 작업만 필요하며 일부 요청은 오류 처리와 인증 작업이 모두 필요할 수 있습니다. 이 코드 구조를 기반으로 유지 관리 난이도는 점점 더 커질 것입니다.
이와는 대조적으로 Gin 프레임워크는 미들웨어 로직을 활성화하고 비활성화하는 보다 유연한 방법을 제공합니다. 각 라우팅 규칙을 별도로 설정할 필요 없이 특정 라우팅 그룹에 대해 설정할 수 있습니다. 다음은 예제 코드입니다.
package main import ( "github.com/gin-gonic/gin" ) func authMiddleware() gin.HandlerFunc { return func(c *gin.Context) { // 인증 시뮬레이션 if c.GetHeader("Authorization") != "secret" { c.AbortWithStatusJSON(401, gin.H{"error": "인증되지 않음"}) return } c.Next() } } func main() { router := gin.Default() // Logger 및 Recovery 미들웨어를 전역적으로 추가 // 라우팅 그룹을 만들고 이 그룹의 모든 경로는 authMiddleware 미들웨어를 적용합니다. authenticated := router.Group("/") authenticated.Use(authMiddleware()) { authenticated.GET("/hello", func(c *gin.Context) { c.String(200, "안녕하세요, 세계!") }) authenticated.GET("/private", func(c *gin.Context) { c.String(200, "개인 데이터") }) } // 라우팅 그룹에 없으므로 authMiddleware 미들웨어가 적용되지 않습니다. router.GET("/welcome", func(c *gin.Context) { c.String(200, "환영합니다!") }) router.Run(":8080") }
위의 예에서는 router.Group("/")을 통해 authenticated라는 라우팅 그룹을 만들고 Use 메서드를 사용하여 이 라우팅 그룹에 대해 authMiddleware 미들웨어를 활성화합니다. 이 라우팅 그룹의 모든 라우팅 규칙은 authMiddleware에서 구현한 인증 작업을 자동으로 실행합니다.
net/http와 비교할 때 Gin의 장점은 다음과 같습니다. 첫째, 미들웨어 로직을 추가하기 위해 각 핸들러를 데코레이트할 필요가 없습니다. 개발자는 비즈니스 로직 개발에만 집중하면 되므로 개발 부담이 줄어듭니다. 둘째, 유지 관리성이 더 높습니다. 비즈니스에서 더 이상 인증 작업이 필요하지 않은 경우 Gin에서는 Use 메서드에 대한 호출만 삭제하면 됩니다. 반면 net/http에서는 모든 핸들러의 데코레이션 작업을 처리하고 데코레이터 노드에서 인증 작업 노드를 삭제해야 합니다. 이는 많은 작업량과 오류가 발생하기 쉽습니다. 마지막으로 요청의 서로 다른 부분에서 서로 다른 미들웨어를 사용해야 하는 시나리오에서 Gin은 더 유연하고 구현하기 쉽습니다. 예를 들어 일부 요청은 인증 작업이 필요하고 일부 요청은 오류 처리가 필요하며 일부 요청은 오류 처리와 인증 작업이 모두 필요합니다. 이 시나리오에서는 Gin을 통해 세 개의 라우팅 그룹만 만들면 되고 서로 다른 라우팅 그룹에서 Use 메서드를 각각 호출하여 서로 다른 미들웨어를 활성화하면 요구 사항을 충족할 수 있습니다. 이는 net/http에 비해 더 유연하고 유지 관리하기 쉽습니다. 이것이 바로 net/http가 이미 있음에도 불구하고 Gin 프레임워크가 등장하는 중요한 이유 중 하나입니다.
IV. 데이터 바인딩
HTTP 요청을 처리할 때 일반적인 기능은 요청의 데이터를 구조체에 자동으로 바인딩하는 것입니다. 폼 데이터를 예로 들어 다음은 net/http를 사용하는 경우 데이터를 구조체에 바인딩하는 방법입니다.
package main import ( "fmt" "log" "net/http" ) type User struct { Name string `json:"name"` Email string `json:"email"` } func handleFormSubmit(w http.ResponseWriter, r *http.Request) { var user User // 폼 데이터를 User 구조체에 바인딩 user.Name = r.FormValue("name") user.Email = r.FormValue("email") // 사용자 데이터 처리 fmt.Fprintf(w, "사용자가 생성되었습니다: %s (%s)", user.Name, user.Email) } func main() { http.HandleFunc("/createUser", handleFormSubmit) http.ListenAndServe(":8080", nil) }
이 과정에서 FormValue 메서드를 호출하여 폼에서 데이터를 하나씩 읽은 다음 구조체에 설정해야 합니다. 필드가 많은 경우 일부 필드를 놓치기 쉽고 후속 처리 로직에 문제가 발생할 수 있습니다. 또한 각 필드를 수동으로 읽고 설정해야 하므로 개발 효율성이 심각하게 저하됩니다.
다음으로 Gin이 폼 데이터를 읽고 구조체에 설정하는 방법을 살펴보겠습니다.
package main import ( "fmt" "github.com/gin-gonic/gin" ) type User struct { Name string `json:"name"` Email string `json:"email"` } func handleFormSubmit(c *gin.Context) { var user User // 폼 데이터를 User 구조체에 바인딩 err := c.ShouldBind(&user) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "잘못된 폼"}) return } // 사용자 데이터 처리 c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("사용자가 생성되었습니다: %s (%s)", user.Name, user.Email)}) } func main() { router := gin.Default() router.POST("/createUser", handleFormSubmit) router.Run(":8080") }
위의 예제 코드의 17행을 살펴보면 ShouldBind 함수를 직접 호출하여 각 필드를 하나씩 읽고 구조체에 별도로 설정할 필요 없이 폼 데이터를 구조체에 자동으로 매핑할 수 있습니다. net/http를 사용하는 것과 비교할 때 Gin 프레임워크는 데이터 바인딩 측면에서 더 편리하고 오류가 발생하기 쉽습니다. Gin은 다양한 유형의 데이터를 구조체에 매핑할 수 있는 다양한 API를 제공하며 사용자는 해당 API만 호출하면 됩니다. 그러나 net/http는 이러한 작업을 제공하지 않으며 사용자는 데이터를 직접 읽고 구조체에 수동으로 설정해야 합니다.
V. 결론
Go 언어에서 net/http는 기본적인 HTTP 기능을 제공하지만, 그 설계 목표는 고급 기능과 편리한 개발 경험을 제공하기보다는 단순성과 일반성에 초점을 맞춥니다. HTTP 요청을 처리하고 웹 애플리케이션을 구축할 때 net/http는 복잡한 라우팅 규칙에 직면했을 때 불충분합니다. 로깅 및 오류 처리와 같은 일부 일반적인 작업의 경우 플러그 가능한 설계를 달성하기 어렵습니다. 요청 데이터를 구조체에 바인딩하는 측면에서 net/http는 편리한 작업을 제공하지 않으며 사용자는 수동으로 구현해야 합니다.
이것이 Gin과 같은 타사 라이브러리가 나타나는 이유입니다. Gin은 net/http를 기반으로 구축되었으며 웹 애플리케이션 개발을 단순화하고 가속화하는 것을 목표로 합니다. 전반적으로 Gin은 개발자가 웹 애플리케이션을 더 효율적으로 구축할 수 있도록 지원하여 더 나은 개발 경험과 더 풍부한 기능을 제공합니다. 물론 net/http 또는 Gin을 사용할지 여부는 프로젝트의 규모, 요구 사항 및 개인 선호도에 따라 다릅니다. 간단한 소규모 프로젝트의 경우 net/http만으로 충분할 수 있지만 복잡한 애플리케이션의 경우 Gin이 더 적합할 수 있습니다.
Leapcell: Golang 앱 호스팅을 위한 차세대 서버리스 플랫폼
마지막으로 Go 서비스를 배포하는 데 가장 적합한 플랫폼인 **Leapcell**을 추천합니다.
1. 다국어 지원
- JavaScript, Python, Go 또는 Rust로 개발하십시오.
2. 무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 지불하십시오. 요청도 없고 요금도 없습니다.
3. 타의 추종을 불허하는 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
4. 간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
5. 손쉬운 확장성 및 고성능
- 높은 동시성을 쉽게 처리하기 위한 자동 확장.
- 운영 오버헤드가 없습니다. 빌드에만 집중하십시오.
Leapcell Twitter: https://x.com/LeapcellHQ