Gin 기초 #7 프로젝트 구조와 미니 REST API
지난 글까지 라우팅, 검증, 응답, 미들웨어, 데이터베이스를 모두 다뤘습니다. 마지막 글은 지금까지 main.go 한 파일에 모은 코드를 레이어별로 나누고, 환경설정을 분리해 작은 REST API의 골격을 완성합니다. Gin 기초 시리즈의 마무리입니다.
- #1 시작과 첫 서버
- #2 라우팅과 핸들러
- #3 요청 바인딩과 검증
- #4 응답 처리 — JSON, 상태 코드, 에러
- #5 미들웨어
- #6 데이터베이스 연동 (GORM)
- #7 프로젝트 구조와 미니 REST API ← 이번 글
한 파일의 한계 #
지금까지는 모든 코드를 main.go에 모았습니다. 학습 단계에서는 한눈에 보여 편하지만, 핸들러가 늘고 모델이 많아지면 금방 한계가 옵니다. 라우팅과 비즈니스 로직과 DB 접근이 한곳에 섞여, 어디를 고쳐야 할지 찾기 어려워집니다.
해법은 역할별로 파일을 나누는 것입니다. 작은 프로젝트라도 다음 정도의 구분이면 충분합니다.
gin-app/
├── main.go # 진입점, 조립
├── config/
│ └── config.go # 환경설정
├── database/
│ └── database.go # DB 연결
├── models/
│ └── user.go # 데이터 모델
├── handlers/
│ └── user.go # 요청 처리
└── routes/
└── routes.go # 라우트 등록거창한 아키텍처가 아니라, “설정”, “연결”, “모델”, “핸들러”, “라우트"로 관심사를 나눈 정도입니다. 규모가 커지면 핸들러와 DB 접근 사이에 서비스 계층을 더 두기도 하지만, 입문 단계에서는 이 구조로 시작하는 편을 권합니다.
설정 분리 — config #
포트, DB 경로 같은 값은 코드에 박아 두지 않고 환경 변수로 빼는 편이 좋습니다. 개발과 운영에서 다른 값을 쓰기 쉬워집니다.
package config
import "os"
type Config struct {
Port string
DBPath string
}
func Load() Config {
return Config{
Port: getEnv("PORT", "8080"),
DBPath: getEnv("DB_PATH", "app.db"),
}
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}환경 변수가 없으면 기본값을 쓰는 단순한 패턴입니다. .env 파일을 쓰고 싶다면 github.com/joho/godotenv 같은 라이브러리를 더할 수 있지만, 필수는 아닙니다.
DB 연결 분리 — database #
#6의 연결 코드를 별도 패키지로 옮깁니다.
package database
import (
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gin-app/models"
)
func Connect(dbPath string) *gorm.DB {
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
log.Fatal("DB 연결 실패:", err)
}
db.AutoMigrate(&models.User{})
return db
}모델 분리 — models #
package models
import "time"
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name"`
Email string `json:"email" gorm:"uniqueIndex"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}핸들러 분리 — handlers #
핸들러가 *gorm.DB를 필요로 하므로, #6의 클로저 대신 구조체에 의존성을 담는 방식으로 바꿉니다. 핸들러가 많아질수록 이쪽이 깔끔합니다.
package handlers
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gin-app/models"
)
type UserHandler struct {
DB *gorm.DB
}
func (h *UserHandler) Create(c *gin.Context) {
var req struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user := models.User{Name: req.Name, Email: req.Email}
if err := h.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "저장 실패"})
return
}
c.JSON(http.StatusCreated, user)
}
func (h *UserHandler) Get(c *gin.Context) {
var user models.User
if err := h.DB.First(&user, c.Param("id")).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "사용자를 찾을 수 없습니다"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "조회 실패"})
return
}
c.JSON(http.StatusOK, user)
}
func (h *UserHandler) List(c *gin.Context) {
var users []models.User
if err := h.DB.Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "조회 실패"})
return
}
c.JSON(http.StatusOK, users)
}UserHandler가 DB 필드를 갖고, 메서드들이 그것을 공유합니다. 핸들러를 메서드로 묶으니 라우트 등록도 단정해집니다.
라우트 분리 — routes #
package routes
import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gin-app/handlers"
)
func Register(r *gin.Engine, db *gorm.DB) {
userHandler := &handlers.UserHandler{DB: db}
users := r.Group("/users")
{
users.POST("", userHandler.Create)
users.GET("", userHandler.List)
users.GET("/:id", userHandler.Get)
}
}조립 — main.go #
마지막으로 main.go는 각 조각을 불러와 연결하는 역할만 맡습니다. 코드가 짧고, 전체 흐름이 한눈에 보입니다.
package main
import (
"github.com/gin-gonic/gin"
"gin-app/config"
"gin-app/database"
"gin-app/routes"
)
func main() {
cfg := config.Load()
db := database.Connect(cfg.DBPath)
r := gin.Default()
routes.Register(r, db)
r.Run(":" + cfg.Port)
}설정을 읽고, DB에 연결하고, 라우터를 만들어 라우트를 등록한 뒤 서버를 시작합니다. 각 단계가 별도 패키지에 있어서, 핸들러를 추가하거나 DB를 바꿔도 손댈 곳이 분명합니다.
시리즈를 마치며 #
Gin 기초 시리즈에서 다룬 내용을 정리합니다.
- 첫 서버와
net/http대비 Gin의 이점 (#1) - 라우팅, 경로, 쿼리 파라미터, 라우터 그룹 (#2)
- 요청 바인딩과 검증 (#3)
- 응답 형식, 상태 코드, 일관된 에러 (#4)
- 미들웨어와 컨텍스트 값 전달 (#5)
- GORM으로 CRUD (#6)
- 레이어별 프로젝트 구조 (이번 글)
여기까지면 작은 REST API를 처음부터 끝까지 만들 수 있습니다. 다음 단계인 Gin 중급 시리즈에서는 JWT 인증, 미들웨어 기반 중앙 에러 처리, 의존성 주입, 테스트, 페이지네이션, 그리고 Docker 배포까지 다루겠습니다. 실전 서비스에 가까운 형태로 발전시켜 나가겠습니다.