티스토리 뷰
Go언어에서 사용할 수 있는 GORM을 사용하다 보면, 자연스럽게 Soft Delete라는 개념을 마주치게 된다.
나에게는 좀 색다른 개념이였는데, DB에 데이터를 저장하는 한 가지 패턴이라는 것을 알게 되었고 오늘은 이에 대해 정리해보고자 한다. Go, GORM을 기준으로 정리할 것이다.
import "gorm.io/gorm"
type Users struct {
gorm.Model
Name string `gorm:"size:255"`
Email string
Password string
}
GORM의 Entity는 위와 같이 선언할 수 있다. 여기서 gorm.Model이라는 필드가 보이는데, 이는 GORM 측에서 제공하는 기본적인 모델 스키마이다. 그래서 이를 선언하면, 기본적으로 아래의 4가지 column이 추가된 테이블을 볼 수 있다.
id, created_at, updated_at, deleted_at
- created_at : raw가 추가된 시간을 나타낸다.
- updated_at : raw가 업데이트된 시간을 나타낸다.
- deleted_at : raw가 삭제된 시간을 나타낸다.
soft delete는 이 3가지 column 중 당연하게도 deleted_at과 연관되어 있다. 우선은 어떤 개념인지 정의하기 전, 눈으로 결과를 먼저 살펴보는 것이 가장 와닿으므로 간단한 프로젝트를 만들어봤다.
user_entity.go
package entity
import "gorm.io/gorm"
type Users struct {
gorm.Model
Name string `gorm:"size:255"`
Email string
Password string
}
간단한 entity 계층 코드이다. gorm.Model을 선언한 것 말고는 특별한 점이 없기에 넘어가겠다.
user_repository.go
package repository
import (
"custom-modules/entity"
"errors"
"fmt"
"gorm.io/gorm"
)
type UserRepository interface {
Save(user *entity.Users) error
FindByEmail(email string) (interface{}, error)
DeleteByEmail(email string) error
}
type UserRepositoryImpl struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &UserRepositoryImpl{db}
}
func (repo *UserRepositoryImpl) Save(user *entity.Users) error {
return repo.db.Create(user).Error
}
func (repo *UserRepositoryImpl) FindByEmail(email string) (interface{}, error) {
var user entity.Users
err := repo.db.Where("email = ?", email).First(&user).Error
fmt.Println(email, err)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New("user not found")
}
return nil, err
}
return &user, nil
}
func (repo *UserRepositoryImpl) DeleteByEmail(email string) error {
var user entity.Users
err := repo.db.Where("email = ?", email).First(&user).Error
fmt.Println(email, err)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New("user not found")
}
return err
}
repo.db.Delete(&user)
return nil
}
User Entity에 대해 CRD를 수행할 repository 계층 코드를 위와 같이 작성했다.
user_service.go
package service
import (
"custom-modules/dto"
"custom-modules/entity"
"custom-modules/repository"
"errors"
"fmt"
)
type UserService interface {
SaveUser(request dto.CreateUserRequest) error
FindOneByEmail(email string) (interface{}, error)
DeleteUserByEmail(email string) error
}
type UserServiceImpl struct {
userRepository repository.UserRepository
}
func NewUserService(userRepository repository.UserRepository) UserService {
return &UserServiceImpl{
userRepository: userRepository,
}
}
func (userService *UserServiceImpl) SaveUser(request dto.CreateUserRequest) error {
user := entity.Users{
Name: request.Name,
Email: request.Email,
Password: request.Password,
}
return userService.userRepository.Save(&user)
}
func (userService *UserServiceImpl) FindOneByEmail(email string) (interface{}, error) {
user, err := userService.userRepository.FindByEmail(email)
if err != nil {
return nil, err
}
return user, err
}
func (userService *UserServiceImpl) DeleteUserByEmail(email string) error {
err := userService.userRepository.DeleteByEmail(email)
if err != nil {
return err
}
return nil
}
비즈니스 로직을 수행하는 service 계층 코드이다.
user_controller.go
package controller
import (
"custom-modules/dto"
"custom-modules/service"
"github.com/gofiber/fiber/v2"
)
type UserController interface {
AddUser(c *fiber.Ctx) error
FindOneByEmail(c *fiber.Ctx) error
DeleteByEmail(c *fiber.Ctx) error
}
type UserControllerImpl struct {
userService service.UserService
}
func NewUserController(userService service.UserService) UserController {
return &UserControllerImpl{
userService,
}
}
func (controller *UserControllerImpl) AddUser(c *fiber.Ctx) error {
var request dto.CreateUserRequest
err := c.BodyParser(&request)
if err != nil {
return err
}
controller.userService.SaveUser(request)
return c.SendStatus(fiber.StatusOK)
}
func (controller *UserControllerImpl) FindOneByEmail(c *fiber.Ctx) error {
email := c.Params("email")
user, err := controller.userService.FindOneByEmail(email)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"message": email + "과 일치하는 회원이 존재하지 않습니다.",
})
}
return c.JSON(user)
}
func (controller *UserControllerImpl) DeleteByEmail(c *fiber.Ctx) error {
email := c.Params("email")
err := controller.userService.DeleteUserByEmail(email)
if err != nil {
return c.SendStatus(fiber.StatusNotFound)
}
return c.SendStatus(fiber.StatusOK)
}
마지막으로 요청을 받을 controller 코드를 위와 같이 작성했다.
이제 직접 요청을 보내서, table raw가 어떻게 변하는지 살펴보자.
user를 생성하는 request를 보내면, 위와 같은 raw가 추가된 것을 볼 수 있다. gorm.Model 덕분에 created_at, updated_at을 따로 채우지 않았음에도 값이 들어간 것을 볼 수 있다. 그리고 deleted_at은 현재 null이 들어있다. 여기서 해당 user를 삭제하는 요청을 보내면 어떤 일이 생길까?
deleted_at column이 채워진 것을 볼 수 있다. 값을 삭제했음에도 table에는 여전히 해당 값이 저장되어 있고, deleted_at값만 업데이트 된 것이다.
여기서 id나 email을 기준으로 조회를 하면 어떤 일이 생길까??
404 response를 받았다. 실제 DB에는 여전히 값이 저장되어 있음에도 정상적으로 값을 조회하지 못한다. 어떻게 이것이 가능할 것일까?
로그를 살펴보면, 그 이유를 알 수 있는데, GORM으로 조회를 할때, `users`.`deleted_at` IS NULL이라는 조건이 추가되었기 때문이다.
Soft Delete의 장단점
Soft Delete는 데이터가 실제로 삭제되지 않고, 삭제된 시각을 업데이트해서 값이 삭제되었다고 '표시'를 하는 기법이다.
Soft Delete를 사용할 때, DB에 저장된 값이 어떻게 변하는지, 요청을 보낼때 어떻게 처리하는지는 이제 눈으로 살펴봐서 알겠는데.. 왜 이러한 기법을 쓰는 걸까? 값을 그냥 지워버리면 탐색 범위도 작아지고, 쿼리도 단순해서 성능이 빠를 것 같은데 말이다. 분명 어떤 장점이 있기에 이 기법이 등장한 것일텐데... 그래서 장단점을 비교해봤다.
장점:
- 복구 가능: 실수로 레코드를 삭제한 경우, soft delete를 사용하면 삭제된 데이터를 복구할 수 있다.
- 데이터 보존: soft delete를 사용하면 레코드를 삭제할 때 데이터가 영구적으로 손실되는 것을 방지할 수 있다.
- 기록 보존: 삭제 이력을 저장하면 보안 및 규정 준수와 같은 이유로 기록을 보존할 수 있다.
단점:
- 저장 공간 필요: soft delete를 사용하면 실제로 삭제되지 않은 레코드가 데이터베이스에 유지된다. 따라서, 더 많은 저장 공간이 필요하다.
- 성능 저하: 삭제되지 않은 레코드를 유지하기 때문에 데이터베이스의 성능이 저하될 수 있다. 특히, 많은 양의 레코드가 있는 경우 성능 저하가 심각할 수 있다.
- 불필요한 데이터 검색: soft delete를 사용하면 삭제된 레코드도 데이터베이스에서 검색된다. 따라서, 데이터베이스 쿼리가 더 복잡해질 수 있다.
선택은 각자의 몫이다. 나는 안쓸거다.
'Language > Go' 카테고리의 다른 글
[Go] Goroutine (4) | 2023.03.18 |
---|
- Total
- Today
- Yesterday
- Database
- effective
- Operating System
- fiber
- ARP
- network
- mmu
- spring
- java
- 공지
- algorithm
- cs
- go
- Effective Java
- soft delete
- GORM
- OS
- paging
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |