SQLC와 GORM - Go에서 데이터베이스 상호작용을 위한 두 가지 접근 방식
Wenhao Wang
Dev Intern · Leapcell

소개
데이터베이스와의 상호작용은 거의 모든 진지한 애플리케이션의 초석이며, Go는 강력한 타이핑과 성능 특성으로 이러한 시스템을 구축하는 데 인기 있는 선택지가 되었습니다. Go에서 관계형 데이터베이스로 작업할 때 개발자는 종종 두 가지 광범위한 도구 범주 중에서 선택해야 합니다. 하나는 원시 SQL과 코드 생성을 강조하는 도구이고, 다른 하나는 객체 관계형 매핑(ORM) 계층을 제공하는 도구입니다. 이 글에서는 이 범주에 속하는 두 가지 주요 예인 SQLC와 GORM을 자세히 살펴봅니다. 각기 다른 철학, 실제 구현, 적합한 사용 사례를 탐색하여 Go에서 데이터베이스 상호작용이라는 과제에 각 도구가 어떻게 접근하는지에 대한 명확한 이해를 제공할 것입니다.
철학 이해하기: SQLC와 GORM
세부 사항에 들어가기 전에 관련된 핵심 개념에 대한 공통된 이해를 확립해 봅시다.
SQLC (SQL Code Generation): SQLC는 원시 SQL 쿼리에서 Go 코드를 생성하는 도구입니다. 그 철학은 SQL의 선언적 특성이 강력하며 직접 활용되어야 한다는 믿음에 뿌리를 두고 있습니다. SQLC는 SQL 쿼리를 작성함으로써 해당 쿼리를 실행하고 결과를 Go 구조체로 스캔하는 타입 안전한 Go 코드를 자동으로 생성합니다. 이 접근 방식은 명시적인 SQL, 컴파일 타임 안전성, 데이터베이스 작업에 대한 최소한의 추상화를 우선시합니다.
GORM (Go ORM - Object-Relational Mapping): GORM은 Go를 위한 완전한 ORM 라이브러리입니다. 그 철학은 데이터베이스 테이블을 Go 구조체에 매핑하고 원시 SQL 대신 Go 관용구를 사용하여 데이터베이스와 상호작용하기 위한 고수준 API를 제공하는 데 중점을 둡니다. GORM은 기본 SQL을 추상화하여 개발자가 Go 객체로 데이터를 조작할 수 있도록 하는 것을 목표로 합니다. 이 접근 방식은 개발자 편의성, 빠른 개발, 데이터베이스에 대한 객체 지향적인 보기를 우선시합니다.
실제 예를 통해 이러한 차이점을 설명해 보겠습니다.
SQLC: 명시적인 SQL 및 코드 생성 채택
SQLC의 워크플로우는 .sql 파일에 SQL 스키마와 쿼리를 작성하는 것을 포함합니다. 그런 다음 SQLC는 이러한 파일을 처리하여 Go 코드를 생성합니다.
예제: SQLC로 스키마 및 쿼리 정의
먼저 SQL 스키마를 정의합니다(예: schema.sql):
CREATE TABLE authors ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, bio TEXT );
다음으로 SQL 쿼리를 정의합니다(예: query.sql):
-- name: GetAuthor :one SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1; -- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name; -- name: CreateAuthor :one INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id, name, bio; -- name: UpdateAuthor :one UPDATE authors SET name = $1, bio = $2 WHERE id = $3 RETURNING id, name, bio; -- name: DeleteAuthor :exec DELETE FROM authors WHERE id = $1;
sqlc generate를 실행한 후 SQLC는 Go 파일(예: db.go, models.go, query.sql.go)을 생성합니다. 다음은 생성된 query.sql.go의 스니펫입니다.
// Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.25.0 // source: query.sql package db import ( "context" ) const createAuthor = `-- name: CreateAuthor :one INSERT INTO authors (name, bio) VALUES ($1, $2) RETURNING id, name, bio ` type CreateAuthorParams struct { Name string `json:"name"` Bio *string `json:"bio"` } func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) { row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) var i Author err := row.Scan( &i.ID, &i.Name, &i.Bio, ) return i, err } const getAuthor = `-- name: GetAuthor :one SELECT id, name, bio FROM authors WHERE id = $1 LIMIT 1 ` func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) { row := q.db.QueryRowContext(ctx, getAuthor, id) var i Author err := row.Scan( &i.ID, &i.Name, &i.Bio, ) return i, err } // ... other generated functions
그리고 애플리케이션에서 사용하는 방법은 다음과 같습니다.
package main import ( "context" "database/sql" "fmt" _ "github.com/lib/pq" // PostgreSQL driver "your-project/db" // Assuming db package is where sqlc generates code ) func main() { connStr := "user=postgres password=password dbname=sqlc_example sslmode=disable" conn, err := sql.Open("postgres", connStr) if err != nil { panic(err) } defer conn.Close() queries := db.New(conn) ctx := context.Background() // Create an author newAuthor, err := queries.CreateAuthor(ctx, db.CreateAuthorParams{ Name: "Jane Doe", Bio: sql.NullString{String: "A talented writer", Valid: true}, }) if err != nil { panic(err) } fmt.Printf("Created author: %+v\n", newAuthor) // Get an author author, err := queries.GetAuthor(ctx, newAuthor.ID) if err != nil { panic(err) } fmt.Printf("Fetched author: %+v\n", author) // List authors authors, err := queries.ListAuthors(ctx) if err != nil { panic(err) } fmt.Printf("All authors: %+v\n", authors) }
SQLC의 주요 이점은 다음과 같습니다.
- 컴파일 타임 안전성: SQL 오류는 런타임이 아닌 컴파일 시에 감지됩니다.
- 런타임 리플렉션 없음: 생성된 코드는 일반 Go 코드이므로 뛰어난 성능을 제공합니다.
- 직접 SQL 제어: 복잡한 조인, 윈도우 함수 등 SQL의 모든 기능을 사용할 수 있습니다.
- 상용구 감소: 결과를 구조체로 스캔하는 작업이 자동으로 처리됩니다.
그러나 추가 generate 단계가 필요하며 간단한 CRUD 작업에는 더 장황하게 느껴질 수 있습니다.
GORM: Go를 위한 객체 관계형 매핑
GORM은 다른 접근 방식을 사용하여 직접 데이터베이스 테이블에 매핑되는 Go 구조체를 정의할 수 있도록 합니다.
예제: GORM으로 모델 및 작업 정의
먼저 Go 구조체(모델)를 정의합니다.
package main import ( "gorm.io/gorm" ) type Author struct { gorm.Model // Provides ID, CreatedAt, UpdatedAt, DeletedAt Name string `gorm:"not null"` Bio *string }
그런 다음 GORM의 API를 사용하여 데이터베이스와 상호 작용합니다.
package main import ( "fmt" "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" // Import logger for better output "log" "os" "time" ) func main() { dsn := "host=localhost user=postgres password=password dbname=gorm_example port=5432 sslmode=disable TimeZone=Asia/Shanghai" // Configure GORM logger newLogger := logger.New( log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer logger.Config{ SlowThreshold: time.Second, // Slow SQL threshold LogLevel: logger.Info, // Log level IgnoreRecordNotFoundError: false, // Ignore ErrRecordNotFound error for logger Colorful: true, // Disable color }, ) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ Logger: newLogger, // Apply the logger }) if err != nil { panic("failed to connect database") } // Migrate the schema (creates authors table if it doesn't exist) db.AutoMigrate(&Author{}) // Create an author bio := "A talented writer" author := Author{Name: "John Doe", Bio: &bio} res := db.Create(&author) // pass pointer of data to Create if res.Error != nil { panic(res.Error) } fmt.Printf("Created author: %+v\n", author) // Get an author by its primary key var fetchedAuthor Author db.First(&fetchedAuthor, author.ID) // find author with id 1 fmt.Printf("Fetched author: %+v\n", fetchedAuthor) // Update an author db.Model(&fetchedAuthor).Update("Name", "Jonathan Doe") fmt.Printf("Updated author: %+v\n", fetchedAuthor) // Delete an author (soft delete by default with gorm.Model) // db.Delete(&fetchedAuthor, fetchedAuthor.ID) // This would soft delete }
GORM은 다음을 제공합니다.
- 편의성 및 신속성: SQL을 추상화하면 일반적인 작업의 상용구가 줄어듭니다.
- 객체 지향 상호작용: Go 구조체로 직접 작업하므로 Go 개발자에게 더 관용적인 코드가 됩니다.
- 고급 기능: 내장 후크, 연관 관계, 지연 로딩 및 트랜잭션 관리.
- 데이터베이스 추상화: 최소한의 코드 변경으로 다양한 SQL 데이터베이스 간에 쉽게 전환할 수 있습니다.
그러나 GORM은 런타임 리플렉션에 의존하므로 성능 오버헤드가 발생할 수 있습니다(일반 애플리케이션에서는 종종 무시할 수 있지만).
또한 SQL을 추상화하므로 복잡한 쿼리 디버깅이 때때로 더 어려워지고 데이터베이스별 기능에 대한 액세스가 제한될 수 있습니다.
언제 무엇을 선택해야 하는가
SQLC를 선택하는 경우:
- SQL, 성능 및 컴파일 타임 보증에 대한 완전한 제어를 우선시하는 경우.
- 복잡하고 수동으로 최적화된 SQL 쿼리가 포함된 프로젝트인 경우.
- 런타임 리플렉션과 잠재적인 성능 문제를 피하고 싶은 경우.
- "SQL 우선" 개발 접근 방식을 선호하는 경우.
- 기존 데이터베이스 스키마와 직접 통합해야 하는 경우.
GORM을 선택하는 경우:
- 빠른 개발, 편의성 및 높은 수준의 추상화를 우선시하는 경우.
- 대부분의 표준 CRUD 작업이 포함된 프로젝트인 경우.
- 원시 SQL 대신 Go 구조체로 작업하는 것을 선호하는 경우.
- 연관 관계, 후크 및 마이그레이션과 같은 내장 기능이 필요한 경우.
- 다른 SQL 데이터베이스 간에 전환해야 할 수 있는 경우.
결론
SQLC와 GORM은 Go에서 데이터베이스와 상호 작용하는 두 가지 뚜렷하지만 동등하게 유효한 철학을 나타냅니다. SQLC는 코드 생성을 통한 명시적인 SQL과 컴파일 타임 안전성을 옹호하며, 수동으로 조정된 쿼리에 깊이 의존하는 애플리케이션에 대해 비교할 수 없는 제어와 성능을 제공합니다. 반면에 GORM은 객체 관계형 매핑의 편의성과 추상화를 채택하여 관용적인 Go API와 강력한 기능 세트로 개발을 가속화합니다. 궁극적으로 둘 사이의 선택은 프로젝트 요구 사항, 팀 선호도 및 SQL 명시성과 Go 수준 추상화 간의 원하는 균형에 따라 달라집니다. 두 도구 모두 해당 도메인에서 훌륭하며 Go 애플리케이션에서 데이터를 관리할 수 있는 강력한 방법을 제공합니다.

