본문으로 건너뛰기

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 다중 행 반환
:execerror 반환, 행 없음
:execresultsql.Result, error 반환
:execrowsint64, 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/sqlGORMsqlc
SQL 제어완전 제어제한적완전 제어
타입 안전성수동ORM 방식컴파일 타임
코드 양많음적음중간
복잡 쿼리쉬움어려움쉬움
성능최고낮음최고
학습 곡선중간낮음낮음
마이그레이션별도 도구AutoMigrate별도 도구

추천 시나리오:

  • database/sql: 복잡한 커스텀 쿼리, 최고 성능이 필요한 경우
  • GORM: 빠른 개발, 단순한 CRUD 중심 앱
  • sqlc: SQL을 잘 알고 타입 안전성도 원하는 경우 (추천)

핵심 정리

  • sqlc는 SQL → Go 코드를 자동 생성 하여 타입 안전성을 보장합니다.
  • 생성된 코드는 수정하지 말고, SQL 파일을 수정 후 재생성합니다.
  • 트랜잭션은 sqlc.New(tx) 패턴으로 동일하게 사용합니다.
  • 스키마 변경 → SQL 쿼리 수정 → sqlc generate → 컴파일 에러로 누락 확인의 워크플로우가 매우 강력합니다.