Go에서 데이터베이스를 다루는 방법을 database/sql, sqlx, GORM, pgx를 통해 비교하고, 커넥션 풀링, 트랜잭션, 마이그레이션, 쿼리 빌더 패턴을 실전 예제로 다룹니다.
database/sql은 Go 표준 라이브러리의 데이터베이스 추상화 계층입니다. 드라이버 인터페이스를 정의하고, 구체적인 데이터베이스 드라이버는 별도 패키지로 제공됩니다.
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq" // 드라이버 등록 (사이드 이펙트 임포트)
)
func main() {
connStr := "host=localhost port=5432 user=app dbname=myapp sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 연결 확인
if err := db.PingContext(context.Background()); err != nil {
log.Fatal("데이터베이스 연결 실패:", err)
}
}sql.Open은 실제 연결을 생성하지 않습니다. 커넥션 풀의 설정만 완료하며, 실제 연결은 첫 번째 쿼리 실행 시 지연 생성됩니다. 연결 가능 여부를 확인하려면 반드시 PingContext를 호출하세요.
// 단일 행 조회
func (r *UserRepo) FindByID(ctx context.Context, id int64) (*User, error) {
var user User
err := r.db.QueryRowContext(ctx,
"SELECT id, name, email, created_at FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("UserRepo.FindByID(%d): %w", id, err)
}
return &user, nil
}
// 다중 행 조회
func (r *UserRepo) FindAll(ctx context.Context) ([]*User, error) {
rows, err := r.db.QueryContext(ctx,
"SELECT id, name, email, created_at FROM users ORDER BY created_at DESC",
)
if err != nil {
return nil, fmt.Errorf("UserRepo.FindAll: %w", err)
}
defer rows.Close() // 반드시 닫아야 커넥션이 풀로 반환됨
var users []*User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt); err != nil {
return nil, fmt.Errorf("UserRepo.FindAll scan: %w", err)
}
users = append(users, &u)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("UserRepo.FindAll iteration: %w", err)
}
return users, nil
}
// 삽입
func (r *UserRepo) Create(ctx context.Context, user *User) error {
err := r.db.QueryRowContext(ctx,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, created_at",
user.Name, user.Email,
).Scan(&user.ID, &user.CreatedAt)
if err != nil {
return fmt.Errorf("UserRepo.Create: %w", err)
}
return nil
}rows.Close()를 호출하지 않으면 데이터베이스 커넥션이 풀로 반환되지 않습니다. 커넥션 고갈의 주요 원인이므로 반드시 defer rows.Close()를 사용하세요. 또한 rows.Err()로 반복 중 발생한 에러를 확인해야 합니다.
database/sql은 내장 커넥션 풀을 제공합니다. 적절한 풀 크기 설정은 성능에 직접적인 영향을 미칩니다.
db, _ := sql.Open("postgres", connStr)
// 풀 설정
db.SetMaxOpenConns(25) // 최대 동시 연결 수
db.SetMaxIdleConns(10) // 유휴 연결 유지 수
db.SetConnMaxLifetime(30 * time.Minute) // 연결 최대 수명
db.SetConnMaxIdleTime(5 * time.Minute) // 유휴 연결 최대 대기 시간MaxOpenConns: CPU 코어 수 * 2 + 디스크 수 (PostgreSQL 권장 공식)
일반적으로 25-50 범위가 적합
MaxIdleConns: MaxOpenConns의 50% 이상 권장
너무 낮으면 연결 재생성 비용 발생
ConnMaxLifetime: 30분-1시간 (로드밸런서 뒤의 DB에서는 중요)
ConnMaxIdleTime: 5-10분sqlx는 database/sql의 상위 호환 확장으로, 구조체 스캔과 네임드 파라미터를 지원합니다.
import "github.com/jmoiron/sqlx"
type User struct {
ID int64 `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
CreatedAt time.Time `db:"created_at"`
}
// 구조체 자동 매핑 -- Scan 호출 불필요
func (r *UserRepo) FindByID(ctx context.Context, id int64) (*User, error) {
var user User
err := r.db.GetContext(ctx, &user,
"SELECT id, name, email, created_at FROM users WHERE id = $1", id,
)
if err != nil {
return nil, fmt.Errorf("UserRepo.FindByID(%d): %w", id, err)
}
return &user, nil
}
// 다중 행도 간결하게
func (r *UserRepo) FindAll(ctx context.Context) ([]*User, error) {
var users []*User
err := r.db.SelectContext(ctx, &users,
"SELECT id, name, email, created_at FROM users ORDER BY created_at DESC",
)
if err != nil {
return nil, fmt.Errorf("UserRepo.FindAll: %w", err)
}
return users, nil
}
// 네임드 파라미터
func (r *UserRepo) Create(ctx context.Context, user *User) error {
query := `INSERT INTO users (name, email) VALUES (:name, :email) RETURNING id`
rows, err := r.db.NamedQueryContext(ctx, query, user)
if err != nil {
return fmt.Errorf("UserRepo.Create: %w", err)
}
defer rows.Close()
if rows.Next() {
rows.Scan(&user.ID)
}
return nil
}pgx는 PostgreSQL에 특화된 고성능 드라이버입니다. database/sql 호환 모드와 네이티브 모드 두 가지를 지원합니다.
import (
"github.com/jackc/pgx/v5/pgxpool"
)
// 네이티브 모드 -- database/sql 미사용
func NewPool(ctx context.Context, connStr string) (*pgxpool.Pool, error) {
config, err := pgxpool.ParseConfig(connStr)
if err != nil {
return nil, fmt.Errorf("설정 파싱 실패: %w", err)
}
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = 30 * time.Minute
config.MaxConnIdleTime = 5 * time.Minute
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("풀 생성 실패: %w", err)
}
return pool, nil
}
// 쿼리 -- pgx 네이티브 API
func (r *UserRepo) FindByID(ctx context.Context, id int64) (*User, error) {
var user User
err := r.pool.QueryRow(ctx,
"SELECT id, name, email, created_at FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("UserRepo.FindByID(%d): %w", id, err)
}
return &user, nil
}PostgreSQL만 사용하는 프로젝트라면 pgx 네이티브 모드를 권장합니다. COPY, LISTEN/NOTIFY, 배열 타입, JSON 타입 등 PostgreSQL 고유 기능을 직접 활용할 수 있으며, database/sql 추상화 계층의 오버헤드가 없어 성능도 더 좋습니다.
GORM은 Go에서 가장 널리 사용되는 ORM(Object-Relational Mapping)입니다.
import "gorm.io/gorm"
type User struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"uniqueIndex;size:255;not null"`
Articles []Article `gorm:"foreignKey:AuthorID"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"` // 소프트 삭제
}
// CRUD
func (r *UserRepo) Create(ctx context.Context, user *User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *UserRepo) FindByID(ctx context.Context, id uint) (*User, error) {
var user User
result := r.db.WithContext(ctx).First(&user, id)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, ErrNotFound
}
return nil, result.Error
}
return &user, nil
}
// 관계 로딩
func (r *UserRepo) FindWithArticles(ctx context.Context, id uint) (*User, error) {
var user User
result := r.db.WithContext(ctx).
Preload("Articles", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at DESC").Limit(10)
}).
First(&user, id)
return &user, result.Error
}func (s *OrderService) PlaceOrder(ctx context.Context, order *Order) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("트랜잭션 시작 실패: %w", err)
}
defer tx.Rollback() // 커밋되지 않으면 자동 롤백
// 재고 확인 및 차감
var stock int
err = tx.QueryRowContext(ctx,
"SELECT stock FROM products WHERE id = $1 FOR UPDATE", order.ProductID,
).Scan(&stock)
if err != nil {
return fmt.Errorf("재고 조회 실패: %w", err)
}
if stock < order.Quantity {
return ErrInsufficientStock
}
_, err = tx.ExecContext(ctx,
"UPDATE products SET stock = stock - $1 WHERE id = $2",
order.Quantity, order.ProductID,
)
if err != nil {
return fmt.Errorf("재고 차감 실패: %w", err)
}
// 주문 생성
_, err = tx.ExecContext(ctx,
"INSERT INTO orders (product_id, quantity, total) VALUES ($1, $2, $3)",
order.ProductID, order.Quantity, order.Total,
)
if err != nil {
return fmt.Errorf("주문 생성 실패: %w", err)
}
return tx.Commit()
}트랜잭션의 시작, 커밋, 롤백을 추상화하는 헬퍼 함수를 만들면 반복 코드를 줄일 수 있습니다.
func WithTransaction(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("트랜잭션 시작 실패: %w", err)
}
defer tx.Rollback()
if err := fn(tx); err != nil {
return err
}
return tx.Commit()
}
// 사용
err := WithTransaction(ctx, db, func(tx *sql.Tx) error {
// 트랜잭션 내 작업들
if _, err := tx.ExecContext(ctx, query1, args1...); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, query2, args2...); err != nil {
return err
}
return nil
})golang-migrate는 가장 널리 사용되는 마이그레이션 도구입니다.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);DROP TABLE IF EXISTS users;# 마이그레이션 적용
migrate -path ./migrations -database "postgres://localhost:5432/myapp?sslmode=disable" up
# 롤백
migrate -path ./migrations -database "postgres://..." down 1
# 특정 버전으로
migrate -path ./migrations -database "postgres://..." goto 3goose는 SQL과 Go 코드 모두에서 마이그레이션을 작성할 수 있습니다.
-- +goose Up
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
-- +goose Down
DROP TABLE users;마이그레이션 파일은 반드시 **순방향(up)**과 역방향(down) 양쪽을 작성해야 합니다. 프로덕션 환경에서 문제 발생 시 롤백이 가능해야 하며, 팀원들의 로컬 환경을 일관되게 유지하는 데에도 필수적입니다.
database/sql sqlx pgx GORM
SQL 제어 완전 완전 완전 제한적
학습 곡선 낮음 낮음 낮음 중간
구조체 매핑 수동 자동 수동 자동
관계 로딩 수동 수동 수동 자동
마이그레이션 외부 도구 외부 도구 외부 도구 내장
성능 좋음 좋음 매우 좋음 보통
DB 호환 범용 범용 PostgreSQL 범용실무 추천: PostgreSQL을 사용하는 프로젝트에서는 pgx + squirrel(쿼리 빌더) 조합을 권장합니다. SQL에 대한 완전한 제어를 유지하면서도, 동적 쿼리 구성의 편의성을 얻을 수 있습니다. 빠른 프로토타이핑이 필요하거나 관계가 복잡한 경우에는 GORM이 유리합니다.
이번 장에서 살펴본 핵심 내용을 정리합니다.
8장에서는 미들웨어, 인증, API 설계를 다룹니다. 미들웨어 패턴, JWT 인증 구현, CORS, 레이트 리미팅, 요청 유효성 검사, OpenAPI 문서 생성, 그리고 Go 1.21+의 log/slog를 활용한 구조화된 로깅을 살펴봅니다.
이 글이 도움이 되셨나요?
Go 백엔드의 미들웨어 패턴, JWT 인증 구현, CORS 설정, 레이트 리미팅, 요청 유효성 검사, OpenAPI/Swagger 문서 생성, log/slog를 활용한 구조화된 로깅을 다룹니다.
Go의 표준 라이브러리 net/http부터 Gin, Chi, Fiber, Echo까지 주요 웹 프레임워크의 아키텍처, 성능 특성, 미들웨어 구조를 비교하고 프로젝트별 선택 기준을 제시합니다.
Go의 testing 패키지를 중심으로 테이블 기반 테스트, testify, httptest, 목(Mock) 생성, testcontainers-go 통합 테스트, 벤치마크, 퍼징 테스트 전략을 다룹니다.