Spring Data JPA 벌크 연산 사용(soft delete)

10000DOO·2023년 3월 26일
1

YouHaveTo

목록 보기
11/12


이번 프로젝트를 사용하면서 데이터를 삭제하는 경우 soft delete 방법을 사용했다. soft delete를 사용하면서 데이터 삭제와 수정 시 벌크 연산을 많이 사용하게 됐다. 이번에는 어떻게 벌크 연산을 사용했고 어떤 어려운 점이 있었는지 정리해 보려고 한다.

📌 Soft Detele VS Hard Delete

📌 Soft Detele

논리 삭제(soft delete) : 데이터를 삭제하지 않고, 삭제 표시를 한 뒤에 복구할 수 있는 상태로 만드는 방법이다.

📌 Hard Detele

물리 삭제(hard delete) : 데이터를 완전히 삭제하는 방법이다. 데이터베이스에서 해당 데이터를 영구적으로 삭제하므로 복구할 수 없다.

📌 장단점

논리 삭제와 물리 삭제는 각각 장단점이 있다. 논리 삭제는 데이터를 복구할 수 있어 실수로 데이터를 삭제해도 안전하다는 장점이 있지만, 삭제된 데이터가 여전히 데이터베이스에 남아있기 때문에 용량이 증가할 수 있다. 또한, 데이터를 복구하기 위해 일정 기간 동안 데이터를 보관해야 하는 경우가 있다.
반면, 물리 삭제는 데이터베이스 용량을 절약할 수 있으며, 데이터를 완전히 삭제하기 때문에 보안에 더 강력하다. 그러나 실수로 데이터를 삭제할 경우 데이터를 복구할 수 없다는 단점이 있다.

📌 벌크 연산

벌크 연산은 한 번의 쿼리로 여러 개의 데이터를 삭제 or 수정해야 될 때 사용한다

프로젝트에서 두가지 경우 벌크 연산을 사용했다.
1. 외래 키 관계를 끊어줄 때
2. 데이터를 삭제 표시할 때

📌 1. 외래 키 관계를 끊어줄 때

이번 프로젝트에서는 soft delete를 사용했지만 저장 공간을 무한히 사용할 수 없기 때문에 
삭제된 지 30일 뒤에는 데이터가 물리적으로 지워지도록 했다.
따라서 데이터가 삭제될 때 먼저 외래 키 관계를 끊고 삭제해야 됐다.
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query(value = "update Comment c set c.commentWriter = null where c.commentWriter = :member")
void updateCommentWriterToNull(@Param("member") Member member);
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query(value = "update Post p set p.postWriter = null where p.postWriter = :member")
void updatePostWriterToNull(@Param("member") Member member);

📌 2. 데이터를 삭제 표시할 때

데이터를 삭제할 때 데이터의 삭제 일을 표시하도록 했다.
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query(value = "UPDATE Diary d SET d.dateTime.canceledAt = :canceledAt WHERE d.member = :member")
void deleteByMemberId(@Param("canceledAt") String canceledAt, @Param("member") Member member);
    
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query(value = "UPDATE Comment c SET c.dateTime.canceledAt = :canceledAt WHERE c.post.id = :postId and c.dateTime.canceledAt = null")
void deleteCommentByPostId(String canceledAt, Long postId);

📌 주의점

벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.

권장하는 방안
1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행한다.
2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.

🥲 문제 상황

위의 주의점을 생각해서 @Modifying()에 clearAutomatically = true옵션을 추가해서
@Modifying(clearAutomatically = true) 이렇게 적어줬다.
clearAutomatically @Modifying이 적용된 쿼리 메서드를 실행한 후, 영속성 컨텍스트를 clear 할 것인지를 지정하는 속성이다.

게시글을 삭제하는 과정에서 해당 게시글에 작성된 댓글의 삭제 시간을 업데이트하여 
삭제 처리하고 게시글 자신의 삭제 시간 또한 업데이트해야 되는데 게시글의 삭제 시간이 제대로 반영되지 않았다.
다음은 게시글 삭제 기능을 구현한 코드이다.
public ResponseResult<Long> deletePost(Long postId) throws IOException {
        Post postById = postRepository.findById(postId)
                .orElseThrow(() -> new EntityNotFoundException(ErrorCode.NOT_FOUND_EXCEPTION_POST.getMessage()));

        String localTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));

        postById.getDateTime().canceledAtUpdate();
        commentRepository.deleteCommentByPostId(localTime, postById.getId());
        mediaService.deletePostImage(postById.getId());
        likeCountRepository.deleteAllByPost(postById);

        log.info("username : {}, {}번 게시글 삭제 완료", SecurityUtil.getCurrentUsername(), postById.getId());
        return new ResponseResult<>(HttpStatus.OK.value(), postById.getId());
}
//commentRepository.deleteCommentByPostId(localTime, postById.getId());

@Modifying(clearAutomatically = true)
@Query(value = "UPDATE Comment c SET c.dateTime.canceledAt = :canceledAt WHERE c.post.id = :postId and c.dateTime.canceledAt = null")
void deleteCommentByPostId(String canceledAt, Long postId);

//mediaService.deletePostImage(postById.getId());
// 동일하게 게시글에 포함된 이미지 파일을 삭제하는 메서드 이다.

//likeCountRepository.deleteAllByPost(postById);
@Modifying(clearAutomatically = true)
@Query("delete from LikeCount l where l.post = :post")
void deleteAllByPost(Post post);

🧐 원인

원인은 @Modifying(clearAutomatically = true) 이것이었다.
postById.getDateTime().canceledAtUpdate() 메서드가 Post 엔티티의 canceledAt을 변경한다.
이 변경 사항은 트랜잭션이 종료될 때 commit 되면서 반영된다.
(JPA Entity 수정에 대한 설명) https://velog.io/@10000doo/JPA-Entity-수정
하지만 clearAutomatically 옵션 때문에 변경 사항이 트랜잭션 종료 시점까지 1차 캐시에 남아있지 못해서 반영되지 않은 것이었다.

정리
1. post의 canceledAt을 업데이트 하면 1차 캐쉬에 반영
2. 벌크 연산(@Modifying)을 하면 1차 캐쉬에 관계없이 바로 DB에 값 반영
3. clearAutomatically = true에 의해 1차 캐시 클리어 시킴
4. 그래서 1번 사항이 1차 캐시에만 반영 되어있다가 clear돼서 DB에 반영이 안 됨
5. 따라서 comment는 canceledAt update가 되는데 post는 안 됐던 것

🎉 해결방안

@Modifying(clearAutomatically = true) 여기에 flushAutomatically = true 옵션을 추가해준다.
이렇게 되면 1차 캐시를 clear 하기전에 DB에 flush하여 반영시키고 1차 캐시를 지우기 때문에 더티체킹이 동작하게된다. 따라서 게시글 삭제처리가 정상적으로 동작하게 된다.

📚 참고 자료

https://www.inflearn.com/course/스프링-데이터-JPA-실전/dashboard
https://wildeveloperetrain.tistory.com/142

profile
iOS 개발자 지망생 https://github.com/10000DOO

0개의 댓글