PostgreSQL용 Go ORMs 비교: GORM vs Ent vs Bun vs sqlc
GO에서 ORM에 대한 실용적이고 코드 중심의 시점
Page content
GO용 ORM(ORMs for GO)의 가장 주요한 것은 GORM, Ent, Bun, sqlc입니다.
이들은 순수 GO에서 CRUD 작업의 예시와 함께 비교해보겠습니다.
TL;DR
- GORM: 기능이 풍부하고 편리하며, “단순히 배포"하기가 가장 쉬우나, 실행 시간에 더 많은 오버헤드가 있습니다.
- Ent: 스키마-코드 생성으로 타입 안전한 API를 제공하며, 대규모 코드베이스 및 리팩토링에 매우 적합합니다.
- Bun: 가볍고 SQL 중심의 쿼리 빌더/ORM이며, Postgres 기능이 우수하고, 설계상 명확합니다.
- sqlc (ORM이 아니지만 여전히): SQL을 작성하고, 타입 안전한 Go를 얻을 수 있으며, 가장 뛰어난 원시 성능과 제어, 실행 시간 마법 없이 최고입니다.
선택 기준 및 빠른 비교
저의 기준은 다음과 같습니다:
- 성능: 지연 시간/처리량, 피할 수 있는 오버헤드, 배치 작업.
- 개발자 경험(DX): 학습 곡선, 타입 안전성, 디버깅 가능성, 코드 생성 마찰.
- 생태계: 문서, 예제, 활동성, 통합(마이그레이션, 추적).
- 기능 세트: 관계, 간접 로딩, 마이그레이션, 훅, 원시 SQL escape hatch.
도구 | 패러다임 | 타입 안전성 | 관계 | 마이그레이션 | 원시 SQL 사용성 | 일반적인 사용 사례 |
---|---|---|---|---|---|---|
GORM | Active Record–style ORM | 중간 (런타임) | 예 (태그, Preload/Joins) | 자동 마이그레이션 (선택적) | db.Raw(...) |
빠른 배포, 풍부한 기능, 전통적인 CRUD 앱 |
Ent | 스키마 → 코드 생성 → 유창한 API | 높음 (컴파일 시간) | 첫 번째 등급 (엣지) | 생성된 SQL (별도 단계) | entsql , 사용자 정의 SQL |
대규모 코드베이스, 리팩토링 중심 팀, 엄격한 타이핑 |
Bun | SQL 중심의 쿼리 빌더/ORM | 중간–높음 | 명시적 (Relation ) |
별도 마이그레이션 패키지 | 자연스러움 (빌더 + 원시) | 성능 중심 서비스, Postgres 기능 |
sqlc | SQL → 코드 생성 함수 (ORM이 아님) | 높음 (컴파일 시간) | SQL 조인을 통해 | 외부 도구 (예: golang-migrate) | 바로 SQL입니다 | 최대 제어 및 속도; DBA 친화적인 팀 |
예제를 통한 CRUD
설정 (PostgreSQL)
pgx 또는 도구의 네이티브 PG 드라이버를 사용하세요. 예시 DSN:
export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'
모든 ORM에 공통된 임포트
go code 예제의 각 파일의 시작 부분에 다음을 추가하세요:
import (
"context"
"os"
)
우리는 간단한 users
테이블을 모델링할 것입니다:
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
GORM
초기화
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type User struct {
ID int64 `gorm:"primaryKey"`
Name string
Email string `gorm:"uniqueIndex"`
}
func newGorm() (*gorm.DB, error) {
dsn := os.Getenv("DATABASE_URL")
return gorm.Open(postgres.Open(dsn), &gorm.Config{})
}
// 자동 마이그레이션 (선택적; 프로덕션에서는 주의)
func migrate(db *gorm.DB) error { return db.AutoMigrate(&User{}) }
CRUD
func gormCRUD(ctx context.Context, db *gorm.DB) error {
// 생성
u := User{Name: "Alice", Email: "alice@example.com"}
if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }
// 읽기
var got User
if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }
// 업데이트
if err := db.WithContext(ctx).Model(&got).
Update("email", "alice+1@example.com").Error; err != nil { return err }
// 삭제
if err := db.WithContext(ctx).Delete(&User{}, got.ID).Error; err != nil { return err }
return nil
}
참고 사항
- 관계는 구조체 태그 +
Preload
/Joins
을 통해 처리합니다. - 트랜잭션 헬퍼:
db.Transaction(func(tx *gorm.DB) error { ... })
.
Ent
스키마 정의 (ent/schema/user.go
에):
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int64("id").Unique().Immutable(),
field.String("name"),
field.String("email").Unique(),
}
}
코드 생성
go run entgo.io/ent/cmd/ent generate ./ent/schema
초기화
import (
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql"
_ "github.com/jackc/pgx/v5/stdlib"
"your/module/ent"
)
func newEnt() (*ent.Client, error) {
dsn := os.Getenv("DATABASE_URL")
drv, err := sql.Open(dialect.Postgres, dsn)
if err != nil { return nil, err }
return ent.NewClient(ent.Driver(drv)), nil
}
CRUD
func entCRUD(ctx context.Context, client *ent.Client) error {
// 생성
u, err := client.User.Create().
SetName("Alice").
SetEmail("alice@example.com").
Save(ctx)
if err != nil { return err }
// 읽기
got, err := client.User.Get(ctx, u.ID)
if err != nil { return err }
// 업데이트
if _, err := client.User.UpdateOneID(got.ID).
SetEmail("alice+1@example.com").
Save(ctx); err != nil { return err }
// 삭제
if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }
return nil
}
참고 사항
- 끝에서 끝까지 강한 타이핑; 엣지로 관계를 처리합니다.
- 생성된 마이그레이션 또는 사용하는 마이그레이션 도구를 선택하세요.
Bun
초기화
import (
"database/sql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
_ "github.com/jackc/pgx/v5/stdlib"
)
type User struct {
bun.BaseModel `bun:"table:users"`
ID int64 `bun:",pk,autoincrement"`
Name string `bun:",notnull"`
Email string `bun:",unique,notnull"`
}
func newBun() (*bun.DB, error) {
dsn := os.Getenv("DATABASE_URL")
sqldb, err := sql.Open("pgx", dsn)
if err != nil { return nil, err }
return bun.NewDB(sqldb, pgdialect.New()), nil
}
CRUD
func bunCRUD(ctx context.Context, db *bun.DB) error {
// 생성
u := &User{Name: "Alice", Email: "alice@example.com"}
if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }
// 읽기
var got User
if err := db.NewSelect().Model(&got).
Where("id = ?", u.ID).
Scan(ctx); err != nil { return err }
// 업데이트
if _, err := db.NewUpdate().Model(&got).
Set("email = ?", "alice+1@example.com").
WherePK().
Exec(ctx); err != nil { return err }
// 삭제
if _, err := db.NewDelete().Model(&got).WherePK().Exec(ctx); err != nil { return err }
return nil
}
참고 사항
.Relation(". ..")
을 통해 명시적인 조인/간접 로딩.- 마이그레이션을 위해 별도의
bun/migrate
패키지를 사용합니다.
sqlc
sqlc는 기술적으로 ORM이 아닙니다. SQL을 작성하고, 타입 안전한 Go 메서드를 생성합니다.
sqlc.yaml
version: "2"
sql:
- engine: postgresql
queries: db/queries
schema: db/migrations
gen:
go:
package: db
out: internal/db
sql_package: "database/sql" # 또는 "github.com/jackc/pgx/v5"
쿼리 (db/queries/users.sql
)
-- name: CreateUser :one
INSERT INTO users (name, email)
VALUES ($1, $2)
RETURNING id, name, email;
-- name: GetUser :one
SELECT id, name, email FROM users WHERE id = $1;
-- name: UpdateUserEmail :one
UPDATE users SET email = $2 WHERE id = $1
RETURNING id, name, email;
-- name: DeleteUser :exec
DELETE FROM users WHERE id = $1;
생성
sqlc generate
사용
import (
"database/sql"
_ "github.com/jackc/pgx/v5/stdlib"
"your/module/internal/db"
)
func sqlcCRUD(ctx context.Context) error {
dsn := os.Getenv("DATABASE_URL")
sqldb, err := sql.Open("pgx", dsn)
if err != nil { return err }
q := db.New(sqldb)
// 생성
u, err := q.CreateUser(ctx, db.CreateUserParams{
Name: "Alice", Email: "alice@example.com",
})
if err != nil { return err }
// 읽기
got, err := q.GetUser(ctx, u.ID)
if err != nil { return err }
// 업데이트
up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
ID: got.ID, Email: "alice+1@example.com",
})
if err != nil { return err }
// 삭제
if err := q.DeleteUser(ctx, up.ID); err != nil { return err }
return nil
}
참고 사항
- 마이그레이션은 직접 제공 (예:
golang-migrate
). - 동적 쿼리: 여러 SQL 변형을 작성하거나 작은 빌더와 결합하세요.
성능 참고 사항
- GORM: 편리하지만, 리플렉션/추상화 오버헤드를 추가합니다. 일반적인 CRUD에 적합하지만, N+1 쿼리에 주의하세요 (
.Joins
또는 선택적.Preload
를 사용하세요). - Ent: 생성된 코드는 리플렉션을 피하고, 복잡한 스키마에 적합합니다. 일반적인 런타임 마법 ORM보다 종종 더 빠릅니다.
- Bun:
database/sql
위에 얇은 츸을 제공하며, 빠르고 명확하며, 대량 작업 및 대규모 결과 세트에 적합합니다. - sqlc: 컴파일 시간 안전성과 함께 원시 SQL 성능을 제공합니다.
일반 팁
- pgx (v5) 드라이버와 context를 사용하세요.
- 높은 처리량을 위해 배치 작업 (
COPY
, 다중 행INSERT
)을 선호하세요. - SQL 프로파일링:
EXPLAIN ANALYZE
, 인덱스, 커버링 인덱스, 불필요한 라운드트립 피하기. - 연결 재사용; 작업량에 따라 풀 크기를 조정하세요.
개발자 경험 및 생태계
- GORM: 가장 큰 커뮤니티, 많은 예제/플러그인; 고급 패턴에 대해 학습 곡선이 더 가파릅니다.
- Ent: 훌륭한 문서; 코드 생성 단계가 주요 사고 전환; 리팩토링 중심 팀에 매우 적합합니다.
- Bun: 읽기 쉬운, 예측 가능한 쿼리; 작은 하지만 활발한 커뮤니티; Postgres 기능이 우수합니다.
- sqlc: 최소한의 런타임 의존성; 마이그레이션 도구 및 CI와 잘 통합; SQL에 익숙한 팀에게 최고입니다.
기능 강조점
- 관계 및 간접 로딩: 모두 관계를 처리합니다; GORM (태그 +
Preload
/Joins
), Ent (엣지 +.With...()
), Bun (Relation(...)
), sqlc (조인을 직접 작성). - 마이그레이션: GORM (자동 마이그레이션; 프로덕션에서는 주의), Ent (생성된/차이 SQL), Bun (
bun/migrate
), sqlc (외부 도구). - 훅/확장성: GORM (콜백/플러그인), Ent (훅/미들웨어 + 템플릿/코드 생성), Bun (미들웨어 유사한 쿼리 훅, 원시 SQL이 쉬움), sqlc (앱 레이어에서 조합).
- JSON/배열 (Postgres): Bun과 GORM은 편리한 도움자 제공; Ent/sqlc는 사용자 정의 타입 또는 SQL을 통해 처리.
무엇을 선택할지 결정하는 경우
- GORM을 선택하세요: 최대의 편리함, 풍부한 기능, 일반적인 CRUD 서비스의 빠른 프로토타이핑이 필요할 때.
- Ent를 선택하세요: 컴파일 시간 안전성, 명확한 스키마, 더 큰 팀에서의 장기 유지보수를 중시할 때.
- Bun을 선택하세요: 성능과 명확한 SQL 형식의 쿼리와 ORM의 편리함이 필요할 때.
- sqlc를 선택하세요: 팀이 순수 SQL과 타입 안전한 Go 바인딩을 선호하고, 실행 시간 오버헤드가 없을 때.
로컬 PostgreSQL용 최소 docker-compose.yml
version: "3.8"
services:
db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: app
ports: ["5432:5432"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d app"]
interval: 5s
timeout: 3s
retries: 5