sqlc — SQL에서 타입 안전 Go 코드 자동 생성
sqlc는 SQL 쿼리를 작성하면 타입 안전한 Go 코드를 자동 생성 해주는 도구입니다. SQL의 표현력과 Go의 타입 안전성을 동시에 누릴 수 있습니다.
sqlc 소개
sqlc는 ORM의 "SQL 숨기기"와 raw SQL의 "타입 없음" 문제를 모두 해결합니다.
SQL 쿼리 파일 (.sql)
↓ sqlc generate
타입 안전 Go 코드 (models.go, query.go)
↓
직접 사용 (컴파일 타임 타입 체크)
설치
# Go 도구로 설치
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# macOS (Homebrew)
brew install sqlc
프로젝트 구성
myapp/
├── sqlc.yaml # sqlc 설정 파일
├── db/
│ ├── migrations/ # 마이그레이션 파일
│ │ └── 001_init.sql
│ ├── queries/ # SQL 쿼리 파일
│ │ └── users.sql
│ └── sqlc/ # 생성된 코드 (수정 금지)
│ ├── db.go
│ ├── models.go
│ └── users.sql.go
└── main.go
sqlc.yaml 설정 파일
version: "2"
sql:
- engine: "postgresql"
queries: "./db/queries"
schema: "./db/migrations"
gen:
go:
package: "sqlcdb"
out: "./db/sqlc"
emit_json_tags: true
emit_prepared_queries: false
emit_interface: true
emit_exact_table_names: false
emit_empty_slices: true
스키마 및 쿼리 작성
마이그레이션 파일 (스키마 정의)
-- db/migrations/001_init.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
age INT,
bio TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
쿼리 파일
쿼리는 특수 주석으로 이름과 반환 타입을 지정합니다.
-- db/queries/users.sql
-- name: GetUser :one
SELECT * FROM users
WHERE id = $1 LIMIT 1;
-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = $1 LIMIT 1;
-- name: ListUsers :many
SELECT * FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;
-- name: CountUsers :one
SELECT COUNT(*) FROM users;
-- name: CreateUser :one
INSERT INTO users (name, email, age, bio)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: UpdateUser :one
UPDATE users
SET name = $2,
age = $3,
bio = $4,
updated_at = NOW()
WHERE id = $1
RETURNING *;
-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1;
-- name: SearchUsers :many
SELECT * FROM users
WHERE name ILIKE $1 OR email ILIKE $1
ORDER BY name;
쿼리 반환 타입 지정자
| 지정자 | 의미 |
|---|---|
:one | *Model 단일 행 반환 |
:many | []Model 다중 행 반환 |
:exec | error 반환, 행 없음 |
:execresult | sql.Result, error 반환 |
:execrows | int64, error 반환 |
코드 생성
sqlc generate
생성되는 파일들:
models.go — 테이블 구조체 자동 생성
// Code generated by sqlc. DO NOT EDIT.
package sqlcdb
import (
"time"
)
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age sql.NullInt32 `json:"age"`
Bio sql.NullString `json:"bio"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Post struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
Title string `json:"title"`
Content sql.NullString `json:"content"`
Published bool `json:"published"`
CreatedAt time.Time `json:"created_at"`
}
users.sql.go — 쿼리 함수 자동 생성
// Code generated by sqlc. DO NOT EDIT.
package sqlcdb
const createUser = `-- name: CreateUser :one
INSERT INTO users (name, email, age, bio)
VALUES ($1, $2, $3, $4)
RETURNING id, name, email, age, bio, created_at, updated_at
`
type CreateUserParams struct {
Name string `json:"name"`
Email string `json:"email"`
Age sql.NullInt32 `json:"age"`
Bio sql.NullString `json:"bio"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.Name, arg.Email, arg.Age, arg.Bio)
var i User
err := row.Scan(
&i.ID, &i.Name, &i.Email, &i.Age, &i.Bio, &i.CreatedAt, &i.UpdatedAt,
)
return i, err
}
실제 사용 예제
package main
import (
"context"
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq"
"myapp/db/sqlc"
)
func main() {
db, err := sql.Open("postgres",
"host=localhost user=myuser password=mypass dbname=mydb sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
queries := sqlcdb.New(db)
ctx := context.Background()
// 사용자 생성
user, err := queries.CreateUser(ctx, sqlcdb.CreateUserParams{
Name: "김고랭",
Email: "golang@example.com",
Age: sql.NullInt32{Int32: 30, Valid: true},
Bio: sql.NullString{String: "Go 개발자", Valid: true},
})
if err != nil {
log.Fatal("사용자 생성 실패:", err)
}
fmt.Printf("생성됨: ID=%d, 이름=%s\n", user.ID, user.Name)
// 사용자 조회
found, err := queries.GetUser(ctx, user.ID)
if err == sql.ErrNoRows {
fmt.Println("사용자를 찾을 수 없음")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Printf("조회됨: %s <%s>\n", found.Name, found.Email)
}
// 목록 조회 (페이지네이션)
users, err := queries.ListUsers(ctx, sqlcdb.ListUsersParams{
Limit: 10,
Offset: 0,
})
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Printf(" - [%d] %s\n", u.ID, u.Name)
}
// 수정
updated, err := queries.UpdateUser(ctx, sqlcdb.UpdateUserParams{
ID: user.ID,
Name: "김고랭(수정)",
Age: sql.NullInt32{Int32: 31, Valid: true},
Bio: sql.NullString{String: "시니어 Go 개발자", Valid: true},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("수정됨: %s\n", updated.Name)
// 삭제
if err := queries.DeleteUser(ctx, user.ID); err != nil {
log.Fatal(err)
}
fmt.Println("삭제 완료")
}
트랜잭션과 함께 사용
sqlc는 *sql.DB 또는 *sql.Tx를 모두 받을 수 있습니다.
func transferOwnership(ctx context.Context, db *sql.DB, fromUserID, postID, toUserID int64) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// 트랜잭션에서 sqlc 사용
qtx := sqlcdb.New(tx)
// 게시글 소유권 확인
post, err := qtx.GetPost(ctx, postID)
if err != nil {
return fmt.Errorf("게시글 조회 실패: %w", err)
}
if post.UserID != fromUserID {
return fmt.Errorf("소유권 없음")
}
// 소유권 이전
if err := qtx.UpdatePostOwner(ctx, sqlcdb.UpdatePostOwnerParams{
ID: postID,
UserID: toUserID,
}); err != nil {
return fmt.Errorf("소유권 이전 실패: %w", err)
}
return tx.Commit()
}
sqlc vs GORM vs database/sql 비교
| 기준 | database/sql | GORM | sqlc |
|---|---|---|---|
| SQL 제어 | 완전 제어 | 제한적 | 완전 제어 |
| 타입 안전성 | 수동 | ORM 방식 | 컴파일 타임 |
| 코드 양 | 많음 | 적음 | 중간 |
| 복잡 쿼리 | 쉬움 | 어려움 | 쉬움 |
| 성능 | 최고 | 낮음 | 최고 |
| 학습 곡선 | 중간 | 낮음 | 낮음 |
| 마이그레이션 | 별도 도구 | AutoMigrate | 별도 도구 |
추천 시나리오:
- database/sql: 복잡한 커스텀 쿼리, 최고 성능이 필요한 경우
- GORM: 빠른 개발, 단순한 CRUD 중심 앱
- sqlc: SQL을 잘 알고 타입 안전성도 원하는 경우 (추천)
핵심 정리
- sqlc는 SQL → Go 코드를 자동 생성 하여 타입 안전성을 보장합니다.
- 생성된 코드는 수정하지 말고, SQL 파일을 수정 후 재생성합니다.
- 트랜잭션은
sqlc.New(tx)패턴으로 동일하게 사용합니다. - 스키마 변경 → SQL 쿼리 수정 →
sqlc generate→ 컴파일 에러로 누락 확인의 워크플로우가 매우 강력합니다.