트랜잭션 실행하기
트랜잭션 실행하기 (Executing transactions)
데이터베이스 트랜잭션은 sql.Tx라는 타입으로 실행할 수 있어요. sql.Tx는 트랜잭션 하나를 나타내죠. Commit과 Rollback 같은 트랜잭션 전용 메서드 외에도, 흔한 데이터베이스 작업에 쓰는 메서드를 모두 갖추고 있습니다. sql.Tx를 얻으려면 DB.Begin이나 DB.BeginTx를 호출하면 돼요.
트랜잭션은 여러 작업을 하나의 큰 목표로 묶는 개념입니다. 모든 작업이 다 성공해야 하고, 하나라도 실패하면 아무것도 반영되지 않아요 — 어느 쪽이든 데이터의 무결성은 지켜집니다. 전형적인 트랜잭션 워크플로는 다음과 같아요.
출처: Go 공식 문서
- 트랜잭션을 시작한다.
- 일련의 데이터베이스 작업을 수행한다.
- 오류가 없으면 트랜잭션을 커밋해 데이터베이스 변경을 적용한다.
- 오류가 발생하면 트랜잭션을 롤백해 데이터베이스를 그대로 둔다.
sql 패키지는 트랜잭션을 시작하고 마무리하는 메서드와, 그 사이의 데이터베이스 작업을 수행하는 메서드를 함께 제공합니다. 이 메서드들이 위 워크플로의 네 단계에 딱 맞아떨어져요.
- 트랜잭션 시작.
DB.Begin또는DB.BeginTx가 새 트랜잭션을 시작하고, 이를 나타내는sql.Tx를 돌려줍니다. - 데이터베이스 작업 수행.
sql.Tx를 쓰면 단일 커넥션을 사용하는 일련의 작업으로 데이터베이스를 쿼리하거나 갱신할 수 있어요. 이를 위해Tx는 다음 메서드들을 노출합니다.INSERT,UPDATE,DELETE같은 SQL 문으로 데이터를 바꾸는Exec,ExecContext. 자세히: Executing SQL statements that don't return data- 행을 반환하는 작업용
Query,QueryContext,QueryRow,QueryRowContext. 자세히: Querying for data - 프리페어드 스테이트먼트를 미리 정의하는
Prepare,PrepareContext,Stmt,StmtContext. 자세히: Using prepared statements
- 트랜잭션 마무리. 다음 중 하나로 끝냅니다.
Tx.Commit으로 커밋.Commit이 성공하면(에러를nil로 반환) 모든 쿼리 결과가 유효한 것으로 확정되고, 실행된 모든 업데이트가 단일 원자적 변경으로 데이터베이스에 적용됩니다.Commit이 실패하면 그Tx에서 실행한Query와Exec의 결과는 모두 무효로 폐기해야 해요.Tx.Rollback으로 롤백.Tx.Rollback이 실패해도 그 트랜잭션은 더 이상 유효하지 않으며, 데이터베이스에 커밋되지도 않습니다.
모범 사례 (Best practices)
트랜잭션이 요구하는 까다로운 의미론과 커넥션 관리를 헤쳐 나가려면 아래 모범 사례를 따르는 게 좋아요.
- 이 섹션에서 설명하는 API들로 트랜잭션을 관리하세요.
BEGIN이나COMMIT같은 트랜잭션 관련 SQL 문을 직접 쓰면 안 됩니다 — 특히 동시성 프로그램에서는 데이터베이스가 예측 불가능한 상태로 남을 수 있어요. - 트랜잭션을 쓸 때는 트랜잭션 밖의
sql.DB메서드도 직접 호출하지 않도록 주의하세요. 그 메서드들은 트랜잭션 밖에서 실행되어 코드가 데이터베이스 상태를 일관되지 않게 보거나, 심하면 교착 상태(deadlock)를 일으킬 수 있어요.
예시 (Example)
다음 예시의 코드는 트랜잭션을 사용해 앨범에 대한 새 고객 주문을 만듭니다. 그 과정에서 코드는 다음 일들을 수행해요.
- 트랜잭션을 시작한다.
- 트랜잭션의 롤백을 defer 한다. 트랜잭션이 성공하면 함수가 끝나기 전에 커밋되므로 deferred 롤백은 아무 일도 하지 않아요. 실패하면 커밋되지 않은 채 함수가 끝나면서 롤백이 호출됩니다.
- 고객이 주문하는 앨범의 재고가 충분한지 확인한다.
- 충분하다면 주문 수량만큼 재고 수를 줄인다.
- 새 주문을 만들고 클라이언트에게 돌려줄 새 주문의 생성 ID를 가져온다.
- 트랜잭션을 커밋하고 ID를 반환한다.
이 예시는 context.Context 인자를 받는 Tx 메서드를 사용합니다. 덕분에 실행(데이터베이스 작업 포함)이 너무 오래 걸리거나 클라이언트 연결이 끊겨도 취소될 수 있어요. 자세한 내용은 Canceling in-progress operations을 참고하세요.
// CreateOrder creates an order for an album and returns the new order ID.
func CreateOrder(ctx context.Context, albumID, quantity, custID int) (orderID int64, err error) {
// Create a helper function for preparing failure results.
fail := func(err error) (int64, error) {
return 0, fmt.Errorf("CreateOrder: %v", err)
}
// Get a Tx for making transaction requests.
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fail(err)
}
// Defer a rollback in case anything fails.
defer tx.Rollback()
// Confirm that album inventory is enough for the order.
var enough bool
if err = tx.QueryRowContext(ctx, "SELECT (quantity >= ?) from album where id = ?",
quantity, albumID).Scan(&enough); err != nil {
if err == sql.ErrNoRows {
return fail(fmt.Errorf("no such album"))
}
return fail(err)
}
if !enough {
return fail(fmt.Errorf("not enough inventory"))
}
// Update the album inventory to remove the quantity in the order.
_, err = tx.ExecContext(ctx, "UPDATE album SET quantity = quantity - ? WHERE id = ?",
quantity, albumID)
if err != nil {
return fail(err)
}
// Create a new row in the album_order table.
result, err := tx.ExecContext(ctx, "INSERT INTO album_order (album_id, cust_id, quantity, date) VALUES (?, ?, ?, ?)",
albumID, custID, quantity, time.Now())
if err != nil {
return fail(err)
}
// Get the ID of the order item just created.
orderID, err = result.LastInsertId()
if err != nil {
return fail(err)
}
// Commit the transaction.
if err = tx.Commit(); err != nil {
return fail(err)
}
// Return the order ID.
return orderID, nil
}