Golang의 웹 서버, Gin 프레임워크 시작하기

Golang의 웹 서버, Gin 프레임워크 시작하기

김태홍 (bluemiv)
작성일: 2025-04-08 14:13:25
AD

1. Gin Framework 소개

Gin은 Golang으로 개발된 매우 빠르고 경량화된 웹 프레임워크입니다. Node에서는 Express.js 널리 사용되듯이, Gin은 Go에서 가장 많이 쓰이는 웹 프레임워크 중 하나입니다.

성능이 뛰어나면서도 라우팅, 미들웨어, JSON Serialization, HTML 렌더링, 요청 바인딩, 유효성 검사 등 웹 개발에 필요한 다양한 기능을 제공합니다. 또한, Gin은 Go의 표준 라이브러리인 net/http를 기반으로 개발되어, Go의 장점을 최대한 활용할 수 있도록 설계되어 있습니다.


2. Gin Framwork 시작하기

우선 Go가 설치되어 있어야 합니다. Go 설치 방법은 Mac에 Go 설치하기 문서를 참고하세요.

프로젝트를 생성합니다.

> mkdir myapp
> cd myapp

이후, Go 모듈을 초기화합니다.

Go 모듈은 Go 1.11부터 도입된 의존성 관리 시스템으로, Go 프로젝트의 패키지와 버전을 관리할 때 사용됩니다.

> go mod init myapp

아래 명령어를 사용하여, Gin 프레임워크를 설치합니다.

> go get -u github.com/gin-gonic/gin

필요한 의존성을 설치가 완료되면 main.go 파일을 생성합니다.

package main
 
import (
	"net/http"
 
	"github.com/gin-gonic/gin"
)
 
func main() {
	router := gin.Default()
 
	router.GET("/health", func(context *gin.Context) {
		context.JSON(http.StatusOK, gin.H{
			"results": "200 Okay",
		})
	})
 
	router.Run(":4000") // 4000 포트에서 실행
}

위 코드와 같이 /health 경로에 GET 요청을 처리하는 핸들러를 생성합니다. 그리고, 서버를 4000 포트에서 실행합니다.

> go run main.go
첫 Gin 서버 API
첫 Gin 서버 API

express.js와 비슷한 구조로 되어 gin.Default()를 사용하여 Gin 서버를 생성한 뒤, GET() 메서드를 사용하여 GET 요청을 처리하는 핸들러를 등록할 수 있습니다. 손쉽게 API 서버를 만들 수 있습니다.

Gin 빠르게 시작하기 위한 내용은 공식 문서 "Quickstart" 를 참고해도 좋습니다


3. Gin 프레임워크의 특징

3.1. 기본 라우팅

Gin은 RESTful API를 지원하기 때문에, 다음과 같이 다양한 HTTP 메서드(GET, POST, PUT, DELETE 등)에 대한 라우팅을 지원합니다.

router.GET("/user/:username", getUserProfile)
router.POST("/user", createUser)
router.PUT("/user/:username", updateUser)
router.DELETE("/user/:username", deleteUser)

위 예제에서 :username과 같이 path parameter도 지정할 수 있으며, 핸들러 함수에서 path parameter 값을 사용할 수 있습니다.

func getUserProfile(c *gin.Context) {
    username := c.Param("username")
    c.JSON(200, gin.H{"data": username})
}
 
func createUser(c *gin.Context) {
    var newUser map[string]string
    if err := c.BindJSON(&newUser); err != nil {
        c.JSON(400, gin.H{"error": "Bad Request"})
        return
    }
    c.JSON(201, gin.H{"data": newUser})
}

c.Param은 URL의 path paramter를 가져올 때 사용하는 함수입니다. c.BindJSON은 Request의 body에 포함된 JSON 데이터를 자동으로 파싱해 변수에 담아줍니다.

Golang의 특성상 예외 처리는 직접 해주어야 합니다. 위 예제에서는 c.BindJSON에서 에러가 발생하면 400 Bad Request를 응답합니다.

3.2. 그룹 라우팅

위 User API(/user/**)는 user라는 유사한 경로를 가지고 있습니다. gin에서는 API를 그룹화하여 라우트를 정의할 수 있습니다.

userGroup := router.Group("/user")
{
  userGroup.GET("/:username", getUserProfile)
  userGroup.POST("/", createUser)
  userGroup.PUT("/:username", updateUser)
  userGroup.DELETE("/:username", deleteUser)
}

중괄호로 그룹화하기 때문에 코드의 가독성이 높아지게 됩니다.

3.3. 쿼리 파라미터와 Form 데이터

Client에서 GET 요청으로 데이터를 전송할때 쿼리 파라미터를 보내고, POST 요청을 할 때는 Form 데이터를 사용합니다.

이때 gin에서는 다음과 같이 처리할 수 있습니다. GET 요청의 쿼리 파라미터의 경우,

# Request
GET /search?keyword=go&page=1
router.GET("/search", func(c *gin.Context) {
    keyword := c.Query("keyword")
    page := c.DefaultQuery("page", "1")
    c.JSON(200, gin.H{
        "search": keyword,
        "page": page,
    })
})

c.Queryc.DefaultQuery를 사용하여 쿼리 파라미터를 가져올 수 있습니다. 둘의 차이는 c.DefaultQuery는 쿼리 파라미터가 없을 경우, 기본값을 설정할 수 있습니다.

POST 요청의 Form 데이터는 다음과 같이 처리할 수 있습니다.

# Request
POST /login
 
{
  "username": "bluemiv",
  "password": "ilovegin!@#"
}
router.POST("/login", func(c *gin.Context) {
    username := c.PostForm("username")
    password := c.PostForm("password")
    c.JSON(200, gin.H{
        "username": username,
    })
})

3.4. JSON Body 파싱

HTTP 메소드 POST, PUT, PATCH 요청을 처리할 때, 클라이언트는 HTTP body에 JSON 형식의 데이터를 담아 전송합니다.

Gin 프레임워크에서는 JSON 형식의 데이터를 파싱하는 작업을 매우 간단하게 처리할 수 있도록, c.BindJSON() 또는 c.ShouldBindJSON() 메서드를 제공합니다.

우선, 다음과 같이 구조체를 정의합니다.

type TodoItem struct {
    Title       string `json:"title" binding:"required"`
    Description string `json:"description" binding:"required,min=10,max=300"`
    Done        bool   `json:"done"`
}

바인딩 태그(binding)를 이용하여 유효성 검사를 할 수 있다.

이후 POST 요청을 처리하는 핸들러를 작성합니다.

router.POST("/todos", func(c *gin.Context) {
    var newTodo TodoItem
 
    if err := c.ShouldBindJSON(&newTodo); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "유효하지 않은 JSON 형식입니다."})
        return
    }
 
    c.JSON(http.StatusCreated, gin.H{
        "todo": newTodo,
    })
})

3.4.1. BindJSON과 ShouldBindJSON의 차이

  • BindJSON(): 내부적으로 오류가 발생하면 panic을 발생시키므로 잘 사용하지 않음
  • ShouldBindJSON(): 오류를 반환 값으로 처리할 수 있어서, 실무에서 더 권장되는 방식

3.5. Middleware

Gin은 미들웨어(Middleware)를 지원합니다. Middleware는 Request와 Response 사이에 실행되는 함수로, 인증 / 로깅 / CORS 처리 등 다양한 용도로 사용할 수 있습니다.

// 모든 요청을 로깅하는 미들웨어
func LoggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        path := c.Request.URL.Path
        method := c.Request.Method
        fmt.Printf("요청: %s %s\n", method, path)
 
        c.Next()
 
        status := c.Writer.Status()
        fmt.Printf("응답 상태 코드: %d\n", status)
    }
}

위와 같이 미들웨어를 정의한 후, router.Use() 메서드를 사용하여 Gin 서버에 등록할 수 있습니다.

router.Use(LoggerMiddleware())

3.6. JSON 바인딩과 유효성 검사

Gin은 JSON 바인딩과 유효성 검사를 지원합니다. 구조체를 사용하여 클라이언트로부터 받은 JSON 요청 데이터를 바인딩하고, 유효성 검사를 동시에 처리할 수 있습니다.

우선 구조체를 정의합니다.

type Food struct { // JSON으로부터 받는 데이터 구조
    Name     string `json:"name" binding:"required"`
    Calories int    `json:"calories" binding:"gte=0"`
}
  • json:"name": JSON의 "name" 키를 Name 필드에 매핑
  • binding:"required": 이 필드는 반드시 있어야 함을 의미
  • binding:"gte=0": Calories 값은 0 이상(greater than or equal to 0) 이어야 함을 의미 → 유효성 검사(Validation)
func createFood(c *gin.Context) {
    var newFood Food
    if err := c.ShouldBindJSON(&newFood); err != nil {
        c.JSON(400, gin.H{"에러": err.Error()})
        return
    }
 
    c.JSON(201, gin.H{"등록된 음식": newFood})
}

c.ShouldBindJSON(&newFood) 는 Request Body의 JSON 데이터를 Food 구조체에 바인딩합니다. (이때, 유효성 검사도 함께 수행) 만약 유효성 검사에 실패하면 400 Bad Request를 응답합니다.

3.7. 정적 파일

만약, 정적 파일(HTML, CSS, JS, 이미지 등)을 제공해야 한다면, Static() 메서드를 사용하여 정적 파일을 제공할 수 있습니다.

router.Static("/static", "./public")

위 설정은 ./public 폴더 안의 파일들을 /static 경로로 제공함을 의미합니다.

3.8. HTML 템플릿 렌더링

JSON 응답 외에도 HTML 파일을 렌더링해서 클라이언트에 전달할 수 있습니다.

router.LoadHTMLGlob("templates/*")
 
router.GET("/profile", func(c *gin.Context) {
    c.HTML(200, "profile.tmpl", gin.H{
        "username": "bluemiv",
        "job":   "developer",
    })
})

"templates/*"는 templates 폴더 안에 있는 모든 파일을 대상으로 합니다. 만약 하위 폴더까지 포함하려면 "templates/**/*"처럼 재귀적으로 지정해줄 수도 있습니다.

참고: 템플릿 파일은 보통 .tmpl 확장자를 사용하지만, .html도 무방합니다. 하지만, 일관성을 위해 .tmpl을 권장합니다.

<!DOCTYPE html>
<html lang="ko">
<head><title>Profile</title></head>
<body>
    <h1>이름: {{ .username }}</h1>
    <p>직업: {{ .job }}</p>
</body>
</html>
  • .(점; dot): 현재 템플릿의 컨텍스트(Context)를 의미합니다. 즉 gin.H로 넘긴 map의 키와 매핑되는 값을 참조할 수 있습니다.
  • {{ .username }}: context(.)에서 "username" 키에 해당하는 "bluemiv"를 출력한다.
  • {{ .job }}: context(.)에서 "job" 키에 해당하는 "developer"를 출력한다.
HTML 템플릿 렌더링
HTML 템플릿 렌더링
AD