빠른 흐름
db, err := sql.Open("postgres", dsn)
if err != nil {
return err
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "select name from users where id = $1", id)database/sql은 단일 연결 객체가 아니라 connection pool을 다루는 표준 API로 읽어야 합니다.
기본 흐름
sql.Open은 pool 핸들을 만든다
db, err := sql.Open("postgres", dsn)
if err != nil {
return err
}*sql.DB는 하나의 연결이 아니라 재사용 가능한 connection pool입니다. 요청마다 새로 만들지 않고 애플리케이션 수명 동안 공유하는 쪽이 기본입니다.
database/sql은 공통 API이고 실제 DB 연결은 driver가 담당합니다. PostgreSQL, MySQL, SQLite 같은 driver를 driver 문서에 맞게 import해 등록한 뒤 driver 이름을 sql.Open에 넘기는 구조입니다.
연결 설정이 실제로 가능한지 확인하려면 PingContext를 같이 둡니다.
if err := db.PingContext(ctx); err != nil {
return err
}한 행은 QueryRowContext로 읽는다
var name string
err := db.QueryRowContext(ctx, "select name from users where id = $1", id).Scan(&name)
if err != nil {
return err
}한 행만 기대하면 QueryRowContext가 간단합니다. 결과가 없을 수 있으면 sql.ErrNoRows를 별도로 처리합니다.
if errors.Is(err, sql.ErrNoRows) {
return ErrNotFound
}여러 행은 QueryContext와 rows.Close를 같이 쓴다
rows, err := db.QueryContext(ctx, "select id, name from users")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Name); err != nil {
return err
}
}
if err := rows.Err(); err != nil {
return err
}여러 행을 읽을 때는 rows.Close()와 rows.Err()가 한 세트입니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| DB pool 생성 | sql.Open |
| 시작 시 연결 확인 | PingContext |
| 한 행 조회 | QueryRowContext |
| 여러 행 조회 | QueryContext |
| 쓰기/수정/삭제 | ExecContext |
| 요청 수명 제한 | context.Context |
SQL placeholder는 driver마다 다릅니다. PostgreSQL은 보통 $1, $2를 쓰고, MySQL이나 SQLite 계열은 ?를 쓰는 경우가 많습니다.
주의할 점
*sql.DB를 요청마다 열고 닫으면 pool의 장점을 잃고 연결 비용이 커집니다. 반대로 rows.Close()를 빠뜨리면 연결이 pool로 돌아가지 못할 수 있습니다. 운영 코드에서는 context timeout, pool 설정, rows 정리를 같은 기준으로 점검하는 편이 좋습니다.
참고 링크
2 sources