[Spring JPA] Soft Delete 란?

최동근·2023년 1월 13일
0

JPA

목록 보기
9/13
post-custom-banner

안녕하세요 오늘은 데이터를 삭제하는 방법중 Soft Delete 에 대해 알아보겠습니다 👨‍💻

🎒 Hard Delete vs Soft Delete

보통 데이터베이스 삭제라 함은, 실제 데이터베이스에 존재하는 데이터를 삭제하는 것을 의미합니다.
그러나, 이번 포스팅에서 다룰 'Soft Delete' 는 실제 물리 데이터를 삭제하는 것이 아니라 테이블에 삭제여부와 관련된
칼럼을 추가하여 update 쿼리를 날려서 삭제와 관련된 칼럼 값을 변경시키므로써, 삭제되었음을 표시합니다.

  • Hard Delete : 식제 물리적으로 데이터를 삭제
  • Soft Delete : 실제 물리적으로 데이터를 삭제하는 대신, 삭제 여부를 나타내는 칼럼을 사용

실제 현업에서는 Hard Delete 보다 Soft Delete 를 더 많이 사용한다고 합니다 🧑🏼‍💻
그렇다면 왜 물리적인 데이터를 바로 삭제하지 않고 Soft Delete를 사용할까요?

실제 개발시 다양한 데이터를 다루게 됩니다.
데이터를 CRUD 연산을 통해 자유자재로 다루는 것은 개발자에게는 필수적인 역량입니다 👼
만약 데이터를 삭제(Hard Delete)한다고 생각해볼까요?
데이터를 삭제 하는 순간 해당 데이터는 더 이상 존재하지 않게 됨으로 사용하지 못합니다.
하지만 현업에서는 어떠한 부득이한 상황으로 인해 이젠 삭제한 데이터를 복구해야하는 사례가 있을 수 있습니다.
혹은 예전 기록을 확인하고자 삭제한 데이터에 대한 기록이 필요할 수도 있습니다 ❗️

이처럼 현업에 맞춘 삭제 방법은 Soft Delete 에 더 가깝습니다.
우리는 개발을 하면서 어떠한 일이 발생할지 모르는거니깐요! 만일의 상황에 대비해 혹은 개발의 효율성을 위해 Soft Delete 를 사용하는 것을 권장합니다 💪
📣 Soft Delete가 어떤 방식인지 이해했으니 Spring JPA 에서 어떻게 Soft Delete를 구현할 수 있는지 알아볼까요?

🎒 @SQLDelete

해당 어노테이션을 통해 해당 엔티티를 삭제할때 일괄적으로 설정한 쿼리를 날릴 수 있습니다.
Soft Delete 방식은 Delete 쿼리 대신 삭제 여부 쿼리를 Update 해주는 쿼리를 날려야 함으로 @SQLDelete를 통해 Soft Delete 를 쉽게 사용할 수 있습니다.
제가 현재 진행중인 프로젝트 코드를 통해 해당 어노테이션 사용법을 더 알아보겠습니다.

  @Getter
  @MappedSuperclass
  @EntityListeners(AuditingEntityListener.class)
  public abstract class BaseEntity {

      @CreatedDate
      private LocalDateTime createdAt;

      @LastModifiedDate
      private LocalDateTime modifiedAt;

      private LocalDateTime deletedAt; // 삭제 일시를 나타내는 칼럼
  }
  @Entity
  @NoArgsConstructor(access = AccessLevel.PROTECTED)
  @Where(clause = "DELETED_AT is null")
  @SQLDelete(sql = "UPDATE COMMENT SET COMMENT.DELETED_AT = CURRENT_TIMESTAMP WHERE COMMENT.COMMENT_ID = ?") 
  // DB 테이블 이름 기준
  @Getter
  public class Comment extends BaseEntity {

      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Column(name = "COMMENT_ID")
      private Long id;

      @ManyToOne(fetch = FetchType.LAZY)
      @JoinColumn(name = "MEMBER_ID")
      private Member member;

      @Column(name = "CONTEXT")
      private String context;
  }

해당 코드는 댓글(Comment) 엔티티 클래스 입니다.
@SQLDelete 어노테이션이 보이시나요?
속성으로는 삭제할 때 날리고 싶은 쿼리문을 설정하였습니다. 우리는 soft delete를 구현할 것이기 때문에 삭제 일시를 UPDATE 하는 쿼리문을 설정하겠습니다 👨‍💻
제가 진행중인 프로젝트에서는 BaseEntity 에 deletedAt 이라는 필드를 추가하여 해당 필드를 상속받는 엔티티 클래스에서
상속받도록 설정하였습니다.
또한 삭제 여부를 삭제한 일시가 있는지 없는지로 판단하도록 설정하였습니다.

해당 엔티티에 대해 Test를 진행해보겠습니다 🧪

    @Test
    @DisplayName("Comment soft delete")
    public void delete() {
    
        Commnet comment =
        	Comment.builder()
                   .member(memberA)
                   .context(context)
                   .build();
                   
        Comment savedComment = commentRepository.save(comment); // 댓글 저장

        assertThat(savedComment.getId()).isNotNull();
        assertThat(savedComment.isDeleted()).isFalse(); // 삭제하기전 assertions 실행

        commentRepository.delete(savedComment);
        entityManager.flush(); // DB 반영

        Optional<Comment> afterDelete = commentRepository.findById(savedComment.getId());
        assertThat(afterDelete).isNotEmpty();
        assertThat(afterDelete.get().deletedAt()).isNotNull(); // soft delete 후 assertions 실행
      
    }
// 실제 날라가는 쿼리문
      UPDATE (DELETE x)
          comment 
      SET
          deletedAt = 현재 시간 
      WHERE
          id = ?

해당 코드를 통해 @SQLDelete 어노테이션 사용을 통해 실제 삭제할 때 날라가는 쿼리가 지정한 UPDATE 쿼리라는 것을 확인 할 수 있습니다.

🎒 @Where

우리는 soft delete 방식으로 삭제를 처리했기 때문에, @SQLDelete 를 통해 원하는 데이터를 삭제 처리 했지만, 해당 데이터는 아직 DB에 남아있습니다 🧑🏼‍💻
따라서 데이터 조회시 삭제 처리된 데이터는 조회가 되지 않게끔 조건을 설정해야합니다.
하지만, 조회 요청마다 조건을 걸어줘야하는데 이는 매우 귀찮은 일이며 조건 누락을 통해 실수를 유발할 수 있습니다.

@Where 어노테이션은 이러한 문제를 해결해줍니다.
해당 어노테이션이 있는 엔티티를 조회할때 기본적으로 날리고 싶은 조건 쿼리를 @Where 어노테이션에 설정할 수 있습니다 💪
soft delete 에서 @SQLDelete와 함께 자주 사용됩니다 ❗️

위에서 제시했던 Comment 엔티티 클래스를 조회하는 테스트를 해보겠습니다.

select 
	~~~~~~~~
from 
	comment comment0_
where
	comment0_.id = null

해당 코드는 조회 요청시 실제 날라가는 sql 코드입니다.
따로 조건을 설정하지 않았지만 @Where 에서 설정한 조건 쿼리가 자동으로 날라가는게 보이나요?
이렇게 @Where 어노테이션을 통해 삭제 처리되지 않은 데이터만 조회할 수 있습니다 👨‍💻

🎒 상속관계일때 Soft Delete

상속관계 매핑에 대해 알고싶으시다면 [JPA 프로그래밍] Entity Mapping 3편[상속관계 매핑] 참고 부탁드립니다.

엔티티가 상속관계에 놓여있는 경우(조인 전략) 부모 테이블 데이터를 삭제하는 경우 자식 테이블 데이터는 실제로 삭제됩니다 ❗️

// 부모 엔티티 클래스
@Inheritance(strategy = InheritanceType.JOINED) // 조인 전략을 사용하겠다.
@DiscriminatorColumn // 자식 테이블의 구분 칼럼(default = DTYPE)
@Entity
public class Item extends BaseEntity{ // 앞과 동일하게 deletedAt 칼럼을 사용할 수 있다.

	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private Long price;
    
}
// 자식 엔티티 클래스
@DiscriminatorValue("ALBUM")
@PrimaryKeyJoinColumn(name = "Album_ID")
@Entity
public class Album extends Item{

    private String artist;

}

위에 코드에서 볼 수 있듯이 Item 엔티티 클래스가 부모 클래스이고 Album 엔티티 클래스는 이를 상속하는 자식 클래스입니다.
이때 부모 클래스에 @SQlDelete(을 하게 되면 부모 클래스 데이터는 정상적으로 soft delete 되지만, 이와 관련된 자식 클래스 데이터는 물리적으로 삭제(hard delete)가 됩니다.
따라서 자식 클래스에 @OnDelete(action = OnDeleteAction.CASCADE) 처리를 해서 실제 삭제되는 것을 막아줘야 합니다 🧑🏼‍💻

[ @OnDelete(action = OnDeleteAction.CASCADE) 처리 후 코드 ]

// 부모 엔티티 클래스
@Inheritance(strategy = InheritanceType.JOINED) // 조인 전략을 사용하겠다.
@DiscriminatorColumn // 자식 테이블의 구분 칼럼(default = DTYPE)
@Where(clause = "DELETED_AT is null")
@SQLDelete(sql = "UPDATE COMMENT SET COMMENT.DELETED_AT = CURRENT_TIMESTAMP WHERE COMMENT.COMMENT_ID = ?")
@Entity
public class Item extends BaseEntity{ // 앞과 동일하게 deletedAt 칼럼을 사용할 수 있다.

	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    private Long price;
    
}
// 자식 엔티티 클래스
@DiscriminatorValue("ALBUM")
@PrimaryKeyJoinColumn(name = "Album_ID")
@OnDelete(action = OnDeleteAction.CASCADE) // 자식 클래스 처리
@Entity
public class Album extends Item{

    private String artist;

}

🎒 요약

[ Soft Delete ]

  • 장점
    • 삭제한 데이터를 복구할 수 있습니다.
    • 예전 기록 확인에 용이합니다.
  • 단점
    • 다른 테이블과 JOIN 시 항상 삭제 여부를 판단하는 조건이 필요합니다.
    • Soft Delete 를 사용하는 방법을 인지해야합니다.(ex 어노테이션 사용 방법)


참고

[JPA] soft delete 자동으로 처리하기
소프트 삭제, 하드 삭제

profile
비즈니스가치를추구하는개발자
post-custom-banner

0개의 댓글