본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout
TechProjectsBooksAbout
© 2026 Kreath. All rights reserved.
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 7장: 데이터베이스 연동 - SQL, ORM, 마이그레이션
2026년 8월 23일·프로그래밍·

7장: 데이터베이스 연동 - SQL, ORM, 마이그레이션

Go에서 데이터베이스를 다루는 방법을 database/sql, sqlx, GORM, pgx를 통해 비교하고, 커넥션 풀링, 트랜잭션, 마이그레이션, 쿼리 빌더 패턴을 실전 예제로 다룹니다.

14분1,132자11개 섹션
concurrencyperformancedesign-patternstestinginfrastructure
공유
go-backend7 / 11
1234567891011
이전6장: 웹 프레임워크 비교 - Gin, Chi, Fiber, Echo다음8장: 미들웨어, 인증, API 설계

학습 목표

  • database/sql의 구조와 드라이버 모델을 이해한다
  • sqlx, GORM, pgx의 차이점과 적합한 사용 시나리오를 파악한다
  • 커넥션 풀링과 트랜잭션을 올바르게 관리할 수 있다
  • 마이그레이션 도구(golang-migrate, goose)를 활용할 수 있다

database/sql 표준 패키지

기본 구조

database/sql은 Go 표준 라이브러리의 데이터베이스 추상화 계층입니다. 드라이버 인터페이스를 정의하고, 구체적인 데이터베이스 드라이버는 별도 패키지로 제공됩니다.

database-sql-basic.go
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)
    }
}
Info

sql.Open은 실제 연결을 생성하지 않습니다. 커넥션 풀의 설정만 완료하며, 실제 연결은 첫 번째 쿼리 실행 시 지연 생성됩니다. 연결 가능 여부를 확인하려면 반드시 PingContext를 호출하세요.

CRUD 연산

database-sql-crud.go
go
// 단일 행 조회
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
}
Warning

rows.Close()를 호출하지 않으면 데이터베이스 커넥션이 풀로 반환되지 않습니다. 커넥션 고갈의 주요 원인이므로 반드시 defer rows.Close()를 사용하세요. 또한 rows.Err()로 반복 중 발생한 에러를 확인해야 합니다.


커넥션 풀링

풀 설정

database/sql은 내장 커넥션 풀을 제공합니다. 적절한 풀 크기 설정은 성능에 직접적인 영향을 미칩니다.

connection-pool.go
go
db, _ := sql.Open("postgres", connStr)
 
// 풀 설정
db.SetMaxOpenConns(25)                 // 최대 동시 연결 수
db.SetMaxIdleConns(10)                 // 유휴 연결 유지 수
db.SetConnMaxLifetime(30 * time.Minute) // 연결 최대 수명
db.SetConnMaxIdleTime(5 * time.Minute)  // 유휴 연결 최대 대기 시간
커넥션 풀 설정 가이드
text
MaxOpenConns: CPU 코어 수 * 2 + 디스크 수 (PostgreSQL 권장 공식)
              일반적으로 25-50 범위가 적합
MaxIdleConns: MaxOpenConns의 50% 이상 권장
              너무 낮으면 연결 재생성 비용 발생
ConnMaxLifetime: 30분-1시간 (로드밸런서 뒤의 DB에서는 중요)
ConnMaxIdleTime: 5-10분

sqlx -- database/sql 확장

sqlx는 database/sql의 상위 호환 확장으로, 구조체 스캔과 네임드 파라미터를 지원합니다.

sqlx-example.go
go
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 전용 드라이버

pgx는 PostgreSQL에 특화된 고성능 드라이버입니다. database/sql 호환 모드와 네이티브 모드 두 가지를 지원합니다.

pgx-native.go
go
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
}
Tip

PostgreSQL만 사용하는 프로젝트라면 pgx 네이티브 모드를 권장합니다. COPY, LISTEN/NOTIFY, 배열 타입, JSON 타입 등 PostgreSQL 고유 기능을 직접 활용할 수 있으며, database/sql 추상화 계층의 오버헤드가 없어 성능도 더 좋습니다.


GORM -- ORM

GORM은 Go에서 가장 널리 사용되는 ORM(Object-Relational Mapping)입니다.

gorm-example.go
go
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
}

트랜잭션

database/sql 트랜잭션

transaction-stdlib.go
go
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()
}

트랜잭션 헬퍼 패턴

트랜잭션의 시작, 커밋, 롤백을 추상화하는 헬퍼 함수를 만들면 반복 코드를 줄일 수 있습니다.

transaction-helper.go
go
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

golang-migrate는 가장 널리 사용되는 마이그레이션 도구입니다.

migrations/000001_create_users.up.sql
sql
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);
migrations/000001_create_users.down.sql
sql
DROP TABLE IF EXISTS users;
마이그레이션 CLI 명령어
bash
# 마이그레이션 적용
migrate -path ./migrations -database "postgres://localhost:5432/myapp?sslmode=disable" up
 
# 롤백
migrate -path ./migrations -database "postgres://..." down 1
 
# 특정 버전으로
migrate -path ./migrations -database "postgres://..." goto 3

goose

goose는 SQL과 Go 코드 모두에서 마이그레이션을 작성할 수 있습니다.

migrations/00001_create_users.sql
sql
-- +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;
Info

마이그레이션 파일은 반드시 **순방향(up)**과 역방향(down) 양쪽을 작성해야 합니다. 프로덕션 환경에서 문제 발생 시 롤백이 가능해야 하며, 팀원들의 로컬 환경을 일관되게 유지하는 데에도 필수적입니다.


라이브러리 선택 가이드

데이터베이스 라이브러리 비교
text
              database/sql  sqlx       pgx        GORM
SQL 제어      완전          완전       완전       제한적
학습 곡선     낮음          낮음       낮음       중간
구조체 매핑   수동          자동       수동       자동
관계 로딩     수동          수동       수동       자동
마이그레이션  외부 도구     외부 도구  외부 도구  내장
성능          좋음          좋음       매우 좋음  보통
DB 호환       범용          범용       PostgreSQL 범용
Tip

실무 추천: PostgreSQL을 사용하는 프로젝트에서는 pgx + squirrel(쿼리 빌더) 조합을 권장합니다. SQL에 대한 완전한 제어를 유지하면서도, 동적 쿼리 구성의 편의성을 얻을 수 있습니다. 빠른 프로토타이핑이 필요하거나 관계가 복잡한 경우에는 GORM이 유리합니다.


정리

이번 장에서 살펴본 핵심 내용을 정리합니다.

  • database/sql은 Go의 표준 데이터베이스 추상화이며, 드라이버 모델을 통해 다양한 DB를 지원합니다
  • sqlx는 database/sql에 구조체 자동 매핑과 네임드 파라미터를 추가합니다
  • pgx는 PostgreSQL 전용 고성능 드라이버로, 네이티브 모드에서 최고의 성능을 제공합니다
  • 커넥션 풀링 설정과 트랜잭션 관리는 프로덕션 안정성의 핵심입니다
  • 마이그레이션은 golang-migrate 또는 goose로 관리하며, 항상 up/down 양쪽을 작성합니다

다음 장 미리보기

8장에서는 미들웨어, 인증, API 설계를 다룹니다. 미들웨어 패턴, JWT 인증 구현, CORS, 레이트 리미팅, 요청 유효성 검사, OpenAPI 문서 생성, 그리고 Go 1.21+의 log/slog를 활용한 구조화된 로깅을 살펴봅니다.

이 글이 도움이 되셨나요?

관련 글

프로그래밍

8장: 미들웨어, 인증, API 설계

Go 백엔드의 미들웨어 패턴, JWT 인증 구현, CORS 설정, 레이트 리미팅, 요청 유효성 검사, OpenAPI/Swagger 문서 생성, log/slog를 활용한 구조화된 로깅을 다룹니다.

2026년 8월 25일·11분
프로그래밍

6장: 웹 프레임워크 비교 - Gin, Chi, Fiber, Echo

Go의 표준 라이브러리 net/http부터 Gin, Chi, Fiber, Echo까지 주요 웹 프레임워크의 아키텍처, 성능 특성, 미들웨어 구조를 비교하고 프로젝트별 선택 기준을 제시합니다.

2026년 8월 20일·12분
프로그래밍

9장: 테스트 전략 - 단위, 통합, 벤치마크

Go의 testing 패키지를 중심으로 테이블 기반 테스트, testify, httptest, 목(Mock) 생성, testcontainers-go 통합 테스트, 벤치마크, 퍼징 테스트 전략을 다룹니다.

2026년 8월 28일·14분
이전 글6장: 웹 프레임워크 비교 - Gin, Chi, Fiber, Echo
다음 글8장: 미들웨어, 인증, API 설계

댓글

목차

약 14분 남음
  • 학습 목표
  • database/sql 표준 패키지
    • 기본 구조
    • CRUD 연산
  • 커넥션 풀링
    • 풀 설정
  • sqlx -- database/sql 확장
  • pgx -- PostgreSQL 전용 드라이버
  • GORM -- ORM
  • 트랜잭션
    • database/sql 트랜잭션
    • 트랜잭션 헬퍼 패턴
  • 마이그레이션
    • golang-migrate
    • goose
  • 라이브러리 선택 가이드
  • 정리
  • 다음 장 미리보기