Gin 기초 #6 데이터베이스 연동 (GORM)
지난 글에서 미들웨어를 다뤘습니다. 여기까지 오면 요청을 받고, 검증하고, 응답하고, 공통 처리를 거는 기본기가 갖춰졌습니다. 이번 글은 그 위에 데이터베이스를 붙입니다. GORM으로 실제 데이터를 저장하는 CRUD API를 만듭니다.
- #1 시작과 첫 서버
- #2 라우팅과 핸들러
- #3 요청 바인딩과 검증
- #4 응답 처리 — JSON, 상태 코드, 에러
- #5 미들웨어
- #6 데이터베이스 연동 (GORM) ← 이번 글
- #7 프로젝트 구조와 미니 REST API
GORM이란? #
GORM은 Go에서 가장 널리 쓰이는 ORM입니다. SQL을 직접 쓰는 대신, Go 구조체와 메서드로 데이터베이스를 다루게 해 줍니다. SQL을 직접 다루는 방식은 고 실전 #4 DB 연동에서 다뤘으니, 두 접근을 비교해 보면 차이가 분명합니다.
이번 글은 설치가 간단한 SQLite를 예로 듭니다. PostgreSQL이나 MySQL도 드라이버만 바꾸면 같은 코드가 거의 그대로 동작합니다.
go get gorm.io/gorm
go get gorm.io/driver/sqlite데이터베이스 연결 #
gorm.Open으로 연결을 엽니다. 돌려받은 *gorm.DB가 이후 모든 쿼리의 출발점입니다.
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupDB() *gorm.DB {
db, err := gorm.Open(sqlite.Open("app.db"), &gorm.Config{})
if err != nil {
log.Fatal("DB 연결 실패:", err)
}
return db
}연결 실패는 서버가 정상 동작할 수 없는 상황이므로 log.Fatal로 즉시 종료합니다.
모델 정의 #
GORM은 Go 구조체를 테이블에 대응시킵니다. 구조체 이름의 복수형이 테이블 이름이 됩니다(User → users).
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"`
}gorm:"primaryKey"— 기본 키 지정.ID라는 이름이면 GORM이 자동으로 기본 키로 인식하지만 명시해도 좋음gorm:"uniqueIndex"— 유니크 인덱스, 이메일 중복을 DB 수준에서 차단CreatedAt/UpdatedAt— GORM이 생성, 수정 시각을 자동으로 채워 주는 약속된 필드 이름
json 태그와 gorm 태그를 함께 단 점에 주목하세요. 같은 구조체가 API 응답 직렬화(#4)와 DB 매핑을 겸합니다.
마이그레이션 — AutoMigrate #
AutoMigrate는 모델에 맞춰 테이블을 만들거나 컬럼을 추가합니다. 개발 단계에서 스키마를 빠르게 맞출 때 편합니다.
db.AutoMigrate(&User{})운영 환경에서는 보통 별도의 마이그레이션 도구로 스키마를 관리하지만, 입문 단계에서는 AutoMigrate로 충분합니다.
CRUD 기본 동작 #
GORM의 핵심 메서드들을 먼저 정리합니다.
db.Create(&user) // 생성
db.First(&user, id) // 기본 키로 한 건 조회
db.Find(&users) // 여러 건 조회
db.Save(&user) // 수정 (전체 저장)
db.Model(&user).Updates(...) // 수정 (일부 필드)
db.Delete(&user, id) // 삭제조회 결과가 없을 때 First는 gorm.ErrRecordNotFound 에러를 돌려줍니다. 이 에러를 잡아 404로 응답하는 패턴을 아래에서 씁니다.
Gin 핸들러와 결합하기 #
이제 이 메서드들을 Gin 핸들러에 붙입니다. 핸들러가 *gorm.DB에 접근해야 하므로, 클로저로 db를 가둬 핸들러를 만드는 방식을 씁니다.
func createUser(db *gorm.DB) gin.HandlerFunc {
return func(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 := User{Name: req.Name, Email: req.Email}
if err := db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "저장 실패"})
return
}
c.JSON(http.StatusCreated, user)
}
}GORM 메서드는 *gorm.DB를 돌려주고, 그 안의 .Error 필드로 결과를 확인합니다. 생성에 성공하면 user.ID가 채워지며, #4에서 본 대로 201 Created로 응답합니다.
조회 핸들러입니다.
func getUser(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var user User
if err := db.First(&user, 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)
}
}errors.Is로 “레코드 없음"과 그 외 DB 오류를 구분해, 각각 404와 500으로 응답합니다. 목록 조회는 더 단순합니다.
func listUsers(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var users []User
if err := db.Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "조회 실패"})
return
}
c.JSON(http.StatusOK, users)
}
}수정과 삭제도 같은 틀입니다.
func updateUser(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var user User
if err := db.First(&user, c.Param("id")).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "사용자를 찾을 수 없습니다"})
return
}
var req struct {
Name string `json:"name"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db.Model(&user).Update("name", req.Name)
c.JSON(http.StatusOK, user)
}
}
func deleteUser(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if err := db.Delete(&User{}, c.Param("id")).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "삭제 실패"})
return
}
c.Status(http.StatusNoContent)
}
}라우트에 연결하기 #
클로저로 만든 핸들러를 라우트에 등록합니다. db를 각 핸들러에 넘기는 형태입니다.
func main() {
db := setupDB()
db.AutoMigrate(&User{})
r := gin.Default()
users := r.Group("/users")
{
users.POST("", createUser(db))
users.GET("", listUsers(db))
users.GET("/:id", getUser(db))
users.PUT("/:id", updateUser(db))
users.DELETE("/:id", deleteUser(db))
}
r.Run()
}이제 사용자를 생성하고 조회하는 완전한 CRUD API가 동작합니다.
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"커티스","email":"a@b.com"}'
curl http://localhost:8080/users관계 매핑은 별도로 #
사용자가 여러 게시글을 갖는 1:N 같은 관계 매핑은 GORM의 핵심 주제이지만, 양이 많아 여기서는 다루지 않습니다. 관계 모델링과 연관 데이터 조회는 고 언어 GORM 1:N 관계 글에서 따로 정리했으니 이어서 보면 됩니다.
마무리 #
이번 글에서 정리한 내용입니다.
- GORM은 Go 구조체로 DB를 다루는 ORM, 드라이버만 바꾸면 다른 DB로 이식 가능
gorm.Open으로 연결하고*gorm.DB로 쿼리,AutoMigrate로 개발 단계 스키마 관리- 모델은
gorm태그로 기본 키, 인덱스를 지정,CreatedAt/UpdatedAt은 자동 관리 - CRUD는
Create/First/Find/Updates/Delete, 결과는.Error로 확인 - 핸들러는 클로저로
db를 가둬 만들고,errors.Is로 “없음"을 404로 분기 - 관계 매핑은 별도 GORM 글에서 이어서
다음 글(#7 프로젝트 구조와 미니 REST API)에서는 지금까지 한 파일에 모은 코드를 레이어별로 나누고, 환경설정을 분리해 시리즈를 마무리하겠습니다.