Go generate와 sqlc를 사용한 Go에서의 타입 안전한 데이터베이스 연산
James Reed
Infrastructure Engineer · Leapcell

소개
백엔드 개발 세계에서 데이터베이스와의 상호 작용은 필수적인 작업입니다. Go는 database/sql
패키지를 통해 데이터베이스 액세스를 위한 강력한 추상화를 제공하지만, 애플리케이션 코드에 직접 SQL 쿼리를 작성하는 것은 종종 몇 가지 일반적인 함정으로 이어질 수 있습니다. 예를 들어, 열 이름을 잊거나, 테이블 이름을 오타하거나, 잘못된 데이터 유형 매핑을 사용하거나, SQL 스키마를 Go 구조체와 동기화하기 위한 끊임없는 노력을 기울이는 것들입니다. 이러한 문제들은 개발 속도를 늦출 뿐만 아니라 디버깅하기 어려운 런타임 오류를 발생시킵니다.
다행히도 현대적인 Go 개발 관행은 이러한 문제에 대한 세련된 해결책을 제공합니다. 이 글에서는 go generate
와 sqlc
라는 강력한 조합을 탐구합니다. 이 도구들을 통합함으로써 SQL 스키마 정의와 쿼리에서 직접 타입 안전한 Go 코드를 생성하는 프로세스를 자동화할 수 있습니다. 이 접근 방식은 개발자 생산성을 극적으로 향상시키고, 지루한 SQL 관련 버그의 가능성을 줄이며, 애플리케이션과 데이터베이스 간의 강력한 계약을 보장합니다. 원활한 통합을 달성하는 방법을 살펴보겠습니다.
핵심 개념 설명
구현 세부 사항으로 들어가기 전에 관련 핵심 기술을 명확히 합시다:
- SQL (Structured Query Language): 관계형 데이터베이스를 관리하고 조작하기 위한 표준 언어입니다. 원시 SQL로 데이터베이스 스키마와 쿼리를 작성할 것입니다.
go generate
: 명령 실행을 자동화하는 내장 Go 도구입니다. Go 소스 파일에//go:generate
지시문을 포함하면 컴파일 전에 코드 생성기와 같은 외부 프로그램을 실행하도록 Go 툴체인에 지시할 수 있습니다. 이것이 자동화된 코드 생성을 표준 Go 워크플로의 일부로 만드는 접착제입니다.sqlc
: SQL 쿼리 및 스키마 파일에서 Go 코드를 생성하는 명령줄 도구입니다.sqlc
는 SQL 데이터베이스 스키마를 읽고, 이를 기반으로 쿼리를 검증한 다음, 해당 쿼리를 실행하기 위한 타입 안전한 Go 코드를 생성합니다. 여기에는 테이블용 구조체, 쿼리 실행용 함수, 데이터 액세스 객체(DAO)용 인터페이스가 포함됩니다. 실행 시간을 런타임에서 컴파일 시간으로 이동시켜 Go와 SQL의 상호 작용을 훨씬 더 강력하고 오류가 적게 만드는 것이 핵심 가치입니다.
자동화된 타입 안전한 데이터베이스 액세스의 원리
go generate
와 sqlc
를 함께 사용하는 기본 원칙은 Go 프로젝트에서 SQL을 일급 시민으로 취급하는 것입니다. Go 코드에 SQL 문자열을 포함하는 대신, 별도의 SQL 파일에 스키마 정의(schema.sql
)와 쿼리(query.sql
)를 작성합니다. 그러면 sqlc
가 이러한 SQL 파일의 컴파일러 역할을 하여 이를 관용적인 Go 코드로 변환합니다.
일반적인 워크플로우는 다음과 같습니다.
- SQL 스키마 정의: 데이터베이스 테이블, 열, 제약 조건 등을 정의하는
schema.sql
파일을 만듭니다. - SQL 쿼리 작성: 애플리케이션에서 필요한
SELECT
,INSERT
,UPDATE
,DELETE
문을 포함하는query.sql
파일을 만듭니다. sqlc
구성:sqlc
에 SQL 파일의 위치와 Go 코드 생성 방법(예: 패키지 이름, 출력 디렉터리)을 알려주는sqlc.yaml
구성 파일을 제공합니다.go generate
와 통합:sqlc generate
를 호출하는//go:generate
지시문을 Go 파일(예:db/sqlc/main.go
)에 추가합니다.- 코드 생성: 프로젝트 루트에서
go generate ./...
명령을 실행합니다. 이 명령은sqlc generate
를 실행하고, 이는 다시 SQL 파일을 읽고, 검증한 다음, 지정된 출력 디렉터리에 생성된 Go 코드를 작성합니다. - 생성된 코드 사용: 이제 애플리케이션은 생성된 Go 코드를 가져와 사용하여 타입 안전한 방식으로 데이터베이스와 상호 작용할 수 있습니다.
SQL 스키마 또는 쿼리의 모든 변경 사항은 Go 코드의 재성성을 트리거하여 애플리케이션 코드가 항상 데이터베이스 구조와 일치하도록 보장하며, 컴파일 시간 오류는 모든 불일치를 플래그 지정합니다.
실제 구현
예제를 통해 살펴보겠습니다.
프로젝트 구조
.
├── go.mod
├── go.sum
├── main.go
└── db/
├── sqlc/
│ └── main.go // go:generate 지시문 포함
├── schema.sql
├── query.sql
└── sqlc.yaml
1. db/schema.sql
- 데이터베이스 스키마 정의
간단한 authors
테이블을 상상해 봅시다.
CREATE TABLE authors ( id INT PRIMARY KEY AUTO_INCREMENT, name TEXT NOT NULL, bio TEXT );
2. db/query.sql
- SQL 쿼리 작성
authors
테이블에 대한 몇 가지 일반적인 작업을 정의해 보겠습니다. sqlc
가 쿼리와 해당 함수 이름을 식별하기 위해 주석(-- name:
)을 사용하는 방식에 주목하세요.
-- name: GetAuthor :one SELECT id, name, bio FROM authors WHERE id = ? LIMIT 1; -- name: ListAuthors :many SELECT id, name, bio FROM authors ORDER BY name; -- name: CreateAuthor :execresult INSERT INTO authors (name, bio) VALUES (?, ?); -- name: UpdateAuthor :exec UPDATE authors SET name = ?, bio = ? WHERE id = ?; -- name: DeleteAuthor :exec DELETE FROM authors WHERE id = ?;
참고: MySQL의 경우 AUTO_INCREMENT
가 사용됩니다. PostgreSQL의 경우 id
에 대해 SERIAL
또는 GENERATED ALWAYS AS IDENTITY
가 선호될 것입니다.
참고: execresult
는 sql.Result
(예: LAST_INSERT_ID()
또는 RowsAffected
)를 반환하는 쿼리에 대한 sqlc
특정 지시문입니다. PostgreSQL의 경우 때때로 :one
과 함께 INSERT ... RETURNING id
를 사용할 수 있습니다.
3. db/sqlc/sqlc.yaml
- sqlc
구성
이 YAML 파일은 sqlc
에 스키마, 쿼리 위치 및 Go 출력 생성 방법을 알려줍니다.
version: "2" sql: - engine: "mysql" # 또는 "postgresql", "sqlite" queries: "db/query.sql" schema: "db/schema.sql" gen: go: package: "mysqlc" # 생성된 코드의 Go 패키지 이름 out: "db/sqlc" # 생성된 Go 파일의 출력 디렉터리
4. db/sqlc/main.go
- go:generate
지시문
이 파일은 일반적으로 애플리케이션에서 직접 실행되는 Go 코드를 포함하지 않습니다. 유일한 목적은 go:generate
지시문을 수용하는 것입니다.
package mysqlc //go:generate sqlc generate // 이 파일은 sqlc 코드 생성을 트리거하는 데 사용됩니다. // 실제 Go 코드는 여기에 작성하거나 실행할 필요가 없습니다.
5. 코드 생성
이제 프로젝트의 루트 디렉터리에서 다음을 실행합니다.
go generate ./db/sqlc
이 명령을 실행하면 sqlc
가 db/sqlc
디렉터리에 models.go
, query.sql.go
, db.go
, schema.sql.go
(표준이 아닌 유형을 사용하여 사용자 정의 Go 유형이 필요한 경우)와 같은 새 파일을 생성합니다.
db/sqlc/models.go
: 데이터베이스 테이블(예:Author
)을 나타내는 Go 구조체를 포함합니다.db/sqlc/query.sql.go
: SQL 쿼리에 해당하는 Go 함수(예:GetAuthor
,ListAuthors
)를 포함합니다.db/sqlc/db.go
:Querier
인터페이스와 이 인터페이스를 구현하는Queries
구조체를 정의하여 생성된 쿼리 함수를 실행할 수 있도록 합니다.
6. main.go
에서 생성된 코드 사용
이제 애플리케이션은 sqlc
에 의해 생성된 타입 안전한 함수를 사용하여 데이터베이스와 쉽게 상호 작용할 수 있습니다.
package main import ( "context" "database/sql" "fmt" "log" _ "github.com/go-sql-driver/mysql" // 데이터베이스 드라이버로 대체 "your_module_name/db/sqlc" // 생성된 패키지 가져오기 ) func main() { db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/database") if err != nil { log.Fatalf("failed to connect to database: %v", err) } defer db.Close() if err = db.Ping(); err != nil { log.Fatalf("failed to ping database: %v", err) } fmt.Println("Successfully connected to the database!") queries := mysqlc.New(db) // 생성된 Queries 객체 초기화 ctx := context.Background() // 1. 새 작성자 생성 res, err := queries.CreateAuthor(ctx, mysqlc.CreateAuthorParams{Name: "Jane Doe", Bio: sql.NullString{String: "A prolific writer", Valid: true}}) if err != nil { log.Fatalf("failed to create author: %v", err) } authorID, err := res.LastInsertId() if err != nil { log.Fatalf("failed to get last insert ID: %v", err) } fmt.Printf("Created author with ID: %d\n", authorID) // 2. ID로 작성자 가져오기 author, err := queries.GetAuthor(ctx, int32(authorID)) if err != nil { log.Fatalf("failed to get author: %v", err) } fmt.Printf("Retrieved author: %+v\n", author) // 3. 작성자 업데이트 if err = queries.UpdateAuthor(ctx, mysqlc.UpdateAuthorParams{ID: int32(authorID), Name: "Jane A. Doe", Bio: sql.NullString{String: "An updated biography", Valid: true}}); err != nil { log.Fatalf("failed to update author: %v", err) } fmt.Println("Author updated successfully.") // 4. 모든 작성자 나열 authors, err := queries.ListAuthors(ctx) if err != nil { log.Fatalf("failed to list authors: %v", err) } fmt.Println("All authors:") for _, a := range authors { fmt.Printf("- %+v\n", a) } // 5. 작성자 삭제 if err = queries.DeleteAuthor(ctx, int32(authorID)); err != nil { log.Fatalf("failed to delete author: %v", err) } fmt.Println("Author deleted successfully.") }
"your_module_name"
을 실제 Go 모듈 이름으로 바꾸는 것을 잊지 마세요. 또한 연결 문자열을 바꾸고 데이터베이스에 따라 데이터베이스 드라이버 가져오기(이 예제에서는 github.com/go-sql-driver/mysql
)를 조정하세요.
애플리케이션 시나리오
이 접근 방식은 특히 다음과 같은 경우에 유용합니다.
- 마이크로서비스: 많은 작은 서비스에서 일관되고 타입 안전한 데이터베이스 상호 작용을 보장합니다.
- 대규모 모놀리스: 복잡한 데이터베이스 스키마와 수많은 쿼리를 효율적으로 관리하여 신규 개발자의 학습 곡선을 줄입니다.
- API 백엔드: REST 또는 gRPC API에 대한 강력한 데이터 액세스 계층을 제공합니다.
- 관계형 데이터베이스가 있는 모든 Go 프로젝트: 간단한 CLI 도구부터 복잡한 웹 애플리케이션까지, 이 패턴은 데이터베이스 코드의 신뢰성과 유지 관리성을 크게 향상시킵니다.
결론
go generate
와 sqlc
의 강력함을 활용함으로써 Go 개발자는 데이터베이스 상호 작용 방법론을 크게 향상시킬 수 있습니다. 이 조합은 SQL-Go 유형 매핑 및 쿼리 유효성 검사의 부담을 런타임에서 컴파일 시간으로 이동시켜 더 높은 수준의 타입 안전성, 향상된 개발자 편의성 및 SQL 관련 오류의 상당한 감소를 보장합니다. 이를 통해 Go에서의 데이터베이스 작업은 오류가 적을 뿐만 아니라 실제로 즐겁습니다.