1. 빠른 사용법
Go 언어의 database/sql 패키지는 SQL 또는 SQL류 데이터베이스를 위한 인터페이스를 제공하지만, 구체적인 구현은 포함하지 않습니다. database/sql 패키지를 사용하려면 반드시 다른 데이터베이스 드라이버가 필요합니다. 예를 들어, MySQL용 서드파티 구현: https://github.com/go-sql-driver/mysql
1.1 다운로드
go get -u github.com/go-sql-driver/mysql
1.2 빠른 연결
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 연결 문자열: "사용자명:비밀번호@tcp(주소:포트)/데이터베이스이름"
dsn := "root:123@tcp(127.0.0.1:3306)/mydb"
db, err := sql.Open("mysql", dsn)
if err != nil {
fmt.Println("연결 오류:", err)
}
defer db.Close()
fmt.Println(db)
}
1.3 모범 사용법
sql.Open함수는 인자의 형식만 검증할 뿐, 실제 데이터베이스 연결을 생성하지 않을 수 있습니다. 데이터 소스 이름이 유효한지 확인하려면Ping메서드를 호출해야 합니다.반환된
DB객체는 여러 고루틴에서 안전하게 동시에 사용할 수 있으며, 자체적으로 유휴 연결 풀을 유지합니다. 따라서Open은 한 번만 호출하고, 이DB객체를 닫을 필요는 거의 없습니다.다음으로 전역 변수
db를 정의하여 데이터베이스 연결 객체를 저장합니다. 위 예제 코드를initDB함수로 분리하여 프로그램 시작 시 한 번만 호출함으로써 전역 변수db를 초기화하고, 다른 함수에서는 전역 변수를 직접 사용할 수 있습니다.
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
var DB *sql.DB
func initDB() (err error) {
dsn := "root:123@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4"
// 주의: DB, err := 로 선언하지 않음; err는 반환값이고 DB는 전역 변수
DB, err = sql.Open("mysql", dsn)
if err != nil {
return
}
// 실제 데이터베이스 연결 시도
err = DB.Ping()
if err != nil {
return
}
return nil
}
func main() {
err := initDB()
if err != nil {
fmt.Println("연결 획득 오류:", err)
}
fmt.Println(DB)
}
sql.DB는 데이터베이스 연결 객체를 나타내며, 연결에 필요한 모든 정보를 보관합니다. 내부적으로 0개 이상의 기본 연결을 갖는 연결 풀을 관리하며, 여러 고루틴에서 안전하게 동시에 사용할 수 있습니다.
1.4 연결 풀 설정
기본적으로 연결 풀은 제한 없이 증가하며, 사용 가능한 유휴 연결이 없으면 새 연결이 생성됩니다. DB.SetMaxOpenConns를 사용하여 풀의 최대 크기를 설정할 수 있습니다. 사용되지 않는 연결은 유휴 상태로 표시되며 필요 없으면 닫힙니다. 많은 연결을 생성하고 닫는 것을 피하려면 DB.SetMaxIdleConns로 최대 유휴 연결 수를 설정할 수 있습니다.
참고: 이 설정 메서드는 Go 버전 1.2 이상에서 사용 가능합니다.
DB.SetMaxIdleConns(n int): 연결 풀의 최대 유휴 연결 수를 설정합니다.n이 최대 열린 연결 수보다 크면 새 최대 유휴 연결 수가 최대 열린 연결 수에 맞게 줄어듭니다.n <= 0이면 유휴 연결이 유지되지 않습니다.DB.SetMaxOpenConns(n int): 데이터베이스와의 최대 연결 수를 설정합니다.n이 0보다 크고 최대 유휴 연결 수보다 작으면 최대 유휴 연결 수가 최대 열린 연결 수에 맞게 줄어듭니다.n <= 0이면 최대 연결 수가 제한되지 않으며, 기본값은 0(제한 없음)입니다.DB.SetConnMaxIdleTime(time.Second * 10): 유휴 연결의 최대 대기 시간을 설정합니다.
dsn := "root:123@tcp(127.0.0.1:3306)/mydb?charset=utf8mb4"
DB, _ = sql.Open("mysql", dsn)
DB.SetMaxOpenConns(30)
DB.SetMaxIdleConns(15)
2. 데이터 조회
type Song struct {
ID int
Title string
Year string
SignID int
}
2.1 단일 행 조회: db.QueryRow()
단일 행 조회 db.QueryRow()는 한 번의 쿼리를 실행하고 최대 한 행의 결과를 반환합니다. QueryRow는 항상 nil이 아닌 값을 반환하며, 반환값의 Scan 메서드가 호출될 때 지연된 오류가 반환됩니다.
func querySingle() {
var song Song
// QueryRow 후 반드시 Scan을 호출해야 합니다. 그렇지 않으면 보유한 데이터베이스 연결이 해제되지 않습니다.
err := DB.QueryRow("select * from music where id = ?", 2).Scan(&song.ID, &song.Title, &song.Year, &song.SignID)
if err != nil {
fmt.Println("조회 오류:", err)
}
fmt.Println(song)
}
2.2 여러 행 조회: db.Query()
여러 행 조회 db.Query()는 한 번의 쿼리를 실행하고 여러 행의 결과(즉, Rows)를 반환합니다. 일반적으로 SELECT 명령에 사용됩니다. args는 쿼리의 플레이스홀더 매개변수입니다.
func queryMultiple() {
sqlStr := "select * from music where id > ?"
rows, err := DB.Query(sqlStr, 1)
if err != nil {
fmt.Println("조회 오류:", err)
return
}
// rows를 닫아 보유한 데이터베이스 연결 해제
defer rows.Close()
// 결과 집합의 데이터를 반복해서 읽음
for rows.Next() {
var song Song
err := rows.Scan(&song.ID, &song.Title, &song.Year, &song.SignID)
if err != nil {
fmt.Println("순회 오류:", err)
return
}
fmt.Println(song)
}
}
3. 데이터 삽입
삽입, 갱신, 삭제 작업은 모두 Exec 메서드를 사용합니다.
func insertMusic() {
sqlStr := "insert into music(title, year, sign_id) values (?, ?, ?)"
result, err := DB.Exec(sqlStr, "엄마 말 들어", 2010, 1)
if err != nil {
fmt.Println("삽입 오류:", err)
return
}
newID, err := result.LastInsertId() // 새로 삽입된 데이터의 ID
if err != nil {
fmt.Println("삽입된 ID 획득 오류:", err)
return
}
fmt.Println("삽입 성공, ID:", newID)
}
4. 데이터 삭제
func deleteMusic() {
sqlStr := "delete from music where id = ?"
result, err := DB.Exec(sqlStr, 1)
if err != nil {
fmt.Println("삭제 오류:", err)
return
}
n, err := result.RowsAffected() // 작업에 영향을 받은 행 수
if err != nil {
fmt.Println("영향받은 행 수 획득 오류:", err)
return
}
fmt.Println("삭제 성공, 영향받은 행 수:", n)
}
5. 데이터 갱신
func updateMusic() {
sqlStr := "update music set title = ? where id = ?"
result, err := DB.Exec(sqlStr, "남권마마", 4)
if err != nil {
fmt.Println("갱신 실패:", err)
return
}
n, err := result.RowsAffected()
if err != nil {
fmt.Println("영향받은 행 수 획득 실패:", err)
return
}
fmt.Println("갱신 성공, 영향받은 행 수:", n)
}
6. MySQL 프리페어 스테이트먼트
6.1 프리페어 스테이트먼트란?
일반 SQL 문 실행 과정:
- 클라이언트가 SQL 문의 플레이스홀더를 실제 값으로 대체하여 완전한 SQL 문을 구성합니다.
- 클라이언트가 완전한 SQL 문을 MySQL 서버에 보냅니다.
- MySQL 서버가 완전한 SQL 문을 실행하고 결과를 클라이언트에 반환합니다.
프리페어 스테이트먼트 실행 과정:
- SQL 문을 명령 부분과 데이터 부분으로 나눕니다.
- 명령 부분을 먼저 MySQL 서버에 보내면, MySQL 서버가 SQL 프리페어링을 수행합니다.
- 그런 다음 데이터 부분을 MySQL 서버에 보내면, MySQL 서버가 SQL 문의 플레이스홀더를 실제 값으로 대체합니다.
- MySQL 서버가 완전한 SQL 문을 실행하고 결과를 클라이언트에 반환합니다.
6.2 프리페어 스테이트먼트를 사용하는 이유
- MySQL 서버가 동일한 SQL을 반복 실행할 때 성능을 최적화할 수 있습니다. 미리 컴파일하여 한 번 컴파일하고 여러 번 실행함으로써 이후 컴파일 비용을 절약합니다.
- SQL 인젝션 문제를 방지합니다.
6.3 Go에서 MySQL 프리페어 스테이트먼트 구현
database/sql에서는 다음 Prepare 메서드를 사용하여 프리페어 스테이트먼트를 구현합니다.
func (db *DB) Prepare(query string) (*Stmt, error)
Prepare 메서드는 먼저 SQL 문을 MySQL 서버에 보내고, 이후의 쿼리나 명령을 위해 준비된 상태를 반환합니다. 반환값을 사용하여 여러 개의 쿼리와 명령을 동시에 실행할 수 있습니다.
조회 작업의 프리페어 스테이트먼트 예제:
// 프리페어 스테이트먼트 조회 예제
func prepareQueryExample() {
sqlStr := "select id, name, age from user where id > ?"
stmt, err := db.Prepare(sqlStr)
if err != nil {
fmt.Printf("prepare 실패, err:%v\n", err)
return
}
defer stmt.Close()
rows, err := stmt.Query(0)
if err != nil {
fmt.Printf("query 실패, err:%v\n", err)
return
}
defer rows.Close()
for rows.Next() {
var u user
err := rows.Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Printf("scan 실패, err:%v\n", err)
return
}
fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
}
}
삽입, 갱신, 삭제 작업의 프리페어 스테이트먼트는 매우 유사합니다. 여기서는 삽입 작업을 예로 들어 보겠습니다:
// 프리페어 스테이트먼트 삽입 예제
func prepareInsertExample() {
sqlStr := "insert into user(name, age) values (?, ?)"
stmt, err := db.Prepare(sqlStr)
if err != nil {
fmt.Printf("prepare 실패, err:%v\n", err)
return
}
defer stmt.Close()
_, err = stmt.Exec("작은 왕자", 18)
if err != nil {
fmt.Printf("insert 실패, err:%v\n", err)
return
}
_, err = stmt.Exec("사허 나자", 18)
if err != nil {
fmt.Printf("insert 실패, err:%v\n", err)
return
}
fmt.Println("insert 성공")
}
6.4 SQL 인젝션 문제
절대 SQL 문을 직접 조합해서는 안 됩니다!
다음은 직접 SQL을 조합하는 예제로, name 필드를 기준으로 user 테이블을 조회하는 함수입니다:
// SQL 인젝션 예제
func sqlInjectionExample(name string) {
sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)
fmt.Printf("SQL: %s\n", sqlStr)
var u user
err := db.QueryRow(sqlStr).Scan(&u.id, &u.name, &u.age)
if err != nil {
fmt.Printf("exec 실패, err:%v\n", err)
return
}
fmt.Printf("user:%#v\n", u)
}
다음과 같은 입력 문자열은 SQL 인젝션을 유발할 수 있습니다:
sqlInjectionExample("xxx' or 1=1#")
sqlInjectionExample("xxx' union select * from user #")
sqlInjectionExample("xxx' and (select count(*) from user) <10 #")
7. Go에서 MySQL 트랜잭션 구현
7.1 트랜잭션의 ACID
트랜잭션은 일반적으로 4가지 조건(ACID)을 충족해야 합니다: 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability).
| 조건 | 설명 |
|---|---|
| 원자성 | 트랜잭션 내의 모든 작업은 전부 완료되거나 전부 완료되지 않아야 하며, 중간 단계에서 종료되지 않습니다. 트랜잭션 실행 중 오류가 발생하면 트랜잭션 시작 전 상태로 롤백됩니다. |
| 일관성 | 트랜잭션 시작 전과 종료 후 데이터베이스의 무결성이 유지됩니다. 즉, 쓰여진 데이터가 모든 사전 정의된 규칙을 완전히 준수해야 합니다. |
| 격리성 | 데이터베이스는 여러 동시 트랜잭션이 데이터를 읽고 쓸 수 있도록 허용하며, 격리성은 여러 트랜잭션이 동시에 실행될 때 교차 실행으로 인한 데이터 불일치를 방지합니다. 트랜잭션 격리에는 읽기 미커밋(Read uncommitted), 읽기 커밋(Read committed), 반복 읽기(Repeatable read), 직렬화(Serializable) 등 여러 수준이 있습니다. |
| 지속성 | 트랜잭션 처리가 완료된 후 데이터 변경은 영구적이며, 시스템 장애가 발생해도 손실되지 않습니다. |
7.2 트랜잭션 관련 메서드
db.Begin() // 트랜잭션 시작
db.Commit() // 트랜잭션 커밋
db.Rollback() // 트랜잭션 롤백
7.3 예제
func transactionExample() {
conn, err := DB.Begin()
if err != nil {
fmt.Println("트랜잭션 시작 오류:", err)
return
}
_, err = conn.Exec("insert into music(title, year, sign_id) values (?, ?, ?)", "동물 세계", "2022", 2)
if err != nil {
fmt.Println("삽입 오류:", err)
conn.Rollback()
return
}
_, err = conn.Exec("insert into music(title, year, sign_id) values (?, ?, ?)", "세상에 엄마만 좋아", "2022", 3)
if err != nil {
fmt.Println("삽입 오류:", err)
conn.Rollback()
return
}
conn.Commit()
}