Spring soft delete로 논리 삭제 적용하기

devguri·2023년 7월 21일
1

Soft Delete란 ?

  • 데이터 DELETE시 Soft Delete와 Hard Delete로 나뉘어진다.
  • delete 쿼리로 데이터 자체를 삭제하는 것이 아니라 update 쿼리로 상태를 변경하여 삭제된 데이터로 구분하는 것을 말함
  • 삭제된 데이터를 따로 보관해야하거나 중요한 정보 삭제에 대해 방지하고자 사용하는 것임
  • deleted_at을 사용하여 삭제된 날짜를 기록할 것이다.
  • 물리적 데이터를 삭제하는 것이 아니므로 디스크 사용량이 증가한다

Soft Delete query

  • user 삭제시 soft delete로 데이터를 30일 보관할 수 있도록 구현할거다.
  • 어떤식으로 동작하는지 쿼리를 확인해보자

Soft Delete VS Hard Delete 쿼리 비교

Soft Delete

# 삭제
UPDATE user SET deleted_at = LocalDateTime.now() WHERE id = ?

# 조회
SELECT * FROM user WHERE deleted_at = null

# 복원
UPDATE user SET deleted_at = null WHERE id = ?

Hard Delete


# 삭제
DELETE user FROM table WHERE id = ?

# 조회
SELECT * FROM user
  • delete query가 아니라서 cascade를 사용할 수 없다 -> 이부분을 주의해야한다.
  • 조회시 -> 삭제된 데이터 제외해야하므로 where, on절에 삭제되 데이터를 제외하는 조건이 포함되어야 한다.

Unique Constraint

  • soft Delete 사용하면 unique constraint를 위반하게 된다.
  • 삭제된 데이터로 인해 칼럼 값 중복된다.

On Delete Cascade

  • cascade 삭제를 사용하기 위해 트리거로 직접 구현해야한다.
  • 데이터 삭제되는 경우, foreign key constraint 위반이 발생하여 삭제 전략에 따라 별도의 로직과 동시성 처리가 필요하게 된다.
  • 따라서

JPA + Hibernate 개발환경에서 구현하기

@SQLDELETE

  • DELETE 쿼리가 발생할 때, 특정 쿼리로 바꿔서 실행시켜주는 어노테이션이다.
  • user 데이터 삭제시 deleted_at의 값을 현재 시간으로 바꿔주도록 설정가능하다.

@SQLDELETE(sql = "UPDATE user SET deleted_at = LocalDateTime.now() WHERE id = ?")

-> 이 퀴리를 통해 데이터 삭제시 deleted_at의 값을 현재시간으로 update 시킬 수 있다.

@Entity
@Getter
@SQLDelete(sql = "UPDATE user SET deleted_at = LocalDateTime.now() WHERE id = ?")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class user extends BaseEntity {
	.
	.
	.
}
  • user 데이터 삭제시 deleted_at의 값을 현재 시간으로 바꾸도록 쿼리가 실행되는 것을 확인할 수 잇다.

@WHERE

  • Where 구문을 통해 해당값인 경우만 삭제되지 않은 것으로 판단한다.

@Where(clause = "deleted_at IS NULL") -> deleted_at이 NULL인 경우 삭제되지 않은 값으로 확인하여 해당 값을 사용할 수 있는 것이다.

Soft Delete로 논리 삭제 구현하기

@Override
    @Transactional
    public void delete(User user) {
        User target = getUser(user.getId());
        target.delete();

        friendRepository.findAllByUserId(target.getId())
            .forEach(Friend::delete);

        friendRepository.findAllByTargetId(target.getId())
            .forEach(Friend::delete);

        cooldownRepository.findByUserId(target.getId())
            .ifPresent(Cooldown::delete);
    }

-> service에서 해당 delete 함수 실행시 user entity에 delete 함수가 실행된다.

public void delete() {
        this.deletedAt = LocalDateTime.now();
        this.point = 0;
    }

-> deletedAt을 현재시간으로 설정하고 포인트 또한 0으로 바꾼다.

문제 발생

  • 이때 유저와 친구로 맺은 데이터를 다 삭제해야하는데 delete 쿼리를 날리는게 아니라 cascade 정책이 실행되지 않는다. 그래서 따로 친구 데이터를 찾아서 deleted_at을 지금시간으로 바꿔줘야한다.

[A]

friendRepository.findAllByUserId(target.getId())
     .forEach(Friend::delete);

[B]

@Where(clause = "deleted_at IS NULL")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Friend extends AuditingTimeEntity {

	 public void delete() {
        this.deletedAt = LocalDateTime.now();
    }
    
    public void renew() {
        this.deletedAt = null;
    }
}

[C]

public void renew() {
        this.deletedAt = null;
    }
  1. 유저가 삭제된다면 위에 [A]가 실행되어 frien class의 delete 함수가 실행된다.
  2. delete 함수가 실행되면 [B] 코드를 통해 friend entity의 deleted_at이 현재 시간으로 저장되어 삭제된 값으로 구분되게 한다.
  3. Friend class는 deletedAt이 NULL인 경우만 데이터가 존재한다고 판단하여 DB에서 관리하게 된다.
  4. 다시 사용자가 가입하게 되어 데이터를 복구시킬 경우 [C] 코드에서 deletedAt을 다시 null로 만든다.
profile
Always live diligently

0개의 댓글

관련 채용 정보