[Golang] gorm으로 트랜잭션 다루는 방법

suji·2024년 3월 7일
0

Go

목록 보기
8/9
post-thumbnail

Transaction의 설명에 이어
GORM으로 트랜잭션을 다루는 방법을 적어보고자 한다.

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라는 변수를 사용한다.
  • tx는 트랜잭션을 나타내는 GROM의 DB객체이다.
  • 트랜잭션 내에서 수행되는 모든 작업을 하나의 트랜잭션으로 처리하게 된다.
  • 트랜잭션이 nil을 반환한다는 것은 에러가 발생하지 않았다는 의미로, 트랜잭션이 성공되었다고 간주되어 커밋이 수행된다.

중첩 트랜잭션

  • GORM은 중첩 트랜잭션을 지원한다.
  • 큰 트랜잭션 범위 내에서 수행된 일부 작업을 롤백할 수 있다.
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
  • 하나의 큰 트랜잭션 내에 세 개의 중첩된 트랜잭션 수행
  • 에러가 발생하여 user2를 롤백한다.
  • user3는 정상적으로 생성된다.
  • 최상위 트랜잭션도 에러 없이 정상적으로 완료되었기 때문에 user1도 정상적으로 생성된다.
  • 따라서 user1, user3는 commit, user2 rollback 된다.

    중첩된 트랜잭션을 사용하여 특정 작업을 롤백하거나 커밋할 수 있어 일부를 조정하는 방식으로 제어할 수 있다.

트랜잭션 수동 제어

// 트랜잭션 시작
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 인자를 전달하도록 정의한다.
  • tx를 함수 인자로 받으면 해당 함수 내에서 트랜잭션을 제어할 수 있다.
  • 또한 여러 작업을 하나의 트랜잭션으로 묶어서 관리할 수 있게 된다.

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
}
  • 트랜잭션을 begin시작 하면 tx인자를 받는 모든 데이터베이스 작업은 하나의 트랜잭션 내에서 수행된다.
  • petRepo.Create, userRepo.UpdateUser 한 개의 작업이라도 에러가 발생하면 모든 작업이 롤백된다.
  • 두 개의 작업이 모두 에러 없이 성공하면 트랜잭션은 커밋 된다.

Transaction Repository

트랜잭션을 미리 정의된 규칙에 따라 처리되도록 트랜잭션 관리 메서드를 두는 방법도 있다.

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

profile
문제를 해결하는 백엔드 개발자

0개의 댓글