Go 완벽 마스터

Go의 핵심 개념과 실전 활용법

Go중급
12시간
7개 항목
학습 진행률0 / 7 (0%)

학습 항목

1. Go
GORM|데이터베이스|ORM|Go|완벽가이드
퀴즈튜토리얼
2. Go
Gin|REST|API|개발|완벽|가이드
퀴즈튜토리얼
3. Go
초급
Golang|성능|최적화|가이드
퀴즈튜토리얼
4. Go
중급
Golang|핵심|개념|완벽|정리
퀴즈튜토리얼
5. Go
Go|채널|고루틴|동시성|프로그래밍
퀴즈튜토리얼
6. TypeScript
고급
Golang|최신|기능|완벽|가이드
퀴즈튜토리얼
7. TypeScript
고급
Go|디자인|패턴|완벽|가이드
퀴즈튜토리얼
1 / 7

이미지 로딩 중...

GORM 데이터베이스 ORM Go 완벽가이드 - 슬라이드 1/11

GORM 데이터베이스 ORM 완벽 가이드

Go 언어의 가장 인기 있는 ORM 라이브러리 GORM을 완벽하게 마스터하세요. 데이터베이스 모델 정의부터 복잡한 쿼리, 관계 설정, 마이그레이션까지 실무에서 바로 사용할 수 있는 모든 것을 다룹니다.


목차

  1. GORM 시작하기 - 첫 모델과 연결 설정
  2. 데이터 생성하기 - Create와 CreateInBatches
  3. 데이터 조회하기 - Find, First, Where 활용
  4. 데이터 수정하기 - Update, Updates, Save
  5. 데이터 삭제하기 - Delete와 소프트 삭제
  6. 모델 관계 정의하기 - Has One, Has Many, Belongs To
  7. 트랜잭션 처리하기 - Begin, Commit, Rollback
  8. 마이그레이션 자동화하기 - AutoMigrate 활용
  9. 훅 활용하기 - BeforeCreate, AfterUpdate 등
  10. 스코프 활용하기 - 재사용 가능한 쿼리 로직

1. GORM 시작하기 - 첫 모델과 연결 설정

시작하며

여러분이 Go로 백엔드 API를 개발할 때 데이터베이스에 데이터를 저장하려면 어떻게 하시나요? SQL 쿼리를 직접 문자열로 작성하고, 결과를 일일이 구조체에 매핑하는 작업은 번거롭고 실수하기 쉽습니다.

더 큰 문제는 데이터베이스가 PostgreSQL에서 MySQL로 바뀌면 쿼리 문법도 함께 수정해야 한다는 점입니다. 테이블 스키마가 변경될 때마다 모든 SQL 문을 찾아서 고치는 것도 정말 고통스러운 일이죠.

바로 이럴 때 필요한 것이 ORM(Object-Relational Mapping)입니다. GORM은 Go 언어에서 가장 널리 사용되는 ORM 라이브러리로, 데이터베이스 작업을 Go 코드로 직관적으로 처리할 수 있게 해줍니다.

개요

간단히 말해서, GORM은 Go 구조체와 데이터베이스 테이블을 자동으로 연결해주는 도구입니다. 실무에서는 복잡한 SQL 쿼리를 작성하는 대신 Go의 메서드 체이닝으로 쿼리를 작성할 수 있습니다.

사용자 정보를 저장하고, 조회하고, 수정하는 작업이 몇 줄의 직관적인 코드로 해결됩니다. 또한 여러 데이터베이스(MySQL, PostgreSQL, SQLite 등)를 같은 코드로 지원할 수 있어 데이터베이스 전환이 필요할 때도 최소한의 수정만으로 가능합니다.

기존에는 database/sql 패키지로 직접 SQL을 작성하고 Scan()으로 데이터를 매핑했다면, GORM을 사용하면 구조체만 정의하고 Create(), Find() 같은 메서드만 호출하면 됩니다. GORM의 핵심 특징은 자동 마이그레이션, 관계 설정(Has One, Has Many, Many to Many), 트랜잭션 처리, 그리고 풍부한 쿼리 빌더입니다.

이러한 기능들이 개발 생산성을 크게 높이고 버그를 줄여줍니다.

코드 예제

package main

import (
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

// User 모델 정의
type User struct {
    ID    uint   `gorm:"primaryKey"`
    Name  string `gorm:"size:100;not null"`
    Email string `gorm:"uniqueIndex;not null"`
    Age   int    `gorm:"default:0"`
}

func main() {
    // 데이터베이스 연결 (PostgreSQL 예제)
    dsn := "host=localhost user=myuser password=mypass dbname=mydb port=5432"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("데이터베이스 연결 실패")
    }

    // 자동 마이그레이션 - 테이블 자동 생성
    db.AutoMigrate(&User{})
}

설명

이것이 하는 일: GORM을 설치하고 데이터베이스에 연결한 후, Go 구조체를 기반으로 테이블을 자동 생성합니다. 첫 번째로, User 구조체를 정의하면서 gorm 태그를 사용합니다.

gorm:"primaryKey"는 ID를 기본 키로 설정하고, gorm:"size:100;not null"은 Name 필드가 VARCHAR(100)이며 NULL을 허용하지 않음을 의미합니다. gorm:"uniqueIndex"는 Email에 유니크 인덱스를 생성하여 중복을 방지합니다.

이렇게 태그를 사용하면 SQL CREATE TABLE 문을 직접 작성하지 않아도 됩니다. 두 번째로, gorm.Open()으로 데이터베이스 연결을 설정합니다.

첫 번째 인자는 드라이버(여기서는 PostgreSQL)이고, DSN(Data Source Name)에는 호스트, 사용자명, 비밀번호, 데이터베이스명 등이 포함됩니다. MySQL을 사용한다면 gorm.io/driver/mysql을 import하고 드라이버만 바꾸면 됩니다.

세 번째로, db.AutoMigrate(&User{})를 호출하면 GORM이 User 구조체를 분석해서 users 테이블을 자동으로 생성합니다. 이미 테이블이 존재한다면 스키마 변경사항만 적용하고, 없으면 새로 만듭니다.

데이터는 절대 삭제하지 않으므로 안전합니다. 마지막으로, 이 코드를 실행하면 데이터베이스에 id, name, email, age 컬럼을 가진 users 테이블이 생성됩니다.

여러분은 이제 SQL을 한 줄도 작성하지 않고 테이블을 만들었습니다. 여러분이 이 코드를 사용하면 데이터베이스 스키마 변경이 필요할 때 구조체만 수정하고 다시 실행하면 자동으로 마이그레이션됩니다.

또한 여러 테이블을 관리할 때 각각의 구조체만 정의하면 되므로 코드가 깔끔하고 유지보수가 쉬워집니다.

실전 팁

💡 DSN을 하드코딩하지 말고 환경변수나 설정 파일에서 읽어오세요. 실수로 커밋하면 보안 문제가 발생할 수 있습니다.

💡 구조체 필드명은 CamelCase로 작성하면 GORM이 자동으로 snake_case 컬럼명으로 변환합니다(Name → name).

💡 db.AutoMigrate()는 개발 환경에서는 편리하지만, 프로덕션에서는 마이그레이션 도구(golang-migrate 등)를 사용하는 것이 안전합니다.

💡 연결 풀 설정을 위해 sqlDB, _ := db.DB()로 *sql.DB를 얻은 후 SetMaxOpenConns(), SetMaxIdleConns()를 설정하세요.

💡 에러 처리를 반드시 하세요. GORM은 에러를 반환하므로 if err != nil 체크를 빼먹지 마세요.


2. 데이터 생성하기 - Create와 CreateInBatches

시작하며

여러분이 사용자 가입 API를 만들 때 입력받은 정보를 데이터베이스에 저장해야 합니다. SQL로 한다면 INSERT 문을 작성하고, 파라미터 바인딩을 하고, 에러를 체크하는 복잡한 과정을 거쳐야 하죠.

더 복잡한 상황은 여러 사용자를 한 번에 등록할 때입니다. 반복문으로 하나씩 INSERT하면 성능이 매우 떨어지고, 수동으로 벌크 INSERT를 작성하면 코드가 지저분해집니다.

바로 이럴 때 필요한 것이 GORM의 Create 메서드입니다. 구조체에 데이터를 채우고 Create()를 호출하면 알아서 INSERT 쿼리가 실행되고, 여러 개를 한 번에 저장할 때는 CreateInBatches()를 사용하면 됩니다.

개요

간단히 말해서, Create()는 구조체 인스턴스를 데이터베이스에 저장하는 메서드입니다. 실무에서는 HTTP 요청에서 받은 JSON 데이터를 구조체에 언마샬링한 후 바로 Create()로 저장할 수 있습니다.

예를 들어, 신규 사용자 등록, 게시글 작성, 주문 생성 같은 모든 INSERT 작업이 이 방식으로 처리됩니다. GORM은 자동으로 생성 시간을 기록하고, 기본 키를 할당해주는 편리한 기능도 제공합니다.

기존에는 db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", name, email)처럼 작성했다면, GORM에서는 구조체를 만들고 db.Create(&user)만 호출하면 됩니다. GORM의 핵심 특징은 자동 ID 할당(AUTO_INCREMENT), 배치 삽입으로 성능 최적화, 그리고 생성된 레코드의 ID를 구조체에 자동으로 채워주는 점입니다.

이러한 특징들이 코드를 간결하게 만들고 실수를 방지합니다.

코드 예제

// 단일 레코드 생성
user := User{
    Name:  "홍길동",
    Email: "hong@example.com",
    Age:   25,
}

// INSERT 쿼리 자동 실행
result := db.Create(&user)

// 에러 체크
if result.Error != nil {
    // 에러 처리 (예: 중복 이메일)
    panic(result.Error)
}

// 생성된 ID 확인 (자동으로 할당됨)
fmt.Printf("생성된 사용자 ID: %d\n", user.ID)

// 여러 레코드를 배치로 생성 (성능 최적화)
users := []User{
    {Name: "김철수", Email: "kim@example.com", Age: 30},
    {Name: "이영희", Email: "lee@example.com", Age: 28},
}

// 100개씩 배치로 INSERT
db.CreateInBatches(users, 100)

설명

이것이 하는 일: 구조체에 데이터를 설정하고 Create() 메서드로 데이터베이스에 저장하며, 결과를 확인합니다. 첫 번째로, User 구조체의 인스턴스를 만들고 필드에 값을 할당합니다.

ID는 설정하지 않았는데, 이는 데이터베이스가 자동으로 생성해주기 때문입니다(AUTO_INCREMENT). Name, Email, Age만 설정하면 됩니다.

두 번째로, db.Create(&user)를 호출하면 GORM이 내부적으로 INSERT INTO users (name, email, age) VALUES ('홍길동', 'hong@example.com', 25) 같은 쿼리를 생성하고 실행합니다. 중요한 점은 &user처럼 포인터를 전달해야 한다는 것입니다.

그래야 GORM이 생성된 ID를 user.ID에 채워줄 수 있습니다. 세 번째로, result.Error를 체크해서 에러를 처리합니다.

예를 들어 Email이 중복되면 uniqueIndex 제약 조건 위반 에러가 발생합니다. result.RowsAffected로 영향받은 행 수도 확인할 수 있습니다(보통 1이어야 함).

네 번째로, CreateInBatches()는 슬라이스를 받아서 여러 레코드를 한 번에 삽입합니다. 두 번째 인자는 배치 크기로, 100이면 100개씩 묶어서 INSERT 쿼리를 실행합니다.

1000개 레코드를 삽입한다면 10번의 쿼리로 처리되어 성능이 크게 향상됩니다. 여러분이 이 코드를 사용하면 CSV 파일이나 외부 API에서 받은 대량의 데이터를 빠르게 데이터베이스에 저장할 수 있습니다.

또한 생성된 ID를 즉시 사용할 수 있어 연관 데이터를 생성하는 작업도 편리합니다.

실전 팁

💡 Create() 후에는 반드시 result.Error를 체크하세요. 제약 조건 위반이나 네트워크 문제가 발생할 수 있습니다.

💡 대량 삽입 시 CreateInBatches()의 배치 크기를 너무 크게 하면 메모리 문제가, 너무 작게 하면 성능 저하가 발생합니다. 100-1000 사이가 적당합니다.

💡 CreatedAt, UpdatedAt 필드를 gorm.Model에 포함시키면 생성/수정 시간이 자동으로 기록됩니다.

💡 트랜잭션 내에서 Create()를 사용하면 여러 테이블에 데이터를 원자적으로 저장할 수 있습니다.

💡 Email 같은 unique 필드가 중복되면 에러가 발생하므로, 사전에 존재 여부를 체크하거나 에러를 적절히 처리하세요.


3. 데이터 조회하기 - Find, First, Where 활용

시작하며

여러분이 사용자 목록을 가져오거나 특정 조건에 맞는 데이터를 찾을 때 SQL SELECT 문을 직접 작성하면 복잡한 WHERE 절과 파라미터 바인딩을 처리해야 합니다. 더 까다로운 것은 결과를 구조체 슬라이스에 매핑하는 작업입니다.

rows.Next()로 반복하면서 Scan()으로 각 컬럼을 일일이 변수에 할당하는 보일러플레이트 코드가 길어집니다. 바로 이럴 때 필요한 것이 GORM의 Find, First, Where 메서드입니다.

메서드 체이닝으로 조건을 연결하고, 결과를 구조체에 자동으로 매핑받을 수 있습니다.

개요

간단히 말해서, Find()는 여러 레코드를 조회하고, First()는 첫 번째 레코드를 가져오며, Where()는 조건을 추가하는 메서드입니다. 실무에서는 사용자 목록 조회, 특정 이메일을 가진 사용자 찾기, 나이가 특정 범위인 사용자 필터링 등 모든 SELECT 작업에 사용됩니다.

예를 들어, 관리자 대시보드에서 최근 가입한 사용자 10명을 보여주거나, 검색 기능으로 이름에 특정 키워드가 포함된 사용자를 찾는 작업이 이 방식으로 처리됩니다. 기존에는 rows, _ := db.Query("SELECT * FROM users WHERE age > ?", 20) 후 반복문으로 스캔했다면, GORM에서는 db.Where("age > ?", 20).Find(&users)만 작성하면 됩니다.

GORM의 핵심 특징은 메서드 체이닝으로 복잡한 쿼리를 가독성 있게 작성하고, 자동 매핑으로 코드 양을 줄이며, 다양한 조건 연산자(=, >, LIKE, IN 등)를 지원한다는 점입니다. 이러한 특징들이 쿼리 작성을 직관적으로 만들고 버그를 줄입니다.

코드 예제

// 모든 사용자 조회
var users []User
db.Find(&users)

// ID로 특정 사용자 조회
var user User
db.First(&user, 1) // WHERE id = 1

// 조건으로 조회 (나이가 20 이상인 사용자)
db.Where("age >= ?", 20).Find(&users)

// 여러 조건 조합 (AND)
db.Where("name = ?", "홍길동").Where("age > ?", 20).First(&user)

// 이메일로 조회 (구조체 사용)
db.Where(&User{Email: "hong@example.com"}).First(&user)

// IN 조건 사용
db.Where("name IN ?", []string{"홍길동", "김철수"}).Find(&users)

// LIKE 검색
db.Where("name LIKE ?", "%길동%").Find(&users)

// 정렬과 제한
db.Where("age > ?", 20).Order("age desc").Limit(10).Find(&users)

설명

이것이 하는 일: 다양한 조건과 옵션을 사용하여 데이터베이스에서 레코드를 조회하고 구조체에 자동으로 매핑합니다. 첫 번째로, db.Find(&users)는 users 테이블의 모든 레코드를 조회해서 users 슬라이스에 채웁니다.

&users처럼 포인터를 전달해야 GORM이 결과를 채울 수 있습니다. 레코드가 없으면 빈 슬라이스가 되며 에러는 발생하지 않습니다.

두 번째로, db.First(&user, 1)은 ID가 1인 레코드를 찾아서 user 구조체에 채웁니다. First()는 결과가 없으면 gorm.ErrRecordNotFound 에러를 반환하므로 반드시 체크해야 합니다.

Last()를 사용하면 마지막 레코드를 가져옵니다. 세 번째로, Where()는 SQL의 WHERE 절을 추가합니다.

문자열과 파라미터를 분리해서 전달하면 SQL 인젝션을 자동으로 방지합니다. 여러 개의 Where()를 체이닝하면 AND로 연결됩니다.

Or() 메서드로 OR 조건도 만들 수 있습니다. 네 번째로, Order()는 정렬 기준을, Limit()은 최대 레코드 수를, Offset()은 건너뛸 레코드 수를 지정합니다.

예를 들어 페이지네이션을 구현할 때 Offset(page * pageSize).Limit(pageSize)처럼 사용합니다. 여러분이 이 코드를 사용하면 복잡한 검색 기능을 쉽게 구현할 수 있습니다.

여러 필터를 동적으로 조합하거나, 정렬 옵션을 사용자가 선택하게 하거나, 페이지네이션으로 대량의 데이터를 효율적으로 보여줄 수 있습니다.

실전 팁

💡 First()나 Last()를 사용할 때는 errors.Is(result.Error, gorm.ErrRecordNotFound)로 레코드 미존재를 체크하세요.

💡 Where() 조건을 구조체로 전달하면 0 값 필드는 무시됩니다. 모든 필드를 조건에 포함하려면 map을 사용하세요.

💡 복잡한 쿼리는 db.Raw()로 원시 SQL을 작성할 수도 있지만, 가능하면 GORM 메서드를 사용하는 것이 안전합니다.

💡 Select()로 특정 컬럼만 조회하면 성능이 향상됩니다. 예: db.Select("name", "email").Find(&users)

💡 Preload()로 관계 데이터를 함께 조회하면 N+1 쿼리 문제를 방지할 수 있습니다(나중에 자세히 다룹니다).


4. 데이터 수정하기 - Update, Updates, Save

시작하며

여러분이 사용자 프로필 수정 기능을 만들 때 특정 필드만 업데이트해야 하는 경우가 많습니다. 이름만 바꾸거나, 나이만 수정하는 식이죠.

SQL UPDATE 문을 직접 작성하면 SET 절에 수정할 컬럼을 나열하고, WHERE로 대상을 지정하고, 파라미터를 바인딩하는 과정이 번거롭습니다. 실수로 WHERE 절을 빼먹으면 모든 레코드가 수정되는 재앙이 발생할 수도 있습니다.

바로 이럴 때 필요한 것이 GORM의 Update, Updates, Save 메서드입니다. 특정 레코드를 조회한 후 필드를 수정하고 저장하거나, 조건에 맞는 레코드만 선택적으로 업데이트할 수 있습니다.

개요

간단히 말해서, Update()는 단일 필드를, Updates()는 여러 필드를, Save()는 전체 레코드를 저장하는 메서드입니다. 실무에서는 사용자 정보 수정, 게시글 조회수 증가, 주문 상태 변경 등 모든 UPDATE 작업에 사용됩니다.

예를 들어, 사용자가 프로필에서 이메일 주소를 변경하면 해당 필드만 업데이트하고, 관리자가 게시글을 승인하면 status 필드만 바꾸는 작업이 이 방식으로 처리됩니다. 기존에는 `db.Exec("UPDATE users SET email = ?

WHERE id = ?", newEmail, userID)처럼 작성했다면, GORM에서는 레코드를 조회한 후 user.Email = newEmail; db.Save(&user)또는db.Model(&user).Update("email", newEmail)`만 하면 됩니다. GORM의 핵심 특징은 필드별 선택적 업데이트, 구조체나 맵으로 여러 필드 한 번에 수정, 그리고 UpdatedAt 자동 갱신입니다.

이러한 특징들이 코드를 안전하고 간결하게 만듭니다.

코드 예제

// 단일 필드 업데이트
db.Model(&User{}).Where("id = ?", 1).Update("age", 30)

// 여러 필드 업데이트 (구조체 사용)
db.Model(&User{}).Where("id = ?", 1).Updates(User{
    Name: "김철수",
    Age:  35,
})

// 여러 필드 업데이트 (맵 사용 - 0 값도 업데이트됨)
db.Model(&User{}).Where("id = ?", 1).Updates(map[string]interface{}{
    "age":   0,  // 구조체로는 0이 무시되지만 맵은 업데이트됨
    "name": "이영희",
})

// 레코드 조회 후 수정 (Save 사용)
var user User
db.First(&user, 1)
user.Email = "new@example.com"
db.Save(&user)  // 모든 필드가 업데이트됨

// 조건부 업데이트
db.Model(&User{}).Where("age < ?", 20).Update("age", gorm.Expr("age + 1"))

설명

이것이 하는 일: 다양한 방식으로 데이터베이스 레코드의 필드를 수정하고, 조건에 맞는 레코드만 선택적으로 업데이트합니다. 첫 번째로, Update("age", 30)은 age 필드만 30으로 수정합니다.

Model(&User{})로 어느 테이블을 대상으로 할지 지정하고, Where()로 조건을 추가합니다. WHERE 절 없이 사용하면 에러가 발생하므로 안전합니다(전체 업데이트를 방지).

두 번째로, Updates()는 구조체나 맵을 받아서 여러 필드를 한 번에 업데이트합니다. 구조체를 사용하면 0 값(숫자는 0, 문자열은 "")인 필드는 무시됩니다.

반면 맵을 사용하면 모든 키의 값이 업데이트되므로, 명시적으로 0으로 설정하고 싶을 때 유용합니다. 세 번째로, Save()는 조회한 레코드의 모든 필드를 업데이트합니다.

First()로 레코드를 가져온 후 필드를 수정하고 Save()를 호출하면, 기본 키(ID)를 기준으로 모든 컬럼이 UPDATE됩니다. 이 방식은 직관적이지만 불필요한 필드까지 업데이트되므로 성능에 주의해야 합니다.

네 번째로, gorm.Expr()을 사용하면 SQL 표현식을 직접 사용할 수 있습니다. 예를 들어 조회수를 1 증가시키거나, 계산 결과를 저장할 때 유용합니다.

Update("views", gorm.Expr("views + 1"))처럼 쓸 수 있습니다. 여러분이 이 코드를 사용하면 동시성 문제를 줄이고(필요한 필드만 수정), 성능을 최적화하며(불필요한 업데이트 방지), UpdatedAt이 자동으로 갱신되어 변경 이력을 추적할 수 있습니다.

실전 팁

💡 Updates()에 구조체를 사용할 때 0 값을 업데이트하려면 Select()로 명시하거나 맵을 사용하세요.

💡 WHERE 조건 없이 Update()를 호출하면 ErrMissingWhereClause 에러가 발생합니다. 전체 업데이트가 필요하면 db.Session(&gorm.Session{AllowGlobalUpdate: true})를 사용하세요.

💡 UpdatedAt 필드가 gorm.Model에 포함되어 있으면 자동으로 현재 시간으로 갱신됩니다.

💡 트랜잭션 내에서 업데이트하면 여러 테이블을 원자적으로 수정할 수 있습니다.

💡 RowsAffected로 실제 수정된 레코드 수를 확인하세요. 0이면 조건에 맞는 레코드가 없었다는 의미입니다.


5. 데이터 삭제하기 - Delete와 소프트 삭제

시작하며

여러분이 사용자 탈퇴 기능을 구현할 때 데이터를 완전히 삭제해야 할까요, 아니면 삭제 표시만 하고 보관해야 할까요? 법적 요구사항이나 데이터 복구 가능성을 고려하면 완전 삭제는 위험할 수 있습니다.

SQL DELETE 문을 직접 작성하면 실수로 WHERE 절을 빼먹어 모든 데이터가 삭제되는 사고가 발생할 수 있습니다. 또한 삭제된 데이터를 복구하거나 이력을 추적하기 어렵습니다.

바로 이럴 때 필요한 것이 GORM의 Delete 메서드와 소프트 삭제(Soft Delete) 기능입니다. 실제로는 삭제 시간만 기록하고 데이터는 남겨두어, 나중에 복구하거나 감사 로그로 활용할 수 있습니다.

개요

간단히 말해서, Delete()는 레코드를 삭제하고, gorm.DeletedAt 필드가 있으면 소프트 삭제(삭제 표시만)가 자동으로 적용됩니다. 실무에서는 사용자 탈퇴, 게시글 삭제, 주문 취소 등의 작업에 사용됩니다.

예를 들어, 사용자가 계정을 삭제하면 실제로는 deleted_at 컬럼에 현재 시간을 기록하고, 일반 조회에서는 제외되지만 관리자는 복구할 수 있습니다. 법적으로 데이터 보관이 필요한 경우 매우 유용합니다.

기존에는 db.Exec("DELETE FROM users WHERE id = ?", userID)로 영구 삭제했다면, GORM에서는 db.Delete(&user) 또는 db.Delete(&User{}, userID)만 하면 소프트 삭제가 적용됩니다. GORM의 핵심 특징은 소프트 삭제 자동 적용, 삭제된 레코드는 조회에서 자동 제외, 그리고 Unscoped()로 실제 삭제나 삭제된 레코드 조회가 가능하다는 점입니다.

이러한 특징들이 데이터 안전성을 높이고 복구 가능성을 제공합니다.

코드 예제

import "gorm.io/gorm"

// 소프트 삭제를 지원하는 모델
type User struct {
    ID        uint
    Name      string
    Email     string
    DeletedAt gorm.DeletedAt `gorm:"index"`  // 소프트 삭제 필드
}

// 레코드 삭제 (소프트 삭제)
db.Delete(&User{}, 1)  // WHERE id = 1, UPDATE deleted_at

// 조건으로 삭제
db.Where("age < ?", 18).Delete(&User{})

// 조회 시 삭제된 레코드는 자동 제외
var users []User
db.Find(&users)  // WHERE deleted_at IS NULL

// 삭제된 레코드 포함 조회
db.Unscoped().Find(&users)

// 영구 삭제 (물리적 삭제)
db.Unscoped().Delete(&User{}, 1)  // 실제로 DELETE 쿼리 실행

설명

이것이 하는 일: 레코드를 소프트 삭제(논리적 삭제)하여 데이터는 보존하면서 일반 조회에서 제외하고, 필요 시 복구하거나 영구 삭제할 수 있게 합니다. 첫 번째로, User 구조체에 gorm.DeletedAt 타입의 DeletedAt 필드를 추가합니다.

이 필드는 sql.NullTime을 래핑한 타입으로, NULL이면 삭제되지 않은 상태이고 시간 값이 있으면 삭제된 상태입니다. gorm:"index"로 인덱스를 추가하면 삭제 여부 필터링 성능이 향상됩니다.

두 번째로, db.Delete(&User{}, 1)을 호출하면 실제로는 UPDATE users SET deleted_at = NOW() WHERE id = 1이 실행됩니다. DELETE 쿼리가 아니라 UPDATE 쿼리입니다.

데이터는 그대로 남아있고 deleted_at 컬럼만 채워집니다. 세 번째로, db.Find(&users)처럼 일반 조회를 하면 GORM이 자동으로 WHERE deleted_at IS NULL 조건을 추가합니다.

사용자 입장에서는 삭제된 레코드가 보이지 않아 마치 완전히 삭제된 것처럼 동작합니다. 네 번째로, db.Unscoped()를 사용하면 소프트 삭제 필터를 무시합니다.

관리자 페이지에서 삭제된 사용자 목록을 보거나, 복구 기능을 구현할 때 유용합니다. db.Unscoped().Delete()는 실제 DELETE 쿼리를 실행해서 레코드를 영구 삭제합니다.

여러분이 이 코드를 사용하면 실수로 데이터를 영구 삭제하는 사고를 방지하고, GDPR 같은 규정에 따라 일정 기간 데이터를 보관한 후 정리할 수 있으며, 사용자가 실수로 삭제한 데이터를 복구하는 기능도 쉽게 만들 수 있습니다.

실전 팁

💡 DeletedAt에 인덱스를 추가하지 않으면 조회 성능이 크게 저하될 수 있습니다. 반드시 gorm:"index"를 설정하세요.

💡 소프트 삭제된 레코드를 복구하려면 db.Model(&user).Unscoped().Update("deleted_at", nil)로 deleted_at을 NULL로 되돌리세요.

💡 정기적으로 배치 작업으로 오래된 소프트 삭제 레코드를 영구 삭제하여 데이터베이스 크기를 관리하세요.

💡 외래 키 관계에서 부모가 소프트 삭제되면 자식 조회 시 문제가 생길 수 있으므로, 관계 설정에 주의하세요.

💡 Unscoped() 사용 시 의도치 않게 삭제된 레코드까지 수정/삭제할 수 있으므로 신중하게 사용하세요.


6. 모델 관계 정의하기 - Has One, Has Many, Belongs To

시작하며

여러분이 블로그 시스템을 만들 때 사용자(User)와 게시글(Post)의 관계를 어떻게 표현하시나요? 한 사용자는 여러 게시글을 작성하고, 각 게시글은 한 명의 작성자를 가집니다.

SQL로는 외래 키를 직접 설정하고, JOIN 쿼리를 작성하고, 결과를 수동으로 매핑해야 합니다. 사용자를 조회할 때 그의 게시글 목록을 함께 가져오려면 복잡한 쿼리와 매핑 로직이 필요합니다.

바로 이럴 때 필요한 것이 GORM의 관계 정의입니다. Has One(1:1), Has Many(1:N), Belongs To(N:1), Many To Many(N:M) 관계를 구조체 태그로 선언하면, GORM이 자동으로 외래 키를 생성하고 관계 데이터를 로딩합니다.

개요

간단히 말해서, 관계는 구조체 필드에 다른 구조체나 슬라이스를 포함시켜 정의하며, GORM이 외래 키와 JOIN을 자동으로 처리합니다. 실무에서는 사용자-게시글, 주문-상품, 부서-직원 같은 모든 관계형 데이터 모델링에 사용됩니다.

예를 들어, 사용자 프로필을 조회하면서 그의 모든 게시글을 함께 보여주거나, 주문 상세를 보면서 주문 항목들을 함께 로딩하는 작업이 이 방식으로 처리됩니다. 기존에는 SELECT * FROM users u JOIN posts p ON u.id = p.user_id처럼 JOIN 쿼리를 작성하고 결과를 중첩 구조로 매핑했다면, GORM에서는 구조체에 관계를 선언하고 db.Preload("Posts").Find(&users)만 하면 됩니다.

GORM의 핵심 특징은 외래 키 자동 생성, Preload로 관계 데이터 자동 로딩, 그리고 중첩 구조로 직관적인 데이터 접근입니다. 이러한 특징들이 복잡한 관계형 데이터를 쉽게 다룰 수 있게 합니다.

코드 예제

// 사용자 모델 (1)
type User struct {
    ID    uint
    Name  string
    Posts []Post  // Has Many: 한 사용자는 여러 게시글 소유
}

// 게시글 모델 (N)
type Post struct {
    ID      uint
    Title   string
    Content string
    UserID  uint  // 외래 키: users.id를 참조
    User    User  // Belongs To: 각 게시글은 한 사용자에 속함
}

// 관계 데이터 함께 조회
var user User
db.Preload("Posts").First(&user, 1)
// user.Posts에 해당 사용자의 모든 게시글이 로딩됨

// 역방향 조회
var post Post
db.Preload("User").First(&post, 1)
// post.User에 작성자 정보가 로딩됨

// 조건부 Preload
db.Preload("Posts", "created_at > ?", time.Now().AddDate(0, -1, 0)).Find(&user)

설명

이것이 하는 일: 모델 간의 관계를 구조체 필드로 선언하여 외래 키를 자동 생성하고, Preload로 관계 데이터를 효율적으로 로딩합니다. 첫 번째로, User 구조체에 Posts []Post 필드를 추가하면 "한 사용자는 여러 게시글을 가진다"는 Has Many 관계가 정의됩니다.

GORM은 Post 테이블에 user_id 컬럼(외래 키)이 있을 것으로 예상합니다. 두 번째로, Post 구조체에 UserID uint와 User User 필드를 추가합니다.

UserID는 실제 외래 키 값을 저장하는 컬럼이고, User는 관계 데이터를 담을 필드입니다. 이것이 "각 게시글은 한 사용자에 속한다"는 Belongs To 관계입니다.

세 번째로, db.Preload("Posts")를 사용하면 User를 조회할 때 관련된 Posts도 자동으로 조회됩니다. 내부적으로는 SELECT * FROM users WHERE id = 1SELECT * FROM posts WHERE user_id = 1이 실행되어 N+1 문제 없이 효율적으로 로딩됩니다.

네 번째로, Preload에 조건을 추가하면 관계 데이터를 필터링할 수 있습니다. 최근 한 달 게시글만 로딩하거나, 특정 상태의 주문만 가져오는 식으로 활용합니다.

중첩 Preload("Posts.Comments")로 다단계 관계도 로딩 가능합니다. 여러분이 이 코드를 사용하면 복잡한 JOIN 쿼리 없이 관계 데이터에 접근하고, N+1 쿼리 문제를 자동으로 방지하며, 코드가 데이터 구조를 명확하게 표현하여 가독성이 높아집니다.

실전 팁

💡 외래 키 컬럼명을 커스터마이징하려면 gorm:"foreignKey:AuthorID" 태그를 사용하세요.

💡 Preload를 빼먹으면 관계 필드가 빈 값으로 남으므로, 필요한 관계는 반드시 Preload하세요.

💡 Joins()를 사용하면 LEFT JOIN으로 한 번의 쿼리로 데이터를 가져올 수 있지만, 1:N 관계에서는 중복 행이 발생합니다.

💡 순환 참조(User → Posts → User)를 조심하세요. JSON 직렬화 시 무한 루프가 발생할 수 있습니다.

💡 Association()으로 관계 데이터를 추가/삭제할 수 있습니다. 예: db.Model(&user).Association("Posts").Append(&newPost)


7. 트랜잭션 처리하기 - Begin, Commit, Rollback

시작하며

여러분이 계좌 이체 기능을 만들 때 A 계좌에서 돈을 빼고 B 계좌에 돈을 넣는 두 작업이 모두 성공하거나 모두 실패해야 합니다. 중간에 에러가 발생하면 데이터 불일치가 생깁니다.

SQL로는 BEGIN TRANSACTION, COMMIT, ROLLBACK을 수동으로 관리해야 하고, 에러 처리 로직이 복잡해집니다. 패닉이 발생하면 트랜잭션이 제대로 롤백되지 않을 수도 있습니다.

바로 이럴 때 필요한 것이 GORM의 트랜잭션 기능입니다. db.Transaction()으로 함수를 감싸면 에러가 발생하면 자동 롤백되고, 성공하면 자동 커밋됩니다.

패닉도 안전하게 처리됩니다.

개요

간단히 말해서, db.Transaction()은 함수를 받아서 트랜잭션 내에서 실행하고, 에러 반환 시 롤백, 성공 시 커밋을 자동으로 처리합니다. 실무에서는 금융 거래, 재고 관리, 다중 테이블 업데이트 등 데이터 일관성이 중요한 모든 작업에 사용됩니다.

예를 들어, 주문 생성 시 주문 테이블에 레코드를 추가하고, 재고 테이블에서 수량을 감소시키고, 포인트 테이블을 업데이트하는 작업이 원자적으로 처리되어야 합니다. 기존에는 `tx, _ := db.Begin(); defer tx.Rollback(); ...

tx.Commit()처럼 수동으로 관리했다면, GORM에서는 db.Transaction(func(tx *gorm.DB) error { ... })`만 작성하면 됩니다.

GORM의 핵심 특징은 자동 커밋/롤백, 패닉 시 안전한 롤백, 그리고 중첩 트랜잭션(SavePoint) 지원입니다. 이러한 특징들이 데이터 일관성을 보장하고 코드를 간결하게 만듭니다.

코드 예제

// 트랜잭션 자동 관리 (권장)
err := db.Transaction(func(tx *gorm.DB) error {
    // A 사용자 잔액 감소
    if err := tx.Model(&User{}).Where("id = ?", 1).Update("balance", gorm.Expr("balance - ?", 100)).Error; err != nil {
        return err  // 에러 반환 시 자동 롤백
    }

    // B 사용자 잔액 증가
    if err := tx.Model(&User{}).Where("id = ?", 2).Update("balance", gorm.Expr("balance + ?", 100)).Error; err != nil {
        return err
    }

    // 거래 기록 생성
    if err := tx.Create(&Transaction{FromID: 1, ToID: 2, Amount: 100}).Error; err != nil {
        return err
    }

    return nil  // nil 반환 시 자동 커밋
})

if err != nil {
    // 트랜잭션 실패 처리
    log.Printf("거래 실패: %v", err)
}

설명

이것이 하는 일: 여러 데이터베이스 작업을 하나의 원자적 단위로 묶어서 모두 성공하거나 모두 실패하도록 보장합니다. 첫 번째로, db.Transaction()에 익명 함수를 전달합니다.

이 함수는 *gorm.DB 타입의 tx를 인자로 받는데, 이것이 트랜잭션 컨텍스트입니다. 함수 내부의 모든 데이터베이스 작업은 db가 아닌 tx를 사용해야 트랜잭션에 포함됩니다.

두 번째로, 각 작업 후 에러를 체크하고 발생하면 즉시 return합니다. 함수가 nil이 아닌 값을 반환하면 GORM이 자동으로 ROLLBACK을 실행해서 모든 변경사항을 되돌립니다.

A 계좌에서 돈만 빠지고 B 계좌에 추가되지 않는 불일치가 발생하지 않습니다. 세 번째로, 모든 작업이 성공하면 return nil을 합니다.

GORM은 이를 감지하고 자동으로 COMMIT을 실행해서 변경사항을 영구적으로 저장합니다. 수동으로 tx.Commit()을 호출할 필요가 없습니다.

네 번째로, 함수 내에서 패닉이 발생해도 GORM은 defer로 롤백을 보장합니다. 예상치 못한 에러로 프로그램이 중단되어도 데이터베이스는 일관된 상태를 유지합니다.

여러분이 이 코드를 사용하면 복잡한 비즈니스 로직에서 데이터 일관성을 쉽게 보장하고, 에러 처리가 간결해지며, 동시성 문제(동시에 같은 레코드 수정)도 트랜잭션 격리 수준으로 제어할 수 있습니다.

실전 팁

💡 트랜잭션 내의 모든 쿼리는 db가 아닌 tx를 사용해야 합니다. db를 쓰면 트랜잭션 밖에서 실행됩니다.

💡 긴 트랜잭션은 락 경합을 유발하므로, 최소한의 작업만 포함하고 빠르게 완료하세요.

💡 수동 트랜잭션이 필요하면 tx := db.Begin(), tx.Commit(), tx.Rollback()을 사용하되 defer로 롤백을 보장하세요.

💡 SavePoint로 중첩 트랜잭션을 만들 수 있습니다: tx.SavePoint("sp1"), tx.RollbackTo("sp1")

💡 읽기 전용 트랜잭션은 db.Begin(&sql.TxOptions{ReadOnly: true})로 생성하여 성능을 최적화하세요.


8. 마이그레이션 자동화하기 - AutoMigrate 활용

시작하며

여러분이 새로운 기능을 추가할 때마다 데이터베이스 스키마도 변경해야 합니다. 새 컬럼을 추가하거나, 인덱스를 생성하거나, 데이터 타입을 바꾸는 작업이 필요하죠.

수동으로 ALTER TABLE 문을 작성하면 개발, 스테이징, 프로덕션 환경마다 실행해야 하고, 실수로 빠뜨리면 에러가 발생합니다. 팀원들과 스키마 변경을 공유하는 것도 번거롭습니다.

바로 이럴 때 필요한 것이 GORM의 AutoMigrate입니다. 구조체를 수정하고 AutoMigrate()를 호출하면 데이터베이스 스키마가 자동으로 동기화되며, 기존 데이터는 보존됩니다.

개요

간단히 말해서, AutoMigrate()는 구조체를 분석해서 테이블을 생성하거나 누락된 컬럼과 인덱스를 추가하는 메서드입니다. 실무에서는 개발 초기 단계나 빠른 프로토타이핑, 로컬 개발 환경에서 주로 사용됩니다.

예를 들어, User 구조체에 PhoneNumber 필드를 추가하면 AutoMigrate가 phone_number 컬럼을 자동으로 추가해줍니다. 프로덕션에서는 더 정교한 마이그레이션 도구를 사용하는 것이 일반적입니다.

기존에는 ALTER TABLE users ADD COLUMN phone_number VARCHAR(20)을 수동으로 실행했다면, GORM에서는 구조체에 필드만 추가하고 db.AutoMigrate(&User{})를 호출하면 됩니다. GORM의 핵심 특징은 누락된 컬럼과 인덱스 자동 추가, 기존 데이터 보존, 그리고 여러 모델 동시 마이그레이션입니다.

단, 컬럼 삭제나 타입 변경은 자동으로 하지 않아 데이터 손실을 방지합니다.

코드 예제

// 기본 사용
type User struct {
    ID          uint
    Name        string
    Email       string `gorm:"uniqueIndex"`
    PhoneNumber string `gorm:"size:20"`  // 새로 추가된 필드
    Age         int
    CreatedAt   time.Time
}

// 테이블 생성 또는 스키마 동기화
db.AutoMigrate(&User{})

// 여러 모델 한 번에 마이그레이션
db.AutoMigrate(&User{}, &Post{}, &Comment{})

// 마이그레이터로 세밀한 제어
migrator := db.Migrator()

// 테이블 존재 확인
if migrator.HasTable(&User{}) {
    // 컬럼 존재 확인
    if !migrator.HasColumn(&User{}, "PhoneNumber") {
        migrator.AddColumn(&User{}, "PhoneNumber")
    }
}

// 인덱스 생성
migrator.CreateIndex(&User{}, "Email")

설명

이것이 하는 일: 구조체 정의를 데이터베이스 스키마와 비교하여 차이를 자동으로 수정하고, 개발 생산성을 높입니다. 첫 번째로, User 구조체에 PhoneNumber 필드를 추가합니다.

기존에는 Name, Email, Age만 있었다고 가정하면, 이 필드는 데이터베이스에 아직 컬럼이 없습니다. 두 번째로, db.AutoMigrate(&User{})를 호출하면 GORM이 users 테이블을 검사합니다.

테이블이 없으면 CREATE TABLE을 실행하고, 있으면 구조체와 비교해서 phone_number 컬럼이 없음을 감지하고 ALTER TABLE로 추가합니다. 세 번째로, gorm 태그도 반영됩니다.

gorm:"uniqueIndex"가 있으면 해당 컬럼에 유니크 인덱스를 생성하고, gorm:"size:20"은 VARCHAR(20)으로 타입을 설정합니다. 기존 데이터는 전혀 영향받지 않습니다.

네 번째로, Migrator()로 더 세밀한 제어가 가능합니다. HasTable()로 테이블 존재를 확인하고, DropTable()로 삭제하고, RenameColumn()으로 컬럼명을 변경하는 등 DDL 작업을 프로그래밍 방식으로 수행할 수 있습니다.

여러분이 이 코드를 사용하면 개발 속도가 빠라지고(수동 SQL 불필요), 팀원 간 스키마 동기화가 쉬워지며(코드가 스키마의 소스), 환경별 스키마 차이로 인한 버그를 줄일 수 있습니다.

실전 팁

💡 AutoMigrate는 컬럼을 삭제하거나 타입을 변경하지 않습니다. 의도적으로 안전하게 설계되었으므로, 이런 작업은 수동으로 해야 합니다.

💡 프로덕션에서는 golang-migrate나 Atlas 같은 전문 마이그레이션 도구를 사용하고, AutoMigrate는 개발/테스트 환경에만 쓰는 것이 좋습니다.

💡 초기화 스크립트에 AutoMigrate를 넣으면 애플리케이션 시작 시 자동으로 스키마가 동기화됩니다.

💡 ColumnTypes()로 현재 스키마 정보를 조회할 수 있어 디버깅에 유용합니다.

💡 복잡한 제약 조건(CHECK, FOREIGN KEY)은 CreateConstraint()로 명시적으로 생성하세요.


9. 훅 활용하기 - BeforeCreate, AfterUpdate 등

시작하며

여러분이 사용자 비밀번호를 저장할 때 평문으로 저장하면 보안에 심각한 문제가 생깁니다. Create() 호출 전에 매번 수동으로 해싱하는 것은 번거롭고 빠뜨리기 쉽습니다.

또한 레코드가 수정될 때마다 변경 로그를 남기거나, 캐시를 무효화하거나, 이벤트를 발행하는 등의 부가 작업이 필요한 경우가 많습니다. 이런 로직을 비즈니스 코드에 섞으면 복잡해집니다.

바로 이럴 때 필요한 것이 GORM의 훅(Hooks)입니다. BeforeCreate, AfterUpdate 같은 메서드를 구조체에 정의하면 자동으로 호출되어, 데이터 저장 전후에 원하는 작업을 수행할 수 있습니다.

개요

간단히 말해서, 훅은 특정 시점(생성 전, 수정 후 등)에 자동으로 실행되는 메서드로, 구조체에 정의하면 GORM이 알아서 호출합니다. 실무에서는 비밀번호 해싱, UUID 생성, 기본값 설정, 감사 로그 기록, 캐시 갱신, 이벤트 발행 등 다양한 용도로 사용됩니다.

예를 들어, 사용자 생성 시 자동으로 UUID를 생성하거나, 게시글 수정 시 검색 인덱스를 업데이트하는 작업이 훅으로 처리됩니다. 기존에는 Create() 전에 수동으로 user.Password = hashPassword(user.Password)를 호출했다면, GORM에서는 BeforeCreate 훅에 해싱 로직을 넣으면 자동으로 실행됩니다.

GORM의 핵심 특징은 생명주기의 각 단계(Create, Update, Delete, Find)에 전/후 훅이 있고, 훅에서 에러를 반환하면 작업이 중단되며, 비즈니스 로직과 부가 기능을 분리할 수 있다는 점입니다. 이러한 특징들이 코드를 깔끔하고 안전하게 만듭니다.

코드 예제

import (
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
)

type User struct {
    ID       uint
    Email    string
    Password string
}

// BeforeCreate 훅: 레코드 생성 전 자동 실행
func (u *User) BeforeCreate(tx *gorm.DB) error {
    // 비밀번호 해싱
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    if err != nil {
        return err  // 에러 반환 시 Create 작업 중단
    }
    u.Password = string(hashedPassword)
    return nil
}

// AfterUpdate 훅: 레코드 수정 후 자동 실행
func (u *User) AfterUpdate(tx *gorm.DB) error {
    // 캐시 무효화 로직
    // cache.Delete("user:" + strconv.Itoa(int(u.ID)))

    // 감사 로그 기록
    tx.Create(&AuditLog{
        Action: "update",
        UserID: u.ID,
    })
    return nil
}

// 사용 예
user := User{Email: "test@example.com", Password: "plain123"}
db.Create(&user)  // BeforeCreate 훅이 자동으로 비밀번호 해싱

설명

이것이 하는 일: 데이터베이스 작업의 생명주기 각 단계에서 자동으로 실행되는 메서드를 정의하여 부가 기능을 깔끔하게 구현합니다. 첫 번째로, BeforeCreate 메서드를 User 구조체에 정의합니다.

메서드 시그니처는 반드시 func (u *User) BeforeCreate(tx *gorm.DB) error 형태여야 하고, 리시버가 포인터여야 필드를 수정할 수 있습니다. 두 번째로, 메서드 내부에서 비밀번호를 해싱합니다.

bcrypt.GenerateFromPassword()로 안전하게 해싱하고, 에러가 발생하면 return하여 Create 작업을 중단합니다. 에러가 없으면 u.Password를 해시 값으로 교체하고 nil을 반환합니다.

세 번째로, db.Create(&user)를 호출하면 GORM이 자동으로 BeforeCreate 훅을 감지하고 실행합니다. 사용자는 훅의 존재를 의식하지 않아도 되고, 어디서 Create()를 호출하든 항상 비밀번호가 해싱됩니다.

네 번째로, AfterUpdate 훅은 UPDATE 쿼리가 성공적으로 실행된 후 호출됩니다. 여기서 캐시를 무효화하거나, 변경 이력을 기록하거나, 다른 시스템에 이벤트를 발행하는 등의 후처리 작업을 수행합니다.

tx를 사용하면 같은 트랜잭션 내에서 작업됩니다. 여러분이 이 코드를 사용하면 보안 관련 로직을 빼먹을 위험이 없고, 비즈니스 로직이 간결해지며, 공통 기능을 재사용하기 쉬워집니다.

훅은 한 곳에 정의되므로 유지보수도 편리합니다.

실전 팁

💡 사용 가능한 훅: BeforeSave, BeforeCreate, AfterCreate, BeforeUpdate, AfterUpdate, BeforeDelete, AfterDelete, AfterFind

💡 훅에서 tx를 사용하면 같은 트랜잭션 내에서 추가 작업을 수행할 수 있습니다.

💡 훅이 에러를 반환하면 메인 작업(Create, Update 등)이 롤백되므로 데이터 일관성이 보장됩니다.

💡 SkipHooks 옵션으로 훅을 건너뛸 수 있습니다: db.Session(&gorm.Session{SkipHooks: true}).Create(&user)

💡 훅에서 무거운 작업(외부 API 호출)은 성능에 영향을 주므로, 비동기 처리를 고려하세요.


10. 스코프 활용하기 - 재사용 가능한 쿼리 로직

시작하며

여러분이 여러 곳에서 "활성 사용자만 조회" 또는 "최근 30일 데이터만 가져오기" 같은 조건을 반복적으로 사용한다면 코드 중복이 발생합니다. 매번 Where("status = ?", "active")를 작성하면 오타가 생기거나, 조건이 바뀔 때 모든 곳을 수정해야 합니다.

복잡한 필터 조합은 코드를 읽기 어렵게 만듭니다. 바로 이럴 때 필요한 것이 GORM의 스코프(Scopes)입니다.

자주 사용하는 쿼리 조건을 함수로 정의하고, Scopes() 메서드로 재사용하여 코드를 간결하고 일관성 있게 만들 수 있습니다.

개요

간단히 말해서, 스코프는 *gorm.DB를 받아서 조건을 추가한 *gorm.DB를 반환하는 함수로, Scopes() 메서드로 적용합니다. 실무에서는 공통 필터(활성/비활성, 기간별, 권한별), 페이지네이션, 정렬 기준 등을 스코프로 정의하여 재사용합니다.

예를 들어, 관리자용 API와 일반 사용자용 API에서 각각 다른 필터를 적용하거나, 검색 기능에서 여러 조건을 조합할 때 스코프를 활용합니다. 기존에는 db.Where("status = ?", "active").Where("created_at > ?", time.Now().AddDate(0, 0, -30))을 여러 곳에 복사했다면, 스코프로 db.Scopes(ActiveOnly, RecentOnly).Find(&users)처럼 간결하게 작성할 수 있습니다.

GORM의 핵심 특징은 함수형 프로그래밍 스타일의 조합 가능성, 쿼리 로직의 재사용성, 그리고 가독성 향상입니다. 이러한 특징들이 복잡한 쿼리를 관리하기 쉽게 만듭니다.

코드 예제

import "time"

// 활성 사용자만 필터링하는 스코프
func ActiveOnly(db *gorm.DB) *gorm.DB {
    return db.Where("status = ?", "active")
}

// 최근 30일 데이터만 가져오는 스코프
func RecentOnly(db *gorm.DB) *gorm.DB {
    return db.Where("created_at > ?", time.Now().AddDate(0, 0, -30))
}

// 페이지네이션 스코프 (재사용 가능)
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        offset := (page - 1) * pageSize
        return db.Offset(offset).Limit(pageSize)
    }
}

// 스코프 사용 예
var users []User

// 활성 사용자 중 최근 가입한 사용자 조회
db.Scopes(ActiveOnly, RecentOnly).Find(&users)

// 페이지네이션 적용 (2페이지, 페이지당 20개)
db.Scopes(ActiveOnly, Paginate(2, 20)).Find(&users)

// 스코프 조합
db.Scopes(ActiveOnly, RecentOnly, Paginate(1, 10)).Order("created_at desc").Find(&users)

설명

이것이 하는 일: 자주 사용하는 쿼리 조건을 함수로 추상화하여 여러 곳에서 일관성 있게 재사용하고, 복잡한 쿼리를 읽기 쉽게 만듭니다. 첫 번째로, ActiveOnly 함수는 *gorm.DB를 받아서 Where 조건을 추가한 *gorm.DB를 반환합니다.

이 패턴이 스코프의 기본 형태입니다. 함수 내부에서 메서드 체이닝으로 조건을 추가하고 db를 반환하면 됩니다.

두 번째로, Paginate는 파라미터를 받는 스코프입니다. 클로저(closure)를 사용하여 page, pageSize를 캡처하고, 실제 스코프 함수를 반환합니다.

이렇게 하면 동적인 값을 스코프에 전달할 수 있습니다. 세 번째로, db.Scopes(ActiveOnly, RecentOnly)처럼 여러 스코프를 한 번에 적용합니다.

GORM은 각 스코프를 순서대로 실행하여 조건을 누적합니다. 코드가 마치 영어 문장처럼 읽혀서 의도가 명확합니다.

네 번째로, 스코프는 다른 쿼리 메서드(Order, Preload 등)와 자유롭게 조합됩니다. 스코프로 공통 로직을 처리하고, 특정 상황에 필요한 조건만 추가로 작성하면 됩니다.

여러분이 이 코드를 사용하면 쿼리 로직을 한 곳에서 관리하여 변경이 쉽고(ActiveOnly 함수만 수정), 팀원들이 일관된 조건을 사용하며(오타나 실수 방지), 복잡한 비즈니스 규칙을 쉽게 표현할 수 있습니다.

실전 팁

💡 스코프 함수는 보통 별도 파일(scopes.go)에 모아서 관리하면 찾기 쉽습니다.

💡 파라미터가 필요한 스코프는 클로저를 반환하는 팩토리 함수로 만드세요.

💡 스코프 내에서 다른 스코프를 호출할 수도 있어 계층적 구조를 만들 수 있습니다.

💡 테스트 시 스코프를 목으로 대체하기 쉬워 단위 테스트가 편리합니다.

💡 복잡한 OR 조건도 스코프로 만들면 가독성이 좋아집니다: db.Where(db.Where("a = ?", 1).Or("b = ?", 2))


#Go#GORM#ORM#Database#CRUD