빠른 흐름
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, query, args...); err != nil {
return err
}
return tx.Commit()Go transaction 코드는 시작, 실패 시 rollback, 성공 시 commit, transaction 객체로 query 실행을 한 세트로 봅니다.
기본 흐름
transaction은 BeginTx로 시작한다
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}BeginTx는 context와 transaction option을 받습니다. isolation level이나 read-only 같은 조건이 필요하면 sql.TxOptions를 넘깁니다.
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
ReadOnly: false,
})실패 경로에는 Rollback을 둔다
defer tx.Rollback()Rollback은 transaction이 이미 commit된 뒤 호출되면 에러를 돌려줄 수 있지만, defer로 둔 rollback은 실패 경로 안전망으로 자주 씁니다. 중요한 것은 중간 return이 생겨도 transaction이 열린 채 남지 않게 하는 것입니다.
transaction 안에서는 tx로 query한다
if _, err := tx.ExecContext(ctx, "insert into users(id, name) values($1, $2)", id, name); err != nil {
return err
}transaction 안에서 실행되어야 하는 query는 db.ExecContext가 아니라 tx.ExecContext, tx.QueryContext, tx.QueryRowContext를 사용합니다.
커밋
모든 작업이 성공한 뒤 Commit한다
if err := tx.Commit(); err != nil {
return err
}
return nilCommit이 성공해야 transaction 결과가 확정됩니다. Commit 자체도 실패할 수 있으므로 반환값을 확인해야 합니다.
helper로 감싸면 흐름이 선명해진다
func CreateUser(ctx context.Context, db *sql.DB, user User) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if err := insertUser(ctx, tx, user); err != nil {
return err
}
return tx.Commit()
}작업 단위가 길어지면 transaction 경계를 함수 하나로 묶어 두는 편이 읽기 쉽습니다.
선택 기준
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 여러 SQL 작업이 함께 성공/실패해야 함 | transaction |
| context가 있는 transaction | BeginTx |
| 중간 실패 가능 | defer tx.Rollback() |
| transaction 내부 query | tx.ExecContext/tx.QueryContext |
| 최종 확정 | tx.Commit() 반환값 확인 |
주의할 점
transaction을 시작한 뒤 db.ExecContext로 query를 실행하면 그 query는 transaction 밖에서 실행될 수 있습니다. transaction 안의 모든 작업은 tx 객체에 붙여야 합니다. 또한 transaction을 오래 열어 두면 lock과 connection을 오래 잡을 수 있으므로, 외부 API 호출이나 긴 계산을 transaction 안에 넣지 않는 편이 좋습니다.
참고 링크
2 sources