[Gin] Gin 프로젝트 구조 설정

riveroverflow·2024년 9월 2일

시작하기에 앞서..

학교 동아리 페스티벌에 쓰일 웹 벡엔드를 개발하는 기회가 생겨서 관심이 생긴 Go언어로 벡엔드를 개발하고 싶었다.

이 시리즈의 글은 이 프로젝트를 진행하면서 배운 것들을 기록한다.

Gin 애플리케이션 구조화하기

Gin은 유연한 구조를 지원한다.
그래서 소프트웨어의 확장성과 유연성을 고려하기 위해서는 좋은 구조를 설계해서 프로그래밍해야 한다.
아래는 내가 정한 애플리케이션 구조이다:

gin_application_structure

/.github/workflows
/config
/internal
 - /middlewares
 - /models
 - /repositories
 - /routers
 - /services
/utils
go.mod
go.sum
main.go
  • .github/workflows: Github Actions를 위한 워크플로우 파일 저장 폴더이다.
  • config: 설정 파일들을 담는 폴더이다.
    환경 변수 불러오기, DB서버와 연결 부분 등을 맡고있다.
  • internal/: 이 프로젝트 내에서만, 즉 이 패키지만을 위한 계층들을 담고 있다.
    Go에서는 internal 폴더 안에 있는 패키지는 불러올 수 없게 해놨다.
    • middlewares: 공용으로 사용될 미들웨어 함수들을 저장한다.
    • models: DB에 매핑하여 사용할 구조체에 대한 정의를 한다.
    • repositories: 직접 DB와 통신하는 함수들을 내장시킨다. CRUD작업을 한다.
    • routers: 각종 클라이언트의 요청들을 다룰 핸들러들을 정의한다.
    • services: 핸들러와 리포지토리 계층 사이에서 비즈니스 로직을 처리하는 계층을 정의한다.
  • utils: 이 프로젝트 내외로 유용하게 사용될 수 있는 것들을 정의한다.
    예를 들면, JWT토큰생성, S3에 파일업로드하는 함수 등을 정의할 수 있다.

도식화하면, 아래와 같다고 볼 수 있겠다.
프로젝트 계층화 시각화

routers폴더에는 InitRouter.go하나에 모든 라우터를 정의하기로 했다. main함수는 InitRouter.goSetupRouter()로 라우터들을 붙일 수 있다.

예제 코드

아래는 로그인에 대해 최소 기능만을 표현한 예제이다. 코드의 상세한 내용보다는 대충 어떤 일을 하는지만 확인하면 이해가 편할 것이다.

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
}

0개의 댓글