GORM — Go ORM 완전 정복
GORM은 Go에서 가장 널리 사용되는 ORM(Object-Relational Mapper) 라이브러리입니다. SQL을 직접 작성하지 않고 Go 구조체와 메서드로 데이터베이스를 조작할 수 있습니다.
GORM 소개와 설치
go get gorm.io/gorm
go get gorm.io/driver/postgres # PostgreSQL
go get gorm.io/driver/mysql # MySQL
go get gorm.io/driver/sqlite # SQLite
DB 연결
package main
import (
"fmt"
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func connectDB() (*gorm.DB, error) {
dsn := "host=localhost user=myuser password=mypass dbname=mydb port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info), // SQL 쿼리 로깅
})
if err != nil {
return nil, fmt.Errorf("DB 연결 실패: %w", err)
}
// 내부 *sql.DB 접근 (커넥션 풀 설정)
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(25)
return db, nil
}
모델 정의
GORM 모델은 Go 구조체로 정의합니다. 태그(gorm:"...")로 컬럼 동작을 제어합니다.
import (
"time"
"gorm.io/gorm"
)
// gorm.Model 임베딩: ID, CreatedAt, UpdatedAt, DeletedAt 자동 포함
type User struct {
gorm.Model
Name string `gorm:"size:100;not null"`
Email string `gorm:"size:150;uniqueIndex;not null"`
Age int `gorm:"check:age > 0"`
Role string `gorm:"default:member"`
// 연관관계
Orders []Order `gorm:"foreignKey:UserID"`
}
type Order struct {
gorm.Model
UserID uint `gorm:"not null;index"`
ProductID uint `gorm:"not null"`
Amount float64 `gorm:"not null"`
Status string `gorm:"default:pending"`
// 역방향 참조
User User `gorm:"foreignKey:UserID"`
Product Product `gorm:"foreignKey:ProductID"`
}
type Product struct {
gorm.Model
Name string `gorm:"size:200;not null"`
Price float64 `gorm:"not null"`
Stock int `gorm:"default:0"`
}
gorm.Model 정의 (내부 구조)
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` // soft delete 지원
}
AutoMigrate — 자동 마이그레이션
func migrate(db *gorm.DB) error {
return db.AutoMigrate(
&User{},
&Product{},
&Order{},
)
}
AutoMigrate는 테이블이 없으면 생성하고, 새 컬럼이 있으면 추가합니다. 기존 컬럼 삭제나 타입 변경은 하지 않으므로 프로덕션에서는 별도 마이그레이션 도구(goose 등)를 사용하세요.
CRUD 기본 작업
Create
func createUser(db *gorm.DB, name, email string, age int) (*User, error) {
user := &User{Name: name, Email: email, Age: age}
// 모든 필드 저장
result := db.Create(user)
if result.Error != nil {
return nil, fmt.Errorf("사용자 생성 실패: %w", result.Error)
}
// user.ID에 자동으로 생성된 ID가 설정됨
fmt.Printf("생성된 사용자 ID: %d\n", user.ID)
return user, nil
}
// 특정 필드만 저장
func createUserWithFields(db *gorm.DB, user *User) error {
return db.Select("Name", "Email").Create(user).Error
}
// 배치 생성
func createUsersBatch(db *gorm.DB, users []User) error {
return db.CreateInBatches(users, 100).Error // 100개씩 배치
}
Read
// 단일 조회 - 기본키 기준
func getUserByID(db *gorm.DB, id uint) (*User, error) {
var user User
result := db.First(&user, id) // WHERE id = id ORDER BY id LIMIT 1
if result.Error == gorm.ErrRecordNotFound {
return nil, nil
}
return &user, result.Error
}
// 조건 조회
func getUserByEmail(db *gorm.DB, email string) (*User, error) {
var user User
result := db.Where("email = ?", email).First(&user)
if result.Error == gorm.ErrRecordNotFound {
return nil, nil
}
return &user, result.Error
}
// 전체 조회 + 정렬 + 페이지네이션
func getUsers(db *gorm.DB, page, limit int) ([]User, int64, error) {
var users []User
var total int64
db.Model(&User{}).Count(&total)
result := db.
Order("created_at DESC").
Offset((page - 1) * limit).
Limit(limit).
Find(&users)
return users, total, result.Error
}
// 조건 구조체로 조회
func findUsers(db *gorm.DB, role string, minAge int) ([]User, error) {
var users []User
result := db.Where("role = ? AND age >= ?", role, minAge).Find(&users)
return users, result.Error
}
Update
// Save: 모든 필드 업데이트 (zero value 포함)
func saveUser(db *gorm.DB, user *User) error {
return db.Save(user).Error
}
// Updates: 변경된 필드만 업데이트
func updateUserName(db *gorm.DB, id uint, name string) error {
return db.Model(&User{}).Where("id = ?", id).
Updates(map[string]interface{}{"name": name}).Error
}
// 구조체로 업데이트 (zero value 필드 제외됨)
func updateUser(db *gorm.DB, id uint, updates User) error {
return db.Model(&User{}).Where("id = ?", id).Updates(updates).Error
}
// Update + 특정 필드만 강제 업데이트
func updateUserAge(db *gorm.DB, id uint, age int) error {
return db.Model(&User{}).Where("id = ?", id).
Select("Age").
Updates(User{Age: age}).Error
}
Delete
// Soft Delete (DeletedAt 필드가 있으면 자동 적용)
func deleteUser(db *gorm.DB, id uint) error {
return db.Delete(&User{}, id).Error
}
// Hard Delete (영구 삭제)
func hardDeleteUser(db *gorm.DB, id uint) error {
return db.Unscoped().Delete(&User{}, id).Error
}
// 조건부 삭제
func deleteOldOrders(db *gorm.DB, days int) error {
return db.Where("created_at < NOW() - INTERVAL '? days'", days).
Delete(&Order{}).Error
}
연관관계(Association)
Preload — 연관 데이터 즉시 로딩
// N+1 문제 방지: Preload로 한 번에 로딩
func getUserWithOrders(db *gorm.DB, id uint) (*User, error) {
var user User
result := db.Preload("Orders").Preload("Orders.Product").First(&user, id)
return &user, result.Error
}
// 조건부 Preload
func getUserWithPendingOrders(db *gorm.DB, id uint) (*User, error) {
var user User
result := db.Preload("Orders", "status = ?", "pending").First(&user, id)
return &user, result.Error
}
// 중첩 Preload (Joins 방식)
func getUsersWithOrders(db *gorm.DB) ([]User, error) {
var users []User
result := db.
Joins("LEFT JOIN orders ON orders.user_id = users.id AND orders.deleted_at IS NULL").
Preload("Orders").
Find(&users)
return users, result.Error
}
Association 조작
// 연관 데이터 추가
func addOrderToUser(db *gorm.DB, userID uint, order *Order) error {
var user User
if err := db.First(&user, userID).Error; err != nil {
return err
}
return db.Model(&user).Association("Orders").Append(order)
}
// 연관 데이터 개수
func countUserOrders(db *gorm.DB, userID uint) int64 {
var user User
db.First(&user, userID)
return db.Model(&user).Association("Orders").Count()
}
트랜잭션
func createOrderWithStock(db *gorm.DB, userID, productID uint, amount float64) error {
return db.Transaction(func(tx *gorm.DB) error {
// 재고 확인
var product Product
if err := tx.First(&product, productID).Error; err != nil {
return err
}
if product.Stock <= 0 {
return fmt.Errorf("재고 부족")
}
// 주문 생성
order := Order{
UserID: userID,
ProductID: productID,
Amount: amount,
Status: "confirmed",
}
if err := tx.Create(&order).Error; err != nil {
return err
}
// 재고 차감
if err := tx.Model(&product).Update("stock", product.Stock-1).Error; err != nil {
return err
}
return nil // nil 반환 시 자동 Commit
// 에러 반환 시 자동 Rollback
})
}
Raw SQL & Named 쿼리
GORM이 생성하는 쿼리가 부적합할 때 Raw SQL을 직접 사용합니다.
// Raw 쿼리로 조회
func getUsersByAgeRange(db *gorm.DB, min, max int) ([]User, error) {
var users []User
result := db.Raw("SELECT * FROM users WHERE age BETWEEN ? AND ?", min, max).Scan(&users)
return users, result.Error
}
// Exec: 결과 반환 없는 Raw SQL
func truncateTable(db *gorm.DB, tableName string) error {
// 주의: 테이블명은 파라미터 바인딩 불가 → 검증 필수
return db.Exec("TRUNCATE TABLE " + tableName).Error
}
// Named 파라미터
func findUserByNamedParam(db *gorm.DB, name, email string) (*User, error) {
var user User
result := db.Where("name = @name OR email = @email",
sql.Named("name", name),
sql.Named("email", email),
).First(&user)
return &user, result.Error
}
Hooks — 생명주기 훅
type User struct {
gorm.Model
Name string
Email string
Password string `gorm:"->"` // 읽기 전용 (저장 시 제외)
Hash string
}
// BeforeCreate: 생성 전 실행
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Hash = string(hash)
u.Password = "" // 원본 비밀번호는 저장하지 않음
}
return nil
}
// AfterFind: 조회 후 실행
func (u *User) AfterFind(tx *gorm.DB) error {
// 필요 시 후처리
return nil
}
핵심 정리
| 메서드 | 설명 |
|---|---|
db.Create(&model) | 레코드 생성 |
db.First(&model, id) | 기본키로 단일 조회 |
db.Find(&models) | 다중 조회 |
db.Save(&model) | 전체 필드 저장/업데이트 |
db.Updates(...) | 변경 필드만 업데이트 |
db.Delete(&model, id) | 삭제 (Soft Delete) |
db.Preload("Field") | 연관 데이터 즉시 로딩 |
db.Transaction(fn) | 트랜잭션 실행 |
GORM은 빠른 개발에 유리하지만, 복잡한 쿼리나 성능이 중요한 구간에서는 Raw SQL 또는 sqlc를 고려하세요.