golang

Go, XORM, Soft delete, Unscoped

주먹불끈 2024. 2. 14. 19:25

개요

Go에서 XORM을 사용하며 soft delete 기능을 구현하는 것에 대해 이모저모를 알아본다.

ChatGPT를 적극 활용하였음을 밝혀두며, 코드 동작은 확인하였다.

Soft delete

Soft delete란 데이터베이스 관리에서 데이터를 실제로 삭제하지 않고, 데이터가 삭제된 것처럼 처리하는 방식을 말한다.

Soft delete를 구현하는 일반적인 방법은 데이터베이스 테이블에 '삭제됨' 상태를 나타내는 별도의 필드(예: is_deleted, deleted_at 등)를 추가하고 사용자가 데이터를 삭제하려고 할 때, 실제로 데이터 행을 제거하는 대신 '삭제됨' 상태를 나타내는 필드를 업데이트하여 삭제 표시를 한다. 예를 들어, deleted_at 필드에 삭제 요청 시간을 기록하여 언제 데이터가 삭제 처리되었는지 추적할 수 있다.

Soft delete의 주요 장점

  1. 데이터 복구 용이성: 실수로 데이터를 삭제했거나 나중에 삭제된 데이터가 필요한 경우, 손쉽게 복구할 수 있다.
  2. 데이터 무결성 유지: 데이터를 실제로 삭제하지 않음으로써, 참조 무결성을 유지하고 관련 데이터 간의 연결을 보존할 수 있다.
  3. 감사 및 기록 유지: 언제, 어떤 데이터가 삭제되었는지 기록을 통해 감사할 수 있다.

단점

  1. 실제로 데이터를 삭제하지 않기 때문에 데이터베이스의 용량을 점차 증가시킬 수 있다.
  2. 삭제된 데이터를 쿼리에서 제외하기 위한 추가적인 로직이 필요하다.

XORM에서의 Soft delete

Go 언어에서 XORM은 강력한 ORM(Object-Relational Mapping) 라이브러리 중 하나로, 구조체(struct)를 사용하여 데이터베이스의 테이블과 매핑되는 객체를 쉽게 작업할 수 있게 해준다. XORM에서 "soft delete" 기능을 구현하려면 deleted 태그를 사용하면 된다.

XORM에서 Soft Delete 구현하기

XORM에서 soft delete를 구현하려면, 모델 구조체에 삭제를 표시할 수 있는 필드를 추가하고, 이 필드에 deleted 태그를 설정하면 된다. 일반적으로 deleted 태그는 시간 타입 필드(time.Time 또는 *time.Time)에 사용되어 해당 데이터가 삭제된 시간을 기록한다. deleted 태그가 있는 필드가 NULL 또는 time.Time 타입의 zero value인 0001-01-01 00:00:00Z가 아니면, XORM은 해당 레코드를 삭제된 것으로 간주한다.

예제 코드를 보자. 코드 간결화를 위해 에러 처리는 생략했다.

package main

import (
	"fmt"
	"time"

	"github.com/go-xorm/xorm"
	_ "github.com/mattn/go-sqlite3"
)

// User 구조체 정의
type User struct {
	Id      int64     `xorm:"'id' pk autoincr"`
	Name    string    `xorm:"'name'"`
	Deleted time.Time `xorm:"deleted"` // soft delete를 위한 필드
}

func main() {
	engine, _ := xorm.NewEngine("sqlite3", "file::memory:?cache=shared")
	defer engine.Close()

	engine.Sync2(new(User))

	// 사용자 추가
	user1 := User{Name: "John Doe"}
	user2 := User{Name: "Jane Doe"}
	engine.Insert(&user1, &user2)

	// 모든 사용자 조회 (삭제 전)
	fmt.Println("All users before deletion:")
	var allUsersBeforeDelete []User
	engine.Find(&allUsersBeforeDelete)
	fmt.Printf("%+v\\n", allUsersBeforeDelete)
	// 출력
	// All users before deletion:
	// [{Id:1 Name:John Doe Deleted:0001-01-01 00:00:00 +0000 UTC} {Id:2 Name:Jane Doe Deleted:0001-01-01 00:00:00 +0000 UTC}]

	// user1 삭제
	engine.Delete(&User{Id: user1.Id})

	// 삭제 후 모든 사용자 조회 (삭제된 레코드는 조회되지 않음)
	fmt.Println("\\nAll users after deletion:")
	var allUsersAfterDelete []User
	engine.Find(&allUsersAfterDelete)
	fmt.Printf("%+v\\n", allUsersAfterDelete)
	// 출력
	// All users after deletion:
	// [{Id:2 Name:Jane Doe Deleted:0001-01-01 00:00:00 +0000 UTC}]

	// 삭제된 레코드만 조회. Unscoped 사용
	fmt.Println("\\nDeleted record only:")
	var deletedUsers []User
	engine.Unscoped().Where("deleted is not null").Find(&deletedUsers)
	fmt.Printf("%+v\\n", deletedUsers)
	// 출력
	// Deleted record only:
	// [{Id:1 Name:John Doe Deleted:2024-02-14 12:52:04 +0900 KST}]

	// 삭제된 레코드 복구. Unscoped 사용
	if len(deletedUsers) > 0 {
		deletedUser := deletedUsers[0]
		deletedUser.Deleted = time.Time{} // Zero value로 설정하여 복원
		// engine.ID(deletedUser.Id).Unscoped().Update(&deletedUser) // XORM은 기본적으로 zero value는 업데이트하지 않음
		engine.ID(deletedUser.Id).Unscoped().MustCols("deleted").Update(&deletedUser) // 이렇게 MustCols를 사용하면 zero value도 업데이트됨
	}

	// 복구 후 모든 사용자 조회
	fmt.Println("\\nAll users after recovery:")
	var allUsersAfterRecovery []User
	engine.Find(&allUsersAfterRecovery)
	fmt.Printf("%+v\\n", allUsersAfterRecovery)
	// 출력
	// All users after recovery:
	// [{Id:1 Name:John Doe Deleted:0001-01-01 00:00:00 +0000 UTC} {Id:2 Name:Jane Doe Deleted:0001-01-01 00:00:00 +0000 UTC}]

	// 완전히 삭제 (Unscoped 사용)
	if len(allUsersAfterRecovery) > 0 {
		engine.ID(allUsersAfterRecovery[0].Id).Unscoped().Delete(&User{})
	}

	// 완전 삭제 후 삭제된 레코드 조회. Unscoped 사용
	fmt.Println("\\nDeleted record only:")
	var deletedUsers2 []User
	engine.Unscoped().Where("deleted is not null").Find(&deletedUsers2)
	fmt.Printf("%+v\\n", deletedUsers2)
	// 출력
	// Deleted record only:
	// []

}

코드 상세 리뷰

위 코드 전문을 쪼개서 분석해보자.

xorm 태그

// User 구조체 정의
type User struct {
	Id      int64     `xorm:"'id' pk autoincr"`
	Name    string    `xorm:"'name'"`
	Deleted time.Time `xorm:"deleted"` // soft delete를 위한 필드
}

Deleted 필드는 deleted 태그를 사용하여 soft delete를 위한 필드로 지정한다.

time.Time의 zero value인 0001-01-01 00:00:00Z 또는 null 값이 이 필드에 있으면 삭제되지 않은 것으로 간주한다.

이 데이터를 soft delete 하려면, 해당 필드를 현재 시간으로 설정하기만 하면 된다. 언제 삭제하였는지의 기록도 되는 것이다.

soft delete

// 모든 사용자 조회 (삭제 전)
fmt.Println("All users before deletion:")
var allUsersBeforeDelete []User
engine.Find(&allUsersBeforeDelete)
fmt.Printf("%+v\\n", allUsersBeforeDelete)
// 출력
// All users before deletion:
// [{Id:1 Name:John Doe Deleted:0001-01-01 00:00:00 +0000 UTC} {Id:2 Name:Jane Doe Deleted:0001-01-01 00:00:00 +0000 UTC}]

// user1 삭제
engine.Delete(&User{Id: user1.Id})

// 삭제 후 모든 사용자 조회 (삭제된 레코드는 조회되지 않음)
fmt.Println("\\nAll users after deletion:")
var allUsersAfterDelete []User
engine.Find(&allUsersAfterDelete)
fmt.Printf("%+v\\n", allUsersAfterDelete)
// 출력
// All users after deletion:
// [{Id:2 Name:Jane Doe Deleted:0001-01-01 00:00:00 +0000 UTC}]

// 삭제된 레코드만 조회. Unscoped 사용
fmt.Println("\\nDeleted record only:")
var deletedUsers []User
engine.Unscoped().Where("deleted is not null").Find(&deletedUsers)
fmt.Printf("%+v\\n", deletedUsers)
// 출력
// Deleted record only:
// [{Id:1 Name:John Doe Deleted:2024-02-14 12:52:04 +0900 KST}]
  1. 사용자를 2명 추가한 다음 바로 확인하면 2개의 레코드를 확인할 수 있다.
  2. user1을 삭제하고 다시 조회하면 user2만 보인다.
  3. Unscoped()를 적용하면 삭제된 필드까지 볼 수 있는데 이때에는 Where절에 의해 삭제된 user1을 볼 수 있다.

복구

// 삭제된 레코드 복구. Unscoped 사용
if len(deletedUsers) > 0 {
    deletedUser := deletedUsers[0]
    deletedUser.Deleted = time.Time{} // Zero value로 설정하여 복원
    // engine.ID(deletedUser.Id).Unscoped().Update(&deletedUser) // XORM은 기본적으로 zero value는 업데이트하지 않음
    engine.ID(deletedUser.Id).Unscoped().MustCols("deleted").Update(&deletedUser) // 이렇게 MustCols를 사용하면 zero value도 업데이트됨
}

// 복구 후 모든 사용자 조회
fmt.Println("\\nAll users after recovery:")
var allUsersAfterRecovery []User
engine.Find(&allUsersAfterRecovery)
fmt.Printf("%+v\\n", allUsersAfterRecovery)
// 출력
// All users after recovery:
// [{Id:1 Name:John Doe Deleted:0001-01-01 00:00:00 +0000 UTC} {Id:2 Name:Jane Doe Deleted:0001-01-01 00:00:00 +0000 UTC}]
  1. deleted 필드를 zero value로 업데이트하면 삭제된 레코드를 복원할 수 있다.
  2. 이때 기본적인 Update()만으로는 zero value로는 업데이트 하지않는 XORM 특성으로 업데이트 되지 않음에 주의
  3. 다시 전체 조회를 하면 복구되어 있음을 알 수 있다.

영구히 삭제

// 완전히 삭제 (Unscoped 사용)
if len(allUsersAfterRecovery) > 0 {
    engine.ID(allUsersAfterRecovery[0].Id).Unscoped().Delete(&User{})
}

// 완전 삭제 후 삭제된 레코드 조회. Unscoped 사용
fmt.Println("\\nDeleted record only:")
var deletedUsers2 []User
engine.Unscoped().Where("deleted is not null").Find(&deletedUsers2)
fmt.Printf("%+v\\n", deletedUsers2)
// 출력
// Deleted record only:
// []

영구히 삭제를 할 때에는 Unscoped만 사용하면 된다.

반응형