데이터베이스 핸들 열기

데이터베이스 핸들 열기

database/sql 패키지는 연결 관리를 대신해 줘서 데이터베이스 접근을 단순하게 만들어 줍니다. 많은 데이터 접근 API와 달리, database/sql에서는 연결을 명시적으로 열고 작업하고 닫지 않아요. 대신 코드는 **연결 풀(connection pool)**을 나타내는 데이터베이스 핸들을 열고, 그 핸들로 데이터 접근 연산을 실행합니다. 리소스(조회로 얻은 행이나 준비된 문 statement)를 해제해야 할 때만 Close 메서드를 호출하면 됩니다.

출처: Go 공식 문서

쉽게 말해 연결을 직접 관리하는 게 아니라, sql.DB로 표현되는 데이터베이스 핸들이 우리 코드를 대신해 연결을 열고 닫는 구조예요. 코드가 핸들로 데이터베이스 연산을 실행하면 그 연산들은 데이터베이스에 동시에 접근할 수 있습니다. 이 부분은 'Managing connections'에서 더 자세히 다룹니다. 참고로 데이터베이스 연결 하나를 따로 예약해서 쓸 수도 있어요. 자세한 내용은 "Using dedicated connections"를 보세요.

database/sql 패키지의 API 외에도, Go 커뮤니티는 가장 흔한(그리고 흔하지 않은) DBMS를 위한 드라이버를 여럿 개발해 두었습니다. 데이터베이스 핸들을 열 때는 대략 이런 흐름을 따릅니다.

  • 드라이버가 Go 코드와 데이터베이스 사이의 요청·응답을 번역합니다. → '드라이버 찾고 import하기' 참고
  • 드라이버를 import한 뒤 특정 데이터베이스에 대한 핸들을 엽니다. → '데이터베이스 핸들 열기' 참고
  • 핸들을 연 뒤 연결이 가능한지 확인할 수 있습니다. → '연결 확인하기' 참고

우리 코드는 보통 데이터베이스 연결을 명시적으로 열거나 닫지 않아요. 그건 데이터베이스 핸들이 하는 일이니까요. 다만 코드가 그 과정에서 얻은 리소스(예: 쿼리 결과를 담은 sql.Rows)는 해제해 주어야 합니다. → '리소스 해제하기' 참고.

드라이버 찾고 import하기

사용하는 DBMS를 지원하는 데이터베이스 드라이버가 필요합니다. 드라이버를 찾으려면 SQLDrivers를 확인하세요. 드라이버를 코드에서 사용할 수 있게 하려면 다른 Go 패키지처럼 import하면 됩니다.

import "github.com/go-sql-driver/mysql"

드라이버 패키지의 함수를 직접 호출하지 않는 경우(예: sql 패키지가 내부적으로 암묵적으로 쓸 때)에는 blank import를 써야 합니다. import 경로 앞에 밑줄을 붙이는 방식이에요.

import _ "github.com/go-sql-driver/mysql"

모범 사례로, 데이터베이스 연산에는 드라이버 자체의 API를 쓰지 않는 게 좋아요. 대신 database/sql 패키지의 함수를 쓰는 게 낫습니다. 그러면 코드가 DBMS와 느슨하게 결합되어, 필요할 때 다른 DBMS로 바꾸기 쉬워집니다.

데이터베이스 핸들 열기

sql.DB 데이터베이스 핸들은 단독으로든 트랜잭션 안에서든 데이터베이스에 읽고 쓸 수 있는 기능을 제공합니다. 핸들은 sql.Open(연결 문자열을 받음)이나 sql.OpenDB(driver.Connector를 받음)를 호출해서 얻을 수 있어요. 둘 다 *sql.DB 포인터를 반환합니다. 참고로 데이터베이스 자격 증명을 Go 소스에 넣어 두지 않도록 주의하세요. → '데이터베이스 자격 증명 저장하기' 참고.

연결 문자열로 열기

연결 문자열로 연결하고 싶다면 sql.Open 함수를 씁니다. 문자열의 형식은 쓰는 드라이버에 따라 달라져요. MySQL 예시입니다.

db, err = sql.Open("mysql", "username:password@tcp(127.0.0.1:3306)/jazzrecords")
if err != nil {
    log.Fatal(err)
}

다만 연결 속성을 더 구조적인 방식으로 담는 편이 코드를 더 읽기 쉽게 만든다는 걸 알게 될 거예요. 세부 내용은 드라이버마다 다릅니다. 예를 들어 앞 예제를, MySQL 드라이버의 Config로 속성을 지정하고 FormatDSN 메서드로 연결 문자열을 만드는 아래 코드로 대체할 수 있습니다.

// Specify connection properties.
cfg := mysql.NewConfig()
cfg.User = username
cfg.Passwd = password
cfg.Net = "tcp"
cfg.Addr = "127.0.0.1:3306"
cfg.DBName = "jazzrecords"

// Get a database handle.
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
    log.Fatal(err)
}

Connector로 열기

연결 문자열에서 쓸 수 없는 드라이버 특유의 연결 기능을 활용하고 싶다면 sql.OpenDB를 씁니다. 각 드라이버는 자기만의 연결 속성 집합을 지원하며, DBMS 특유의 연결 요청을 커스터마이즈하는 방법을 제공하는 경우가 많아요. 앞의 sql.Open 예제를 sql.OpenDB로 바꾸면 아래처럼 핸들을 만들 수 있습니다.

// Specify connection properties.
cfg := mysql.NewConfig()
cfg.User = username
cfg.Passwd = password
cfg.Net = "tcp"
cfg.Addr = "127.0.0.1:3306"
cfg.DBName = "jazzrecords"

// Get a driver-specific connector.
connector, err := mysql.NewConnector(&cfg)
if err != nil {
    log.Fatal(err)
}

// Get a database handle.
db = sql.OpenDB(connector)

오류 처리하기

sql.Open처럼 핸들 생성을 시도할 때 오류가 나는지 코드에서 확인해야 합니다. 이 오류는 연결 오류가 아니에요. sql.Open이 핸들을 초기화하지 못했을 때 나는 오류죠. 예를 들어 지정한 DSN을 파싱하지 못한 경우에 발생할 수 있습니다.

연결 확인하기

데이터베이스 핸들을 열 때 sql 패키지가 곧바로 새 데이터베이스 연결을 만들지는 않을 수 있어요. 코드가 필요로 할 때 연결을 만들 수도 있는 거죠. 곧바로 데이터베이스를 쓰지 않으면서도 연결이 잘 되는지 확인하고 싶다면 Ping이나 PingContext를 호출하면 됩니다. 아래 예제는 데이터베이스에 ping을 보내 연결을 확인합니다.

db, err = sql.Open("mysql", connString)

// Confirm a successful connection.
if err := db.Ping(); err != nil {
    log.Fatal(err)
}

데이터베이스 자격 증명 저장하기

데이터베이스 자격 증명을 Go 소스에 저장하는 건 피하세요. 그러면 데이터베이스 내용이 노출될 수 있어요. 대신 코드 밖에 있으면서도 코드에서 접근 가능한 곳에 저장하는 방법을 찾는 게 좋습니다. 예를 들어 자격 증명을 저장하고 코드가 DBMS 인증에 쓰는 자격 증명을 가져올 API를 제공하는 시크릿 키퍼(secret keeper) 앱을 고려해 볼 수 있어요.

널리 쓰이는 방법 하나는 프로그램 시작 전에 비밀값을 환경 변수에 넣어 두는 것입니다(시크릿 매니저에서 불러오는 식으로). 그러면 Go 프로그램이 os.Getenv로 그 값을 읽을 수 있어요.

username := os.Getenv("DB_USER")
password := os.Getenv("DB_PASS")

이 방식은 로컬 테스트를 위해 환경 변수를 직접 설정할 수도 있게 해 줍니다.

리소스 해제하기

database/sql 패키지에서는 연결을 명시적으로 관리하거나 닫지 않지만, 코드가 얻은 리소스는 더 이상 필요 없을 때 해제해 주어야 합니다. 여기에는 쿼리에서 반환된 데이터를 나타내는 sql.Rows가 쥔 리소스나, 준비된 문(statement)을 나타내는 sql.Stmt가 쥔 리소스가 포함됩니다. 보통은 닫는 함수를 defer로 호출해서, 바깥 함수가 끝나기 전에 리소스가 해제되도록 합니다. 아래 예제는 defer rows.Close()sql.Rows가 쥔 리소스를 해제합니다.

rows, err := db.Query("SELECT * FROM album WHERE artist = ?", artist)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

// Loop through returned rows.

더 알아보기 (Learn more)

  • 연결 풀을 조정하거나 전용 연결을 쓰는 방법은 'Managing connections'을 보세요.
  • 트랜잭션 실행에 대해선 'Executing transactions' 문서를 참고하세요.
  • database/sql 패키지 전체 API는 표준 라이브러리 문서를 확인하세요.