안녕하세요 오늘은 데이터를 삭제하는 방법중 Soft Delete 에 대해 알아보겠습니다 👨💻
보통 데이터베이스 삭제라 함은, 실제 데이터베이스에 존재하는 데이터를 삭제하는 것을 의미합니다.
그러나, 이번 포스팅에서 다룰 'Soft Delete' 는 실제 물리 데이터를 삭제하는 것이 아니라 테이블에 삭제여부와 관련된
칼럼을 추가하여 update 쿼리를 날려서 삭제와 관련된 칼럼 값을 변경시키므로써, 삭제되었음을 표시합니다.
실제 현업에서는 Hard Delete 보다 Soft Delete 를 더 많이 사용한다고 합니다 🧑🏼💻
그렇다면 왜 물리적인 데이터를 바로 삭제하지 않고 Soft Delete를 사용할까요?
실제 개발시 다양한 데이터를 다루게 됩니다.
데이터를 CRUD 연산을 통해 자유자재로 다루는 것은 개발자에게는 필수적인 역량입니다 👼
만약 데이터를 삭제(Hard Delete)한다고 생각해볼까요?
데이터를 삭제 하는 순간 해당 데이터는 더 이상 존재하지 않게 됨으로 사용하지 못합니다.
하지만 현업에서는 어떠한 부득이한 상황으로 인해 이젠 삭제한 데이터를 복구해야하는 사례가 있을 수 있습니다.
혹은 예전 기록을 확인하고자 삭제한 데이터에 대한 기록이 필요할 수도 있습니다 ❗️
이처럼 현업에 맞춘 삭제 방법은 Soft Delete 에 더 가깝습니다.
우리는 개발을 하면서 어떠한 일이 발생할지 모르는거니깐요! 만일의 상황에 대비해 혹은 개발의 효율성을 위해 Soft Delete 를 사용하는 것을 권장합니다 💪
📣 Soft Delete가 어떤 방식인지 이해했으니 Spring JPA 에서 어떻게 Soft Delete를 구현할 수 있는지 알아볼까요?
해당 어노테이션을 통해 해당 엔티티를 삭제할때 일괄적으로 설정한 쿼리를 날릴 수 있습니다.
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 쿼리라는 것을 확인 할 수 있습니다.
우리는 soft delete 방식으로 삭제를 처리했기 때문에, @SQLDelete 를 통해 원하는 데이터를 삭제 처리 했지만, 해당 데이터는 아직 DB에 남아있습니다 🧑🏼💻
따라서 데이터 조회시 삭제 처리된 데이터는 조회가 되지 않게끔 조건을 설정해야합니다.
하지만, 조회 요청마다 조건을 걸어줘야하는데 이는 매우 귀찮은 일이며 조건 누락을 통해 실수를 유발할 수 있습니다.
@Where 어노테이션은 이러한 문제를 해결해줍니다.
해당 어노테이션이 있는 엔티티를 조회할때 기본적으로 날리고 싶은 조건 쿼리를 @Where 어노테이션에 설정할 수 있습니다 💪
soft delete 에서 @SQLDelete와 함께 자주 사용됩니다 ❗️
위에서 제시했던 Comment 엔티티 클래스를 조회하는 테스트를 해보겠습니다.
select
~~~~~~~~
from
comment comment0_
where
comment0_.id = null
해당 코드는 조회 요청시 실제 날라가는 sql 코드입니다.
따로 조건을 설정하지 않았지만 @Where 에서 설정한 조건 쿼리가 자동으로 날라가는게 보이나요?
이렇게 @Where 어노테이션을 통해 삭제 처리되지 않은 데이터만 조회할 수 있습니다 👨💻
상속관계 매핑에 대해 알고싶으시다면 [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;
}