Transaction의 설명에 이어
GORM으로 트랜잭션을 다루는 방법을 적어보고자 한다.
GROM 이란?
Go언어에 최적화된 ORM 라이브러로, 객체/구조체와 관계형 데이터베이스의 데이터 간의 존재하는 불일치를 매핑시켜주는 도구이다.
먼저 GORM 공식문서에 있는 transaction 부분을 살펴보자.
트랜잭션을 비활성화 하는 방법으로 SkipDefaultTransaction
옵션을 통해 제어한다.
// 전역으로 트랜잭션 비활성화
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
SkipDefaultTransaction: true,
})
// 연속 세션 모드에서 트랜잭션 비활성화
tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)
db.Transaction(func(tx *gorm.DB) error {
// db 대신 tx변수 사용
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
// 에러 반환 시 rollback
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}
// nil 반환 시, 전체 트랜잭션 커밋
return nil
})
tx
라는 변수를 사용한다.nil
을 반환한다는 것은 에러가 발생하지 않았다는 의미로, 트랜잭션이 성공되었다고 간주되어 커밋이 수행된다.db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1)
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user2)
return errors.New("rollback user2") // Rollback user2
})
tx.Transaction(func(tx3 *gorm.DB) error {
tx3.Create(&user3)
return nil
})
return nil
})
// Commit user1, user3
중첩된 트랜잭션을 사용하여 특정 작업을 롤백하거나 커밋할 수 있어 일부를 조정하는 방식으로 제어할 수 있다.
// 트랜잭션 시작
tx := db.Begin()
// tx 사용
tx.Create(...)
// ...
// 에러가 발생하면 롤백하기
tx.Rollback()
// 성공적으로 수행되어 커밋하기
tx.Commit()
func CreateAnimals(db *gorm.DB) error {
// 트랜잭션 내에서는 tx로 데이터베이스를 핸들링한다.
tx := db.Begin()
// defer 사용하여 패닉이 발생하면 함수가 종료되기 전에 트랜잭션을 롤백한다.
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Error; err != nil {
return err
}
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
tx.Rollback()
return err
}
// 모든 작업이 성공적으로 완료되면 nil을 반환하게 되고 커밋된다.
return tx.Commit().Error
}
위에서 GORM 공식 문서를 살펴봤고,
그에 따라 프로젝트에 적용해보자.
controller - service - repository
layer로 나누어져 있는 프로젝트에서 트랜잭션을 아래와 같이 적용했다.
반려견을 등록하게 되면, 유저 정보도 업데이트 되는 기능을 예시로 만들어보자.
repository
func (p *petRepo) Create(ctx context.Context, tx *gorm.DB, pet *entity.Pet) error {
return tx.WithContext(ctx).Create(&pet).Error
}
func (u *userRepo) UpdateUser(ctx context.Context, tx *gorm.DB, user *entity.User) error {
return tx.WithContext(ctx).Model(user).
Where("user_id = ?", user.ID).
Updates(user).Error
}
tx *gorm.DB
인자를 전달하도록 정의한다.service
func (p *PetService) RegisterPet(ctx context.Context, pet *entity.pet, user *entity.user) (err error) {
user.pet = pet
tx := p.writedb.Begin()
if err := p.petRepo.Create(ctx, tx, pet); err != nil {
tx.Rollback()
return err
}
if err := p.userRepo.UpdateUser(ctx, tx, user); err != nil {
tx.Rollback()
return err
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
return err
}
return nil
}
petRepo.Create
, userRepo.UpdateUser
한 개의 작업이라도 에러가 발생하면 모든 작업이 롤백된다.트랜잭션을 미리 정의된 규칙에 따라 처리되도록 트랜잭션 관리 메서드를 두는 방법도 있다.
func (t *TransactionRepository) Transaction(txFn func(tx *gorm.DB) error) error {
return t.db.Transaction(txFn)
}
transactionRepository.Transaction
메서드에 트랜잭션 관리를 위임한다.
(Transaction 함수는 Go Modoules > gorm.io/gorm > finisher_api.go
에서 참고하면 된다.)
service
func (p *PetService) RegisterPet(ctx context.Context, pet *entity.Pet, user *entity.User) (err error) {
user.pet = pet
// 트랜잭션 시작
err = p.transactionRepository.Transaction(func(tx *gorm.DB) error {
if createPetErr := p.petRepo.Create(ctx, tx, pet); createPetErr != nil {
return createPetErr
}
if updateUserErr := p.userRepo.UpdateUser(ctx, tx, user); updateUserErr != nil {
return updateUserErr
}
return nil
})
if err != nil {
p.logger.Errorf("[Error] RegisterPetWithTransaction Error: %v", err)
return err
}
return nil
}
이렇게 구현하게 되면,
트랜잭션의 관리를 중앙화 할 수 있고, 코드 중복을 줄일 수 있다.
또한 통합적으로 작업을 다루면서 에러 발생 시 조치를 일관되게 할 수 있다는 장점이 있다.
참고자료
GORM
Implementing database transactions with gorm in Golang