PostgreSQL용 Go ORM 비교: GORM vs Ent vs Bun vs sqlc
GO ORM에 대한 실용적이고 코드 중심적인 접근
Page content
GO용 가장 주목받는 ORM으로는 GORM, Ent, Bun, sqlc가 있습니다. 이 글에서는 순수 GO를 사용한 CRUD 연산 예시와 함께 각 도구를 간략히 비교해 보겠습니다.

TL;DR
- GORM: 기능이 풍부하고 편리하며 즉시 활용하기 가장 쉽지만, 런타임 오버헤드가 다소 큽니다.
- Ent: 스키마를 코드로 관리하고 타입 안전성을 갖춘 API를 생성하며, 대규모 코드베이스와 리팩토링에 적합합니다.
- Bun: 가볍고 SQL 중심의 쿼리 빌더/ORM으로, 설계상 명시적이며 빠른 속도와 우수한 Postgres 기능을 제공합니다.
- sqlc (엄밀히 말해 ORM은 아니지만): SQL을 작성하면 타입 안전한 Go 코드가 생성됩니다. 가장 낮은 수준의 성능과 제어력을 제공하며 런타임 마법(자동화)이 없습니다.
선택 기준 및 간단한 비교
제 기준은 다음과 같습니다:
- 성능: 대기 시간/처리량, 피할 수 있는 오버헤드, 배치 작업.
- 개발자 경험(DX): 학습 곡선, 타입 안전성, 디버깅 용이성, 코드 생성 마찰.
- 생태계: 문서, 예시, 활동성, 통합(마이그레이션, 추적).
- 기능 집합: 관계, 즉시 로딩(Eager loading), 마이그레이션, 후크, 원시 SQL 탈출구.
| 도구 | 패러다임 | 타입 안전성 | 관계 | 마이그레이션 | 원시 SQL 사용성 | 일반적인 사용 사례 |
|---|---|---|---|---|---|---|
| GORM | 액티브 레코드(Active Record) 스타일 ORM | 중간 (런타임) | 있음 (태그, Preload/Joins) | 자동 마이그레이션 (선택적) | db.Raw(...) |
빠른 출시, 풍부한 기능, 일반적인 CRUD 애플리케이션 |
| Ent | 스키마 → 코드 생성 → 플루언트 API | 높음 (컴파일 시간) | 일급 객체 (edges) | 생성된 SQL (별도 단계) | entsql, 사용자 정의 SQL |
대규모 코드베이스, 리팩토링 중심 팀, 엄격한 타이핑 |
| Bun | SQL 중심 쿼리 빌더/ORM | 중간~높음 | 명시적 (Relation) |
별도 마이그레이션 패키지 | 자연스러움 (빌더 + 원시 SQL) | 성능 중심 서비스, Postgres 기능 활용 |
| sqlc | SQL → 코드 생성 함수 (ORM 아님) | 높음 (컴파일 시간) | SQL 조인(Joins)을 통해 | 외부 도구 (예: golang-migrate) | SQL 그 자체 | 최대 제어 및 속도; DBA 친화적인 팀 |
예제를 통한 CRUD
설정 (PostgreSQL)
pgx 또는 도구의 네이티브 PG 드라이버를 사용하세요. DSN 예시:
export DATABASE_URL='postgres://user:pass@localhost:5432/app?sslmode=disable'
임포트 (모든 ORM에 공통)
Go 코드 예시가 포함된 각 파일의 시작 부분에 다음을 추가하세요:
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 {
// 생성(Create)
u := User{Name: "Alice", Email: "alice@example.com"}
if err := db.WithContext(ctx).Create(&u).Error; err != nil { return err }
// 읽기(Read)
var got User
if err := db.WithContext(ctx).First(&got, u.ID).Error; err != nil { return err }
// 수정(Update)
if err := db.WithContext(ctx).Model(&got).
Update("email", "alice+1@example.com").Error; err != nil { return err }
// 삭제(Delete)
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 {
// 생성(Create)
u, err := client.User.Create().
SetName("Alice").
SetEmail("alice@example.com").
Save(ctx)
if err != nil { return err }
// 읽기(Read)
got, err := client.User.Get(ctx, u.ID)
if err != nil { return err }
// 수정(Update)
if _, err := client.User.UpdateOneID(got.ID).
SetEmail("alice+1@example.com").
Save(ctx); err != nil { return err }
// 삭제(Delete)
if err := client.User.DeleteOneID(got.ID).Exec(ctx); err != nil { return err }
return nil
}
참고
- 엔드투엔드 강한 타입 안전성; 관계를 위한 edges 사용.
- 마이그레이션 생성 또는 선호하는 마이그레이션 도구 사용.
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 {
// 생성(Create)
u := &User{Name: "Alice", Email: "alice@example.com"}
if _, err := db.NewInsert().Model(u).Exec(ctx); err != nil { return err }
// 읽기(Read)
var got User
if err := db.NewSelect().Model(&got).
Where("id = ?", u.ID).
Scan(ctx); err != nil { return err }
// 수정(Update)
if _, err := db.NewUpdate().Model(&got).
Set("email = ?", "alice+1@example.com").
WherePK().
Exec(ctx); err != nil { return err }
// 삭제(Delete)
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)
// 생성(Create)
u, err := q.CreateUser(ctx, db.CreateUserParams{
Name: "Alice", Email: "alice@example.com",
})
if err != nil { return err }
// 읽기(Read)
got, err := q.GetUser(ctx, u.ID)
if err != nil { return err }
// 수정(Update)
up, err := q.UpdateUserEmail(ctx, db.UpdateUserEmailParams{
ID: got.ID, Email: "alice+1@example.com",
})
if err != nil { return err }
// 삭제(Delete)
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(edges +.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 선택: 성능과 ORM의 편의가 도움이 되는 부분에서 명시적인 SQL 형태의 쿼리를 원한다면.
- sqlc 선택: (그리고 당신의 팀이) 타입 안전한 Go 바인딩과 제로 런타임 오버헤드가 있는 순수 SQL을 선호한다면. sqlc는 또한 Go에서의 CQRS 아키텍처 읽기 모델(read model) 측면에 자연스럽게 적합합니다. 여기서는 도메인 엔티티가 아닌 호출자를 위해 쿼리가 형성되며, 명시적인 SQL이 프로젝션에 대해 완전한 제어권을 제공합니다.
ORM 선택을 통합 스타일과 서비스 경계와 여전히 저울잡고 계신다면, 이 애플리케이션 아키텍처 개요가 결정을 더 넓은 프로덕션 컨텍스트에 배치하는 데 도움이 됩니다.
로컬 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