데이터를 삭제할 때 DB에서 완전히 삭제되는 방식은 사실 프로젝트를 운영해 나갈 때 좋은 방식이 아니다.
누군가 데이터를 실수로 삭제했을 때 복구하려면 DB에 어떤 방식이든 데이터를 남겨둬야 했다.
그렇기에 데이터를 복구하는 방법을 찾아보니, soft delete라는 방식을 통해 데이터 삭제를 하는 방법이 있었다.
soft delete란?
데이터를 삭제할 때 두 가지 방식 중 하나다. hard delete, soft delete가 있다.
hard delete : 실제로 db에 delete쿼리를 날려 데이터를 삭제하는 것.
soft delete : 실제로 삭제하는 것이 아니라, 테이블에 deleted_check 같은 필드를 추가해서 그 데이터가 삭제처리 되면 삭제가 아니라 deleted_check만 바꿔주는 방식이다.
예로 게시글 하나가 삭제되지 않았다면 deleted는 0이고, 삭제 메시지가 서버에 전달되면 실제로는 delete 쿼리가 아니라 update쿼리를 통해 deleted필드값을 1로 바꾸는 것이다.
soft delete를 실제로 구현하려면 delete쿼리가 실행되려 할 때마다 update쿼리로 변경해서 보내는 작업이 필요한데, 이는 친절하게 어노테이션으로 지원된다.
soft delete 구현
다음은 board 엔티티이다.
board.java
package com.inProject.in.domain.Board.entity;
import com.inProject.in.Global.BaseEntity;
import com.inProject.in.domain.Board.Dto.RequestUpdateBoardDto;
import com.inProject.in.domain.Comment.entity.Comment;
import com.inProject.in.domain.MToNRelation.ApplicantBoardRelation.entity.ApplicantBoardRelation;
import com.inProject.in.domain.MToNRelation.ClipBoardRelation.entity.ClipBoardRelation;
import com.inProject.in.domain.MToNRelation.RoleBoardRelation.entity.RoleBoardRelation;
import com.inProject.in.domain.Board.Dto.RequestBoardDto;
import com.inProject.in.domain.MToNRelation.TagBoardRelation.entity.TagBoardRelation;
import com.inProject.in.domain.User.entity.User;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Builder
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "board") //테이블과 매핑
@SQLDelete(sql = "UPDATE board SET deleted = true WHERE id = ?")
@Where(clause = "deleted = false")
public class Board extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
private Long id;
@Column(nullable = false)
private String type;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String text;
@Column(nullable = false)
private String proceed_method; //진행 방식
@Column(nullable = false)
private LocalDateTime period; //예상 기간
@Column
private int comment_cnt; //댓글 개수
@Column
private boolean deleted = Boolean.FALSE;
@ManyToOne
@JoinColumn(name = "user_id")
private User author; //작성자 정보 접근
@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
@ToString.Exclude
private List<ApplicantBoardRelation> applicantBoardRelationList; //게시글에 지원서를 제출한 유저에 대한 정보
@OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
@ToString.Exclude
private List<Comment> commentList = new ArrayList<>(); //게시글에 작성된 댓글들
@OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
@ToString.Exclude
private List<TagBoardRelation> tagBoardRelationList; //태그
@OneToMany(mappedBy = "clipedBoard", cascade = CascadeType.REMOVE)
@ToString.Exclude
private List<ClipBoardRelation> clipBoardRelationList; //관심 클립으로 지정한 유저들
@OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
@ToString.Exclude
private List<RoleBoardRelation> roleBoardRelationList; //직군
public void updateBoard(RequestUpdateBoardDto requestUpdateBoardDto){
this.type = requestUpdateBoardDto.getType();
this.title = requestUpdateBoardDto.getTitle();
this.text = requestUpdateBoardDto.getText();
this.proceed_method = requestUpdateBoardDto.getProceed_method();
this.period = requestUpdateBoardDto.getPeriod();
// this.tagBoardRelationList = requestBoardDto.getTagBoardRelationList();
// this.roleBoardRelationList = requestBoardDto.getRoleBoardRelationList();
}
}
@SQLDelete(sql = "UPDATE board SET deleted = true WHERE id = ?")
@Where(clause = "deleted = false")
엔티티 이름 위에 SQLDelete와 Where 어노테이션이 soft delete를 구현하는 핵심이다.
SQLDelete는 delete쿼리가 해당 엔티티를 대상으로 실행되면 대신 설정한 다른 쿼리를 실행하도록 한다.
Where은 해당 테이블을 쿼리할 때 마다, 기본적으로 적용할 where 절을 설정한다. 즉 board를 쿼리할 때 마다 where에 deleted = false가 붙는다. 이 의미는 delete가 false인 board만 찾는 것으로, 삭제 처리가 되지 않은 board들만 가져가기 위함이다.
soft delete가 실제로 적용되는지 알아보기 위해 코드를 실행해본다. 다음은 현재 board 테이블에 들어있는 데이터들이다.

현재 6개의 게시글 중 2개의 게시글이 deleted되어있다. 이제 3번 board를 삭제해본다. swagger를 이용해 api를 사용했다.

성공적으로 삭제가 완료되었다고 응답받았다. 실제로는 어떻게 처리되었을까?

delete가 아닌 update 쿼리 로그가 나타난다. 의도대로 SQLDelete가 동작했다.

3번 board의 deleted 필드가 0에서 1로 바뀐 모습이다. soft delete되었다.
이제 게시글들을 출력하는 getBoardList를 실행해서 삭제 처리된 글들이 출력되지 않는지 확인해본다.
[
{
"board_id": 7,
"username": "cbvfff",
"type": "스터디",
"title": "스터디",
"proceed_method": "비대면",
"period": "2023-09-06T00:00:00",
"comment_cnt": 0,
"tags": [
"spring",
"javascript"
],
"roles": [
{
"role_id": 6,
"name": "PM",
"pre_cnt": 0,
"want_cnt": 2
},
{
"role_id": 4,
"name": "모바일",
"pre_cnt": 0,
"want_cnt": 1
},
{
"role_id": 3,
"name": "designer",
"pre_cnt": 0,
"want_cnt": 2
},
{
"role_id": 1,
"name": "frontend",
"pre_cnt": 0,
"want_cnt": 3
},
{
"role_id": 2,
"name": "backend",
"pre_cnt": 0,
"want_cnt": 2
}
]
},
{
"board_id": 6,
"username": "cbvfff",
"type": "스터디",
"title": "fgdfgdfgf",
"proceed_method": "sdfdsf",
"period": "2023-09-09T00:00:00",
"comment_cnt": 3,
"tags": [
"spring"
],
"roles": [
{
"role_id": 6,
"name": "PM",
"pre_cnt": 0,
"want_cnt": 2
},
{
"role_id": 4,
"name": "모바일",
"pre_cnt": 0,
"want_cnt": 1
},
{
"role_id": 3,
"name": "designer",
"pre_cnt": 0,
"want_cnt": 1
},
{
"role_id": 1,
"name": "frontend",
"pre_cnt": 0,
"want_cnt": 3
},
{
"role_id": 2,
"name": "backend",
"pre_cnt": 0,
"want_cnt": 2
}
]
},
{
"board_id": 5,
"username": "cbvfff",
"type": "string",
"title": "string",
"proceed_method": "string",
"period": "2023-09-02T14:06:24.663",
"comment_cnt": 0,
"tags": [],
"roles": []
}
]
긴 응답 내용이지만, board_id만 봐도 무방하다. board는 총 3개가 응답으로 왔는데, 7, 6, 5 게시글이 응답으로 왔다. 7, 6, 5 게시글은 deleted필드가 0, 즉 삭제 처리가 되지 않은 글들이다. 삭제 처리된 글들은 찾지 않은 모습이다.

쿼리 로그에도 where절에 deleted필드를 체크하는 부분이 추가되어있다. 성공적으로 삭제처리한 게시글들을 가져오지 않게 되었다.
'Backend > spring boot' 카테고리의 다른 글
spring boot : entity <-> dto 변환 방법에 대한 고민( MapStruct 에 대해) (0) | 2024.10.09 |
---|---|
spring boot jpa : hibernate delete 쿼리가 실행되지 않는 현상 (0) | 2023.08.31 |
spring boot에서 DTO를 엔티티로 변환할 때 연관관계에 있는 엔티티와 연결하기 (0) | 2023.08.15 |
외부에서 spring boot 서버에 접속 해보기(접속 안 될 때 이슈, sk 브로드밴드 포트포워딩) (0) | 2023.08.11 |
spring boot 사이드 프로젝트 : service 테스트 (0) | 2023.07.12 |