Soft Delete

Jiwon Jung·2025년 11월 25일

스프링(Spring)

목록 보기
16/20

드디어 JWT 로그인 구현을 끝냈다!

아직 예외 처리도 다듬어야 하고 인가(권한)는 구현하지 못 했지만, 차근 차근 개념을 쌓아가면서 구현해보려고 한다.

근데 JWT에 시간을 쏟다가 CRUD나 JPA를 까먹을까봐 걱정이긴 하다.

코드카타도 못 풀고 있고 해야할 건 많아서 시간 배분을 좀 잘 해야할 것 같다.

무튼 오늘은 논리 삭제(Soft Delete)와 물리 삭제(Hard Delete)에 대해 알아보려고 한다.

현업에서 데이터는 전부 자산이고, 데이터 삭제에는 여러 상황이 엮여있기 때문에 DB에서 아예 삭제하는 일은 드물다고 한다.

그렇기에 Soft Delete가 중요하고, 상황에 맞추어 삭제 방식을 잘 택해야 한다.


🌱 Soft Delete(논리 삭제)란?

  • 데이터베이스에서 데이터를 삭제하지 않고, 사용자 입장에서는 데이터에 접근할 수 없게 하는 방식을 말한다.
  • 물리적으로는 삭제되지 않고 논리상으로만 삭제되는 것이다.
  • 보통 is_deleted 컬럼을 만들어 boolean 값으로 데이터 삭제 여부를 결정한다.

🌱 Hard Delete(물리 삭제)란?

  • 데이터베이스에서 데이터를 직접 삭제하는 방식을 말한다.
  • 더 이상 사용하지 않는 데이터를 데이터베이스에 저장하는 것은 저장 공간을 낭비하는 것일 수도 있다.
  • 이런 경우 DB에서 직접 데이터를 삭제함으로써 저장 공간을 확보할 수 있다.

🌱 Hard Delete를 기피하는 이유가 뭘까?

  • 애플리케이션은 사용자나 검색 기록 등을 기반으로 더 나은 서비스 개선을 위해 데이터 분석을 해야할 수 있다.
  • 데이터를 다시 복구해야 하는 상황이 발생할 수 있다.
  • update 쿼리가 delete 쿼리보다 몇 ms 더 빠르다.
  • 데이터는 곧 자산이다.

그럼 그냥 Sofe Delete만 쓰면 되는 거 아닌가?

당연히 아니다!


🌱 Soft Delete의 단점

  • is_deleted 컬럼이 하나 추가되므로 테이블의 크기가 커진다.
  • 더 이상 사용되지 않는 데이터가 계속 DB 상에 존재하기에 저장 공간이 무거워진다.
  • 매번 삭제 여부를 고려해야 한다.

🌱 Sofe Delete는 어떻게 구현할까?

Hard Delete는 DB에 DELETE 쿼리를 날리면 된다.
그럼 Sofe Delete는 어떻게 구현할까?

JPA로 간단하게 구현할 수 있다.
바로 어노테이션을 사용하면 된다!

✅ @SQLDelete

  • @SQLDelete(sql="삭제 쿼리")
  • 데이터를 삭제하는 경우에 @SQLDelete의 sql 옵션으로 지정해놓은 쿼리가 나간다.
  • 여기서 Sofe Delete에 사용하는 필드값(is_deleted)을 변경해주는 update 문을 작성해서 Soft Delete를 구현한다.

✅ @Where 또는 @SQLRestriction

  • @Where(clause="is_deleted = false")
  • 데이터를 조회하는 경우에 논리적으로 삭제된 데이터를 제외하고 조회하는 조건을 기본값으로 지정한다.
  • @SQLRestriction 어노테이션은 Hibernate 5.4 버전 이상부터 @Where를 대신해서 사용된다.
  • @SQLRestriction("is_deleted = false")
  • 모든 조회 쿼리에 WHERE 조건이 붙어서 나가기 때문에 만약에 삭제된 데이터를 조회해야 하는 경우가 생기면 네이티브 쿼리를 이용하는 등의 다른 방법이 필요하다.
  • 삭제 데이터는 조회할 수 없으므로 삭제 데이터를 조회해야 할 때는 부적합하다.
@Entity
@SQLDelete(sql = "UPDATE users SET is_deleted = true WHERE id = ?") // 🚨 DB 컬럼명 기준
@SQLRestriction("is_deleted = false") // 🚨 DB 컬럼명 기준
public class User() {
	...
}

➡️ 이건 다른 얘긴데, is_deleted 컬럼을 BaseEntity에 추가하는 사안도 고려해보면 좋겠다.

✅ 유저 삭제 시 게시글도 Soft Delete 하고 싶다면?

🚨 유저가 탈퇴 해도 해당 유저의 게시글은 여전히 다른 사용자가 볼 수 있도록 하고 싶다면 이 아래의 내용을 구현하지 않아도 된다.

유저가 삭제 되면 해당 유저가 작성한 게시물이나 댓글도 안 보이게 하고 싶을 수 있다.

먼저 유저의 is_deleted가 true가 되면 그에 따른 게시물과 댓글의 is_deleted도 true로 두는 방식을 생각해보자.

근데 유저 1명이 게시물 10만 개를 가지고 있는 상황이면, 유저 하나를 Soft Delete 할 때 연관된 게시글 수 만큼 UPDATE를 수행해야 해서 트랜잭션이 무거워지고 락도 많이 걸린다.

🤔 그럼 어떻게 해야할까?

이 방법보다는, 게시글을 불러올 때(조회) users 테이블의 is_deleted도 false이고, posts is_deleted도 false인 경우만 가져오는 방식이 권장된다.

// 삭제된 유저라면 해당 유저의 게시글을 안 보여주고, 단독적으로 삭제된 게시글도 안 보여준다.
// 즉, 탈퇴 하지 않은 유저의 삭제되지 않은 게시물만 보여준다.
SELECT p
FROM Post p
JOIN p.user u
WHERE p.isDeleted = false AND u.isDeleted = false

다만 이 방법의 단점이라 하면 게시글이나 댓글 테이블만 따로 봤을 때, 마치 삭제되지 않은 게시글이나 댓글처럼 보일 수 있다. (탈퇴 유저가 작성한 게시물들의 is_deleted는 여전히 false로 남아있기 때문)

반대로 유저의 is_deleted가 true가 되면 그에 따른 게시물과 댓글의 is_deleted도 true로 두는 방식을 쓰면 단독으로 조회해도 논리 삭제 상태가 명확하게 보인다.

하지만 성능 상의 문제가 더 중요하기 때문에 조회 시 조건을 거는 방식을 쓰는 편이 좋겠다.

1️⃣ JPQL을 이용한 방법

// """는 Java의 Text Block 문법이다.
@Query("""
       SELECT p FROM Post p
       JOIN p.user u
       WHERE p.isDeleted = false // 🚨 JPQL에서는 DB 컬럼명이 아니라 엔티티 필드명 기준
         AND u.isDeleted = false // 🚨 JPQL에서는 DB 컬럼명이 아니라 엔티티 필드명 기준
       """)
List<Post> findActivePosts();

PostRepositoryCommentRepository에서 JPQL로 조건을 거는 방법이다.

2️⃣ @Where를 이용한 방법

// User 엔티티
@Entity
@Where(clause = "is_deleted = false")
class User {
    ...
}
// Post 엔티티
@Entity
@Where(clause = "is_deleted = false")
public class Post {

    ...
    
    @ManyToOne
    @Where(clause = "is_deleted = false")
    private User user;
}

나는 아무래도 JPQL 방식이 더 좋아보인다.
조만간 꼭 사용해 봐야겠다!

0개의 댓글