Go(고) 준비된 구문(Prepared Statement) 완벽 정복: 안전하고 빠른 데이터베이스 프로그래밍

Go(고) 준비된 구문(Prepared Statement) 완벽 정복: 안전하고 빠른 데이터베이스 프로그래밍

안녕하세요!

오늘은 Go(고) 언어로 데이터베이스(database) 프로그래밍을 할 때 정말 중요한 개념인 '준비된 구문(Prepared Statement)'에 대해 깊이 알아보려고 합니다.

Go(고)의 database/sql 패키지는 이 준비된 구문(Prepared Statement)을 아주 잘 지원하는데요, 이걸 사용하면 동일한 SQL(Structured Query Language) 문을 여러 번 실행할 때 효율적이면서도 안전하게 처리할 수 있답니다.

오늘 알아볼 핵심 내용!

  • 준비된 구문(Prepared Statement)은 SQL 삽입(SQL Injection) 공격을 막아 보안을 강화해 줍니다.
  • 데이터베이스(database)가 반복되는 쿼리(query) 실행 계획을 최적화할 수 있게 도와 성능 향상에도 기여합니다.
  • 사용한 구문(statement)은 반드시 닫아주는 등 적절한 자원 관리가 필수적입니다. 오류 처리도 꼼꼼하게 해야 하고요.

자, 그럼 준비된 구문(Prepared Statement)이 정확히 무엇인지부터 차근차근 알아볼까요?

1. 준비된 구문(Prepared Statement)이란 무엇일까요?

준비된 구문(Prepared Statement)은 데이터베이스(database) 시스템에 미리 보내져 컴파일(compile)된 SQL(Structured Query Language) 문이라고 생각하면 이해하기 쉬운데요.

한 번 만들어두면, 나중에 파라미터(parameter) 값만 바꿔가면서 여러 번 실행할 수 있습니다.

마치 SQL(Structured Query Language) 문의 '틀'을 미리 만들어 놓고, 실행할 때마다 필요한 '내용물'만 바꿔 끼우는 것과 비슷하다고 할 수 있는데요.

이렇게 SQL(Structured Query Language) 로직과 실제 데이터를 분리하기 때문에 두 가지 큰 장점이 생깁니다.

첫째, 악의적인 사용자가 입력 값에 SQL(Structured Query Language) 코드를 몰래 심어 공격하는 SQL 삽입(SQL Injection) 공격을 효과적으로 방어할 수 있어 보안성이 높아집니다.

둘째, 데이터베이스(database)는 이미 컴파일된 SQL(Structured Query Language) 문을 가지고 있으므로, 반복 실행 시 매번 SQL(Structured Query Language) 문을 분석하고 최적화하는 과정을 생략할 수 있어 성능 면에서도 이득을 볼 수 있습니다.

2. Go(고)에서 준비된 구문(Prepared Statement) 사용하기

그럼 이제 실제 Go(고) 코드에서 준비된 구문(Prepared Statement)을 어떻게 사용하는지 단계별로 살펴보겠습니다.

필수 패키지 가져오기 (Import)

먼저 필요한 패키지들을 임포트(import)해야 합니다.

데이터베이스(database) 작업을 위한 database/sql 패키지와, 사용하는 데이터베이스(database) 종류에 맞는 드라이버(driver) 패키지가 필요합니다.

여기서는 MySQL(마이에스큐엘) 드라이버(driver)를 예시로 사용하는데요.

import (
    "database/sql"
    "fmt"
    _ "github.com/go-sql-driver/mysql" // MySQL 드라이버, _ 는 초기화만 위함
)

 

데이터베이스 연결 설정하기 (Establish Connection)

다음으로 sql.Open 함수를 사용하여 데이터베이스(database) 연결을 설정합니다.

중요한 점은 sql.Open 함수가 호출되는 즉시 데이터베이스(database)에 연결을 맺는 것은 아니라는 건데요.

실제 연결은 나중에 필요할 때 이루어지며, sql.Open은 단지 앞으로 사용할 데이터베이스(database) 핸들(handle)을 준비하는 역할만 합니다.

// "사용자이름:비밀번호@tcp(호스트주소:포트)/데이터베이스이름" 형식으로 연결 정보 입력
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")
if err != nil {
    // 오류 발생 시 처리 (실제로는 로깅 등 더 적절한 처리 필요)
    panic(err)
}
// 함수 종료 직전에 데이터베이스 연결을 닫도록 defer 사용
defer db.Close()

 

구문 준비하기 (Prepare the Statement)

이제 db.Prepare 메서드를 호출하여 준비된 구문(Prepared Statement)을 만듭니다.

이 메서드는 sql.Stmt 타입의 객체를 반환하는데요.

이 객체를 통해 나중에 파라미터(parameter) 값만 바꿔가면서 SQL(Structured Query Language) 문을 여러 번 실행할 수 있습니다.

// 실행할 SQL 문 준비. ?는 나중에 채워질 파라미터 자리 표시자(placeholder)
stmt, err := db.Prepare("INSERT INTO users(name, age) VALUES(?, ?)")
if err != nil {
    // 오류 처리
    panic(err)
}
// Prepare가 성공하면 반드시 stmt도 닫아줘야 함. defer 사용 권장!
defer stmt.Close()

 

위 예제에서 ?는 실행 시점에 제공될 파라미터(parameter) 값을 위한 자리 표시자(placeholder)입니다.

이 자리 표시자(placeholder)의 구체적인 문법은 사용하는 데이터베이스(database) 드라이버(driver)에 따라 다를 수 있는데요.

예를 들어, PostgreSQL(포스트그레스큐엘)에서는 $1, $2와 같은 형식을 사용하기도 합니다.

공식 문서(go.dev) 등에서 사용하는 드라이버(driver)의 문서를 확인하는 것이 좋습니다.

준비된 구문 실행하기 (Execute the Prepared Statement)

준비된 stmt 객체의 Exec 메서드를 사용하면, 파라미터(parameter) 값을 전달하여 실제 SQL(Structured Query Language) 문을 실행할 수 있습니다.

INSERT, UPDATE, DELETE와 같이 결과 행(row)을 반환하지 않는 SQL(Structured Query Language) 문을 실행할 때 주로 사용됩니다.

// 첫 번째 실행: "Alice", 30 값을 파라미터로 전달
_, err = stmt.Exec("Alice", 30)
if err != nil {
    // 오류 처리
    panic(err)
}

// 두 번째 실행: "Bob", 25 값을 파라미터로 전달
_, err = stmt.Exec("Bob", 25)
if err != nil {
    // 오류 처리
    panic(err)
}

 

이렇게 Exec 메서드를 호출할 때마다 준비된 SQL(Structured Query Language) 문이 전달된 파라미터(parameter) 값과 함께 데이터베이스(database)에서 실행됩니다.

Exec 메서드는 결과 요약 정보(예: 영향받은 행 수)와 오류를 반환하는데요, 여기서는 결과 요약 정보는 사용하지 않아 _로 무시했습니다.

준비된 구문으로 데이터 조회하기 (Querying Data)

준비된 구문(Prepared Statement)은 데이터 삽입뿐만 아니라 데이터 조회(SELECT)에도 당연히 사용할 수 있습니다.

데이터를 조회하는 예제를 살펴볼까요?

// 나이가 특정 값보다 큰 사용자를 조회하는 SQL 문 준비
queryStmt, err := db.Prepare("SELECT id, name, age FROM users WHERE age > ?")
if err != nil {
    // 오류 처리
    panic(err)
}
// 역시 defer를 이용해 닫아주기
defer queryStmt.Close()

// 파라미터로 20을 전달하여 쿼리 실행
rows, err := queryStmt.Query(20)
if err != nil {
    // 오류 처리
    panic(err)
}
// 쿼리 결과(rows)도 사용 후 반드시 닫아야 함
defer rows.Close()

// 결과 행들을 반복하며 처리
for rows.Next() {
    var id int
    var name string
    var age int
    // 현재 행의 컬럼 값들을 변수에 스캔 (읽어오기)
    err = rows.Scan(&id, &name, &age)
    if err != nil {
        // 스캔 오류 처리
        panic(err)
    }
    fmt.Printf("ID: %d, 이름: %s, 나이: %d\n", id, name, age)
}

// 반복문이 끝난 후, 반복 과정에서 발생했을 수 있는 오류 확인 (중요!)
if err = rows.Err(); err != nil {
    // rows.Next() 반복 중 발생한 오류 처리
    panic(err)
}

 

이 예제에서는 db.PrepareSELECT 문을 준비하고, queryStmt.Query 메서드에 나이 조건(20)을 파라미터(parameter)로 전달하여 실행합니다.

Query 메서드는 결과 행들을 담고 있는 sql.Rows 타입의 객체를 반환하는데요.

rows.Next() 메서드를 사용하여 각 행을 순회하고, rows.Scan() 메서드로 각 행의 컬럼(column) 값들을 준비된 변수들(id, name, age)에 읽어 들입니다.

반복문이 정상적으로 끝난 후에도 반드시 rows.Err()를 호출하여 반복 과정 중에 발생했을 수 있는 오류를 확인하는 것이 중요합니다.

그리고 쿼리(query) 결과를 담은 rows 객체 역시 defer rows.Close()를 사용해 반드시 닫아주어야 합니다.

3. 준비된 구문(Prepared Statement) 사용 시 권장 사항 (Best Practices)

Go(고)에서 준비된 구문(Prepared Statement)을 효과적으로 사용하기 위해 몇 가지 기억해두면 좋은 점들이 있습니다.

  • 구문 닫기: db.Prepare로 생성한 sql.Stmt 객체는 사용이 끝나면 반드시 stmt.Close() 메서드를 호출하여 닫아주어야 합니다. 이는 데이터베이스(database) 자원을 해제하여 불필요한 자원 소모를 막기 위함인데요. db.Prepare 직후에 defer stmt.Close()를 사용하는 습관을 들이면 실수를 방지하는 데 큰 도움이 됩니다. 마찬가지로 Query 메서드로 얻은 sql.Rows 객체도 defer rows.Close()로 닫아주는 것이 좋습니다.
  • 오류 처리: 데이터베이스(database) 작업에서는 언제든 예기치 않은 오류가 발생할 수 있습니다. sql.Open, db.Prepare, stmt.Exec, stmt.Query, rows.Scan, rows.Err 등 오류를 반환할 수 있는 모든 메서드 호출 후에는 반드시 반환된 오류 값을 확인하고 적절히 처리하는 코드를 작성해야 합니다. 꼼꼼한 오류 처리는 안정적인 애플리케이션(application)의 기본입니다.
  • 커넥션(Connection) 관리: database/sql 패키지의 sql.DB 객체는 내부적으로 데이터베이스(database) 커넥션 풀(connection pool)을 관리합니다. 즉, 여러 개의 데이터베이스(database) 연결을 미리 만들어두고 필요할 때마다 빌려 쓰고 반납하는 방식으로 동작하는데요. 따라서 애플리케이션(application)에서 동일한 SQL(Structured Query Language) 문을 여러 번 실행해야 하는 경우에는, 준비된 구문(Prepared Statement)을 한 번만 Prepare 해두고 재사용하는 것이 커넥션(connection) 관리 및 성능 면에서 훨씬 효율적입니다.

결론

지금까지 Go(고) 언어에서 준비된 구문(Prepared Statement)을 사용하는 방법과 그 이점, 그리고 주의할 점들에 대해 자세히 알아보았습니다.

준비된 구문(Prepared Statement)을 올바르게 활용하면 SQL 삽입(SQL Injection) 공격으로부터 애플리케이션(application)을 안전하게 보호하고, 반복적인 SQL(Structured Query Language) 실행 성능을 개선하는 데 큰 도움을 받을 수 있습니다.

이 단계들과 모범 사례들을 잘 따라서, 여러분의 Go(고) 애플리케이션(application)을 더욱 안전하고 효율적으로 만들어 보시기 바랍니다!