---
id: 134c150de56c188c925476664026a1ff
author: Lemon Mint
title: 'gosuda.org/sqlite3: 공유 메모리 IPC 기반 CGO_ENABLED=0 database/sql 드라이버'
description: CGO_ENABLED=0 환경을 지원하고, HQQ 공유 메모리 워커 격리, 64 KiB 배치 행 선행 로딩(prefetching), SLSA 인증 바이너리를 제공하는 Go용 드롭인(drop-in) database/sql SQLite 3.54.1 드라이버.
language: ko
date: 2026-10-18T14:02:16.803040007Z
path: /sqlite3
go_package: gosuda.org/sqlite3
go_repourl: https://github.com/gosuda/hqq-sqlite3.git
category: database
---

`gosuda.org/sqlite3`는 `CGO_ENABLED=0` 환경에서 사용할 수 있는 Go용 `database/sql` 드라이버로, [HQQ](https://gosuda.org/hqq) 공유 메모리 링을 통해 격리된 워커 프로세스 안에서 upstream C SQLite `3.54.1` amalgamation을 실행합니다. 표준 `"sqlite3"` 드라이버 이름으로 등록되며, `github.com/mattn/go-sqlite3`의 DSN 쿼리 매개변수를 지원하고, 6개의 OS 및 아키텍처 대상에 대해 SLSA 인증을 받은 사전 빌드 서버 바이너리를 내장합니다. 또한 요청 파이프라이닝과 64 KiB 배치 프리페칭을 사용하여 다중 행 쿼리에서 인프로세스 `cgo`보다 높은 성능을 보입니다.

```bash
go get gosuda.org/sqlite3@v0.3.3540100
```

## 또 하나의 Go SQLite 드라이버가 필요한 이유

SQLite는 세계에서 가장 널리 배포된 소프트웨어 중 하나이지만, Go에서 이를 사용할 때 엔지니어들은 역사적으로 빌드 단순성과 엔진 충실도 사이에서 불편한 선택을 강요받아 왔습니다.

한편에는 오랜 커뮤니티 표준인 `github.com/mattn/go-sqlite3`가 있습니다. 이 드라이버는 `cgo`를 통해 공식 C SQLite amalgamation을 직접 감싸고 있습니다. 실제 C 쿼리 플래너, 완전한 WAL 동시성 의미론, 예측 가능한 C 메모리 관리를 얻을 수 있습니다. 그 대가로 `CGO_ENABLED=1`이 필요하며, 로컬 빌드와 CI 파이프라인이 느려지고, 최소 구성의 scratch 또는 distroless 컨테이너 이미지에는 대응하는 C 런타임 라이브러리가 필요하며, 다른 OS나 CPU 아키텍처로의 크로스 컴파일에는 완전한 C 크로스 툴체인이 필요하고, C 코드의 메모리 손상이 Go 프로세스 주소 공간을 공유하게 됩니다.

다른 한편에는 C amalgamation을 에뮬레이션된 C 런타임(`libc`) 위에서 Go 소스 코드로 기계적으로 번역하여 생성한 순수 Go SQLite 포트들이 있습니다. 이들은 `CGO_ENABLED=0`을 복원하여 빌드를 간단하게 만듭니다. 그러나 지속적인 워크로드에서는 번역된 C 힙과 행별 Go 할당이 가비지 컬렉션 부담을 증가시키며, 잠금, busy 타임아웃, 또는 문장 중단과 관련된 미묘한 경우들이 네이티브 C 빌드와 다르게 동작할 수 있습니다.

우리는 Go 애플리케이션이 `CGO_ENABLED=0`과 실제 C SQLite 엔진 사이에서 선택할 필요가 없도록 `gosuda.org/sqlite3`를 만들었습니다. 공식 SQLite `3.54.1` C amalgamation을 전용 공유 메모리 서버(`hqq-sqlite-server`)로 컴파일하고 `gosuda.org/hqq`를 통해 통신함으로써, 여러분의 Go 바이너리는 `CGO_ENABLED=0`으로 컴파일되면서도 프로세스 경계 너머에서 수정되지 않은 `-O3` 최적화 C SQLite를 실행합니다.

## 설정 없는 실행: `go get`에서 `sql.Open`까지

기존 Go 코드베이스에서 `github.com/mattn/go-sqlite3`를 교체하려면 blank import 경로만 변경하면 됩니다. 이 드라이버는 `"sqlite3"`(명시적 선택을 위한 `"hqq-sqlite"`도 포함)로 등록되며 동일한 DSN 쿼리 문자열을 허용합니다.

```go
package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	_ "gosuda.org/sqlite3"
)

func main() {
	dsn := "file:app.db?_journal_mode=WAL&_busy_timeout=5000&_txlock=immediate&_foreign_keys=on"
	db, err := sql.Open("sqlite3", dsn)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	ctx := context.Background()
	if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`); err != nil {
		log.Fatal(err)
	}
	if _, err := db.ExecContext(ctx, `INSERT INTO users (name) VALUES (?)`, "Ada Lovelace"); err != nil {
		log.Fatal(err)
	}

	var id int64
	var name string
	if err := db.QueryRowContext(ctx, `SELECT id, name FROM users WHERE name = ?`, "Ada Lovelace").Scan(&id, &name); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("user #%d: %s\n", id, name)
}
```

### 내장된 SLSA 인증 워커 바이너리

라이브러리가 C 워커 프로세스를 사용한다는 말을 들었을 때 자연스럽게 떠오르는 질문은 _"Go 프로그램과 함께 별도의 C 바이너리를 설치하거나 배포해야 합니까?"_ 입니다.

아닙니다. `gosuda.org/sqlite3`는 6개의 주요 대상 모두에 대한 사전 빌드된 `hqq-sqlite-server` 바이너리를 플랫폼 태그가 지정된 Go 파일(`internal/embedded/embed_<goos>_<goarch>.go`) 내부에 직접 내장합니다. Go의 빌드 제약 조건은 대상 `GOOS` 및 `GOARCH`에 해당하는 파일만 컴파일하므로, `GOOS=linux GOARCH=arm64 go build`로 크로스 컴파일하면 약 1.4 MiB 크기의 `linux/arm64` 서버 바이너리만 자동으로 내장됩니다.

| 대상 OS     | 대상 아키텍처 (`GOARCH`) | 내장 바이너리                         | 빌드 툴체인 및 출처 증명                          |
| :---------- | :----------------------- | :------------------------------------ | :------------------------------------------------ |
| **Linux**   | `amd64`, `arm64`         | `hqq-sqlite-server_linux_<arch>`      | GCC `-O3`, SLSA Build L3 출처 증명                |
| **macOS**   | `arm64`, `amd64`         | `hqq-sqlite-server_darwin_<arch>`     | AppleClang `-O3`, SLSA Build L3 출처 증명         |
| **Windows** | `amd64`                  | `hqq-sqlite-server_windows_amd64.exe` | MSVC `-O2`, SLSA Build L3 출처 증명               |
| **Windows** | `arm64`                  | `hqq-sqlite-server_windows_arm64.exe` | `zig cc -target aarch64-windows-gnu -O3`, SLSA L3 |

`sql.Open`이 첫 번째 연결을 설정할 때, `autolaunch.go`는 내장된 바이너리를 사용자 캐시 디렉터리(`$XDG_CACHE_HOME/hqq/bin/<sha256>/`)에 구체화하고, `gosuda.org/hqq/secureexec`를 통해 `hashes.go`의 고정된 `ReleaseHashes` 맵에 대해 암호학적 SHA-256 다이제스트를 검증한 후, `--parent-stdin` 옵션으로 워커를 기동하여 워커의 수명이 Go 프로세스에 엄격하게 묶이도록 합니다.

### `mattn/go-sqlite3` DSN 클린룸 호환성

진정한 드롭인 대체재로 기능하기 위해, DSN 파서(`dsn.go`)는 `mattn/go-sqlite3` 코드베이스 전반에서 사용되는 연결 및 pragma 설정 항목을 클린룸 방식으로 재구현합니다.

| DSN 쿼리 매개변수                                                                             | 허용 값                                                 | SQLite 효과                                                                                          |
| :-------------------------------------------------------------------------------------------- | :------------------------------------------------------ | :--------------------------------------------------------------------------------------------------- |
| `_busy_timeout` / `_timeout`                                                                  | 정수 밀리초 (예: `5000`)                                | 세션을 열 때 `sqlite3_busy_timeout(db, ms)`를 호출합니다.                                            |
| `_journal_mode` / `_journal`                                                                  | `DELETE`, `TRUNCATE`, `PERSIST`, `MEMORY`, `WAL`, `OFF` | `PRAGMA journal_mode = <mode>`를 실행합니다.                                                         |
| `_txlock`                                                                                     | `immediate`, `exclusive`, `deferred`                    | `db.BeginTx`에 대해 `BEGIN IMMEDIATE`, `BEGIN EXCLUSIVE`, 또는 `BEGIN DEFERRED`를 선택합니다.        |
| `_foreign_keys` / `_fk`                                                                       | `on`, `true`, `1`, `off`, `false`, `0`                  | `PRAGMA foreign_keys = ON/OFF`를 실행합니다.                                                         |
| `_synchronous` / `_sync`                                                                      | `0`/`OFF`, `1`/`NORMAL`, `2`/`FULL`, `3`/`EXTRA`        | `PRAGMA synchronous = <level>`을 실행합니다.                                                         |
| `_cache_size`                                                                                 | 정수 페이지 수 또는 `-KiB` (예: `-20000`)               | `PRAGMA cache_size = <val>`을 실행합니다.                                                            |
| `_loc`                                                                                        | `auto` 또는 IANA 시간대 (예: `UTC`, `Asia/Seoul`)       | `DATETIME`/`TIMESTAMP` 텍스트 열을 대상 위치의 `*time.Time`으로 파싱합니다.                          |
| `mode`                                                                                        | `ro`, `rw`, `rwc`, `memory`                             | `SQLITE_OPEN_READONLY`, `SQLITE_OPEN_READWRITE`, `SQLITE_OPEN_CREATE`, 또는 `SQLITE_OPEN_MEMORY`를 설정합니다. |
| `_case_sensitive_like`, `_query_only`, `_recursive_triggers`, `_secure_delete`, `_AutoVacuum` | 불리언 또는 모드 정수                                   | 연결 초기화 시 해당 세션 `PRAGMA`를 적용합니다.                                                      |

## 프로세스 격리를 처리량의 이점으로 전환하기

언뜻 보면 SQLite를 별도의 프로세스에서 실행하는 것은 `cgo`를 통해 프로세스 내에서 `sqlite3_step`을 호출하는 것보다 느릴 것처럼 보입니다. 실제로는 쿼리가 단일 행 이상을 반환하는 순간 그 반대의 현상이 일어납니다.

### 행 단위 `cgo` 호출의 숨은 비용

기존 `cgo` SQLite 드라이버에서 `database/sql`은 한 번에 한 행, 한 번에 한 열씩 순회를 진행합니다. 4개의 열을 가진 1,000개의 행을 읽으면 다음이 발생합니다.

- `sqlite3_step`에 대한 1,000번의 `cgo` 호출
- `sqlite3_column_type` 및 `sqlite3_column_int64` / `sqlite3_column_text`에 대한 4,000번의 `cgo` 호출
- Go 스케줄러와 C 스택 사이의 수많은 개별 경계 횡단

### 요청 파이프라이닝과 64 KiB 배치 프리페칭

`gosuda.org/sqlite3`는 `stmt.go`, `rows.go`, `sqlite3/server/sqlite3_hooks.c`의 두 가지 메커니즘을 중심으로 와이어 프로토콜을 재설계하여 행별 경계 횡단을 제거합니다.

```text
Go 클라이언트 (stmt.go / rows.go)                   C11 워커 (sqlite3_hooks.c)
-----------------------------                       ----------------------------
1. 1회 플라이트로 파이프라인 구성:
   ResetAsync + BindValuesAsync
   + StmtInfoAsync + FetchRowsAsync(128)  =======>  reset -> bind -> stmt_info ->
                                                    최대 128행까지 sqlite3_step() 루프
                                          <=======  배치 1 (행 1..128을 64 KiB 슬롯에 패킹)
2. 즉시 프리페치 발행:
   FetchRowsAsync(128) (배치 2)           =======>  행 129..256에 대해 sqlite3_step() 루프
   C가 배치 2를 스텝하는 동안, Go 호출자는
   배치 1의 행 1..128을 스캔합니다!        <=======  배치 2가 공유 메모리에 준비됨
```

1. **단일 플라이트 쿼리 파이프라이닝 (`stmt.go`)**: `QueryContext` 또는 `Stmt.QueryContext`가 읽기 전용 문장을 실행할 때, `s.query()`는 `reset`, `bind_values`, `stmt_info`(첫 실행 시), `fetch_rows`를 단일 공유 메모리 플라이트로 비동기 제출합니다. 네 가지 작업이 모두 C 워커에서 연달아 실행되며 한 번의 왕복으로 반환됩니다.
2. **촘촘한 C 배치 루프와 비동기 프리페치 (`rows.go`)**: `fetch_rows` (`sqlite3_hooks.c`) 내부에서 C 워커는 네이티브 C 루프에서 `sqlite3_step(stmt)`을 실행하며, 배치 한도, 슬롯 가득 참, 또는 `SQLITE_DONE`에 도달할 때까지 최대 `FetchSize`(기본값 128)개의 행을 64 KiB 공유 메모리 응답 슬롯에 직접 인코딩합니다. `rows.ingest()`가 배치 1을 디코딩하자마자, 호출자에게 첫 번째 행을 반환하기 전에 배치 2에 대한 `FetchRowsAsync`를 발행합니다. Go 코드가 배치 1을 스캔하는 동안 C 워커는 다른 CPU 코어에서 이미 배치 2를 스텝하고 있습니다.

Apple M4(`darwin/arm64`)에서 동일한 SQLite 데이터베이스를 대상으로 `gosuda.org/sqlite3`(`CGO_ENABLED=0`)와 `github.com/mattn/go-sqlite3`(`CGO_ENABLED=1`)를 비교한 결과, 배치 처리와 파이프라이닝의 효과가 다음과 같이 나타났습니다.

| 워크로드                            | `gosuda.org/sqlite3` (`CGO_ENABLED=0`) | `mattn/go-sqlite3` (`CGO_ENABLED=1`) | 상대 속도                            |
| :---------------------------------- | -------------------------------------: | -----------------------------------: | :----------------------------------- |
| **포인트 셀렉트 (PK로 `QueryRow`)** |            `3.52 µs/op` (9 B, 1 alloc) |      `3.56 µs/op` (232 B, 11 allocs) | **1.01배 (동등, 할당 11배 감소)**    |
| **100행 스캔 (`rows.Next`)**        |    `20.41 µs/op` (30.5 KB, 211 allocs) |  `36.34 µs/op` (21.8 KB, 309 allocs) | **1.78배 빠름**                      |
| **1,000행 스캔 (`rows.Next`)**      |  `125.92 µs/op` (296 KB, 2,030 allocs) | `349.10 µs/op` (216 KB, 3,009 allocs) | **2.77배 빠름**                      |
| **Prepared Insert (자동 커밋)**     |           `5.33 µs/op` (0 B, 0 allocs) |       `5.88 µs/op` (120 B, 5 allocs) | **1.10배 빠름 (0 allocs)**           |
| **100행 트랜잭션 배치**             |         `189.99 µs/op` (16 B, 1 alloc) | `191.50 µs/op` (12.1 KB, 503 allocs) | **1.01배 (할당 500배 감소)**         |

## 취소, 경로 샌드박싱, 연결 풀 복구

SQLite를 격리된 워커에서 실행하면 드라이버가 타임아웃, 신뢰할 수 없는 SQL, 파일시스템 경계를 처리하는 방식도 강화됩니다.

- **결정론적 컨텍스트 취소 (`conn.go` 및 `errors.go`)**: 모든 드라이버 호출은 경량 `context.AfterFunc` 가드(`cn.guard`)를 설정합니다. 서버에서 느린 재귀 쿼리나 잠금 대기가 실행되는 동안 호출자의 `context.Context`가 취소되거나 기한이 만료되면, 가드는 연결을 불량 상태로 표시하고, 로컬 세션 링크를 종료하며, 대역 외 `Cancel()` 프레임을 전송합니다. C 워커는 즉시 `sqlite3_interrupt(db)`를 호출하여 `sqlite3_step`의 차단을 해제하고, Go 드라이버는 결과로 발생한 `SQLITE_INTERRUPT` 상태를 `context.Canceled` 또는 `context.DeadlineExceeded`로 매핑하며 `database/sql`에 풀의 연결을 교체하도록 지시합니다.
- **디렉터리 샌드박싱 (`sqlsrv_pathrules.c`)**: C 워커는 모든 `open_v2` 호출에 대해 `--allow-dir` 경로 규칙을 적용하며, 심볼릭 링크를 해석하고 허용된 디렉터리 밖으로의 상대 경로 `..` 순회를 거부합니다. `sqlite3_load_extension`과 `SQLITE_LIMIT_ATTACHED`는 기본적으로 비활성화되어 있어, SQL 인젝션이 데이터베이스 디렉터리를 벗어나거나 임의의 공유 객체를 로드할 수 없습니다.

## 저장소 구조

저장소는 최상위 패키지가 드롭인 `database/sql` 드라이버를 제공하고, `launcher/` 및 `sqlite3/`는 고급 사용자 정의 서버 배포를 위해 접근 가능하도록 구성되어 있습니다.

```text
gosuda.org/sqlite3
├── driver.go / api.go         # database/sql 드라이버 등록("sqlite3") 및 Connector API
├── dsn.go                     # 클린룸 방식 mattn/go-sqlite3 DSN 파서 및 pragma 설정
├── conn.go / tx.go            # 세션 수명 주기, 컨텍스트 가드, 트랜잭션 상태 머신
├── stmt.go / rows.go          # 단일 플라이트 요청 파이프라이닝 및 64 KiB 배치 프리페칭
├── errors.go                  # 타입이 지정된 SQLite 기본/확장 오류 코드 및 센티넬 매칭
├── autolaunch.go / hashes.go  # 설정 없는 내장 서버 구체화 및 SHA-256 고정
├── internal/embedded/         # 플랫폼 태그가 지정된 //go:embed 바이너리 (linux, darwin, windows)
├── launcher/                  # 사용자 정의 서버 배포를 위한 구성 가능한 프로세스 감독기
└── sqlite3/                   # 생성된 hqq-bindgen Go 클라이언트 및 C11 SQLite 서버 소스
    └── server/                # C11 서버 구현 (sqlite3_hooks.c, sqlsrv_policy.c)
```

## 시작하기

`go get`으로 모듈을 설치하고 `sql.Open("sqlite3", dsn)`으로 데이터베이스를 여십시오.

```bash
go get gosuda.org/sqlite3@v0.3.3540100
```

- **GitHub 저장소**: [https://github.com/gosuda/hqq-sqlite3](https://github.com/gosuda/hqq-sqlite3)
- **DSN 및 Pragma 참조**: [docs/DSN.md](https://github.com/gosuda/hqq-sqlite3/blob/main/docs/DSN.md)
- **아키텍처 및 와이어 프로토콜**: [docs/ARCHITECTURE.md](https://github.com/gosuda/hqq-sqlite3/blob/main/docs/ARCHITECTURE.md)
