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 작업의 예시와 함께 비교해보겠습니다.

golang + postgresql

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

GO에서의 ORM 패키지 및 라이브러리

기타 유용한 링크