학교 동아리 페스티벌에 쓰일 웹 벡엔드를 개발하는 기회가 생겨서 관심이 생긴 Go언어로 벡엔드를 개발하고 싶었다.
이 시리즈의 글은 이 프로젝트를 진행하면서 배운 것들을 기록한다.
Gin은 유연한 구조를 지원한다.
그래서 소프트웨어의 확장성과 유연성을 고려하기 위해서는 좋은 구조를 설계해서 프로그래밍해야 한다.
아래는 내가 정한 애플리케이션 구조이다:

/.github/workflows
/config
/internal
- /middlewares
- /models
- /repositories
- /routers
- /services
/utils
go.mod
go.sum
main.go
.github/workflows: Github Actions를 위한 워크플로우 파일 저장 폴더이다.config: 설정 파일들을 담는 폴더이다.internal/: 이 프로젝트 내에서만, 즉 이 패키지만을 위한 계층들을 담고 있다.middlewares: 공용으로 사용될 미들웨어 함수들을 저장한다.models: DB에 매핑하여 사용할 구조체에 대한 정의를 한다.repositories: 직접 DB와 통신하는 함수들을 내장시킨다. CRUD작업을 한다.routers: 각종 클라이언트의 요청들을 다룰 핸들러들을 정의한다.services: 핸들러와 리포지토리 계층 사이에서 비즈니스 로직을 처리하는 계층을 정의한다.utils: 이 프로젝트 내외로 유용하게 사용될 수 있는 것들을 정의한다.도식화하면, 아래와 같다고 볼 수 있겠다.

routers폴더에는 InitRouter.go하나에 모든 라우터를 정의하기로 했다. main함수는 InitRouter.go의 SetupRouter()로 라우터들을 붙일 수 있다.
아래는 로그인에 대해 최소 기능만을 표현한 예제이다. 코드의 상세한 내용보다는 대충 어떤 일을 하는지만 확인하면 이해가 편할 것이다.
main.go
package main
import (
"proj-name/config"
"proj-name/internal/routers"
"log"
"os"
"github.com/gin-gonic/autotls"
)
func main() {
config.LoadEnv()
config.ConnectToDB()
config.InitLogger()
app := routers.SetupRouter()
log.Fatal(autotls.Run(app, os.Getenv("SERVER_DOMAIN")))
}
routers/InitRoueter.go
func SetupRouter() *gin.Engine {
router := gin.Default()
router.Use(middlewares.JSONParsingMiddleware)
auth := router.Group("/auth")
{
auth.POST("/login", middlewares.ValidateBodyMiddleware("user_id", "user_password"), loginHandler)
}
notice := router.Group("/notices")
{
notice.GET("/", getNoticesHandler)
notice.PUT("/:noticeId", middlewares.RequireAuth, middlewares.ValidateBodyMiddleware("title", "content"), modifyNoticeHandler)
notice.POST("/", middlewares.RequireAuth, middlewares.ValidateBodyMiddleware("title", "content"), createNoticeHandler)
notice.DELETE("/:noticeId", middlewares.RequireAuth, deleteNoticeHandler)
}
middlewares/requireAuth.go
package middlewares
import (
"fmt"
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// RequireAuth - verify with JWT
func RequireAuth(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Authorization header is required",
})
c.Abort()
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid Auth Format",
})
c.Abort()
return
}
accessToken := parts[1]
token, err := jwt.Parse(accessToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("JWT_ACCESS_KEY")), nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid token",
})
c.Abort()
return
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
c.Set("claims", claims)
}
c.Next()
}
loginHander.go
package routers
import (
"net/http"
"proj-name/internal/services"
"github.com/gin-gonic/gin"
)
func loginHandler(c *gin.Context) {
body, exists := c.Get("body")
if !exists {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Body not found",
})
return
}
bodyMap := body.(map[string]interface{})
userID := bodyMap["user_id"].(string)
userPassword := bodyMap["user_password"].(string)
accessToken, err := services.LoginService(userID, userPassword)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error:": err.Error(),
})
return
}
// response with token
c.JSON(http.StatusOK, gin.H{
"accessToken": accessToken,
})
}
services/loginService.go
package services
import (
"errors"
"proj-name/internal/repositories"
"proj-name/utils"
)
// LoginService - Log in
func LoginService(userID string, userPassword string) (string, error) {
// find user by user_id
usr, err := repositories.FindUserByUserID(userID)
if err != nil {
return "", errors.New("user not found")
}
// compare password
if !utils.IsPasswordCorrect(usr.UserPassword, userPassword) {
return "", errors.New("incorrect password")
}
// generate Access Token
accessToken, err := utils.GenerateAccessToken(usr.UserUUID)
if err != nil {
return "", err
}
return accessToken, nil
}
repositories/userRepository.go
package repositories
import (
"proj-name/config"
"proj-name/internal/models"
)
// FindUserByUserID - Find User by its ID
func FindUserByUserID(userID string) (*models.User, error) {
var user models.User
res := config.DB.Where("user_id = ?", userID).First(&user)
if res.Error != nil {
return nil, res.Error
}
return &user, nil
}
models/user.go
package models
import (
"github.com/google/uuid"
)
// User Model..
type User struct {
UserUUID uuid.UUID `gorm:"type:char(36);primaryKey"`
UserID string `gorm:"type:varchar(255);unique;not null"`
UserPassword string `gorm:"type:varchar(512);not null"`
}
utils/token.go
package utils
import (
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
// AuthTokenClaims - Claim Define for JWT..
type AuthTokenClaims struct {
UserUUID uuid.UUID `json:"userUUID"`
jwt.RegisteredClaims
}
utils/generateToken.go
package utils
import (
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
// GenerateAccessToken - generate JWT Access Token
func GenerateAccessToken(userUUID uuid.UUID) (string, error) {
claims := AuthTokenClaims{
userUUID,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString([]byte(os.Getenv("JWT_ACCESS_KEY")))
if err != nil {
return "", err
}
return ss, nil
}