BackEnd/JPA

[JPA] Soft Delete 자동 처리하기

샤아이인 2022. 11. 9.

이번 프로젝트를 진행하면서 1급 컬랙션을 통한 Soft Delete를 처리하면서 기록을 남겨본다!

 

1. Soft Delete 란?

우선 데이터를 삭제하는 방식에는 hard delete, soft delete 2가지 있습니다.

 

hard delete는 delete 쿼리를 날려서 데이터베이스에서 실제로 삭제하는 방식이고,

soft delete는 실제로 데이터베이스에서 데이터를 삭제하는 것이 아니라, 테이블에 deleted와 같은 필드를 추가해주고, update 쿼리를 통해서 deleted 값을 변경해주는 방식입니다.

 

soft delete를 한 경우,

조회시 sofe delete 처리된 값이 함께 반환되면 안되기 때문에 "where deleted = false"같은 조건을 추가하여 sofe delete되지 않은 데이터만 필터링 하는 작업이 필요합니다.

 

이럴때 사용하기 위해 JPA의 구현체인 하이버네이트에는 아래 2가지 기능이 있습니다.


1) 삭제시 delete 쿼리 대신 다른 구문 실행 : 

@SQLDelete


2) 특정 엔티티를 조회하는 쿼리에 where 조건을 추가해주는 기능

이를 이용하면 soft delete처리와 삭제되지 않은 데이터 조회를 편리하게 할 수 있습니다.

@Where

 

2. 예시 상황

2 - 1) Hard Delete 사용시 쿼리

우선 PostPhotos는 PostPhoto을 단일 컬랙션으로 가지고 있는 1급 컬랙션 입니다.

 

▶ PostPhoto

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostPhoto {

    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "uuid")
    private String id;

    @Column(nullable = false)
    private String restaurantId;

    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
    @JoinColumn(name = "post_id")
    private Post post;

    // 일부 생략...

    private Boolean deleted = Boolean.FALSE;
}

 

▶ PostPhotos

@Embeddable
@NoArgsConstructor
public class PostPhotos {

    private static final int MINIMUM_PHOTO_COUNT = 1;

    @OneToMany(mappedBy = "post", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
    private List<PostPhoto> photos = new ArrayList<>();
    
    // 생략...
}

 

우선 PostPhotos를 보면 부모와 life cycle을 함께 하기 위해서 CacadeType 옵션과 orphanReoval = true 를 주었습니다.

마지막으로 이를 사용하는 Post는 다음과 같습니다.

 

▶ Post

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Post extends BaseTimeEntity {

    private static final int LIMIT_LENGTH = 50;
    
    // 일부 생략

    @Embedded
    private PostPhotos photos = new PostPhotos();
}

 

이제 테스트 코드를 살펴볼까요? 다음과 같습니다.

@Transactional
@Rollback(value = false)
@Test
public void hard_delete_test() {
    // given
    Post post = new Post("ownerId", "restaurantId", 4.2f, "contents");
    PostPhoto newPhoto1 = new PostPhoto(RESTAURANT_ID, "file1", "uuid1", ".jpg");
    PostPhoto newPhoto2 = new PostPhoto(RESTAURANT_ID, "file2", "uuid2", ".jpg");
    PostPhoto newPhoto3 = new PostPhoto(RESTAURANT_ID, "file3", "uuid3", ".jpg");
    post.addPhotoList(List.of(newPhoto1, newPhoto2, newPhoto3));

    postRepository.save(post);

    em.flush();
    em.clear();

    // when
    Post findPost = postRepository.findById(post.getId()).get();
    findPost.deletePhoto(newPhoto1);

    // then
    assertThat(findPost.getPhotos()).extracting("uuidFileUrl").contains("uuid2", "uuid3");
    assertThat(findPost.getPhotos().size()).isEqualTo(2);
}

우선 부모격에 해당되는 Post만 영속화 시키고, flush 해도

다음과 같이 자식들도 함께 저장되는 것을 확인할 수 있습니다.

 

실행시 쿼리는 다음과 같이 나오게 됩니다.

 

이후 삭제에 관한 쿼리는 다음과 같이 나옵니다.

 

테이블에서 살펴봐도 1번은 삭제된것을 알 수 있다.

 

2 - 2) Soft Delete 사용시 쿼리

이번에는 다음과 같이 에노테이션을 추가해줍시다!

@Entity
@SQLDelete(sql = "update post_photo set deleted = true where id = ?")
@Where(clause = "deleted = false")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostPhoto {
    // 생략...
}

삭제 로직이 수행되면 delete 쿼리 대신, update 쿼리를 통해 deleted 필드 값만 true로 변경시켜줘야합니다.

따라서 @SQLDelete를 사용하였습니다!

 

또한, soft delete 처리를 하는 경우 조회 요청시 삭제처리되지 않은 데이터만 가져와야합니다.

따라서 @Where을 이용할 수 있습니다.

 

이후 동일한 테스트를 실행해보면 직전에 살펴본것 처럼 Post 1개, PostPhoto 3개 를 저장하는 쿼리가 나온 후,

다음과 같은 update 문을 통하여 SofeDelete 하게 됩니다!

 

테이블을 보면 다음과 같이 true로 변경된것을 확인할 수 있습니다!

 

3. 출처

https://www.baeldung.com/spring-jpa-soft-delete

 

댓글