TIL - #22 JPA, Delete

Quann·2023년 1월 13일
1

00. 개요

개인 프로젝트를 진행하면서, 유저를 삭제할 때 유저가 작성한 모든 게시물을 삭제해야하는 일이 생겼다.

게시글이 만 개면? 댓글이 만 개면? 이라는 생각과 함께 프로젝트를 진행하고 있었던 중이라,
게시글이 만 개일때 삭제 쿼리가 어떻게 나가는지 궁금해졌다.

삭제할 때, 쿼리가 만개가 나간다면 그것대로 성능 저하를 일으킬 것이고, 문제가 발생하는 것이기 때문이다.

그래서, 실험을 진행하고자 했다


01. 문제의 발견

@Override
@Transactional
public void deleteUsersPost(User user) {
	... (유저 검증 로직)
    postRepository.deleteByUser(user);
}

기존 유저가 작성한 게시물 삭제 로직이다.
Spring Data JPA에서 제공해주는 메서드명을 통한 삭제로 진행하고 있었다.
해당 로직이 어떤 쿼리를 날리는지 알아보고자 했다.

InitData.java

@Component
@RequiredArgsConstructor
public class InitData implements ApplicationRunner {

    private final UserRepository userRepository;
    private final PostRepository postRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        String pw = passwordEncoder.encode("temp");
        User user = new User("temp", pw, "temp", "temp", 20, UserRole.USER, "temp");
        userRepository.save(user);

        for (int i = 0; i < 300000; i++) {
            Post post = new Post("title" + i, "content" + i, user);
            postRepository.save(post);
        }

    }

}

실험을 위해 서버가 시작할 때 올라갈 30만 개의 게시글을 적용 시키고,

public interface PostRepository extends JpaRepository<Post, Long> {

    @Transactional
    @Modifying
    @Query("delete from Post p where p.user = ?1")
    void deleteByUserQuery(@NonNull User user);

    long deleteByUser(@NonNull User user);

}

다음 두개의 메서드를 선언해두었다.

deleteByUserQuery 는 직접 쿼리를 날리는 방식이고
deleteByUser 는 Spring Data JPA 에서 제공하는 메서드명을 통한 삭제 방식이다.

1.1 deleteByUser()

가장 먼저, 기존 작성했던 코드인 deleteByUser()를 통해 실험을 진행했다.

이 경우, 삭제 쿼리가 날라갈 때
select 문을 통해 삭제할 게시물을 먼저 불러오고,
delete 쿼리를 날리면서 게시물을 하나하나 삭제하는 것을 알 수 있었다.
여기서 문제가, delete 쿼리는 하나만 필요한테 모든 게시물에 대해 동일한 쿼리를 날리다보니 성능저하 문제가 발생했다.

다음과 같이 27.36s 동안의 시간이 경과되었고, 매우 느린 것을 알 수 있다.

쿼리도 확인하면,

다음과 같이 계속해서 같은 쿼리를 날리고 있다는 것을 알 수 있다.

내가 원하는 것은, 기존 sql문을 통해 전부 삭제하듯이,
delete from posts where user_id= 1 처럼 단 하나만 날라가는 것이다.
즉, 내가 필요한 쿼리는 단 하나의 쿼리인데, 스프링 측에서는 내가 삭제할 모든 게시물에 대해 하나씩 쿼리문을 날리면서 성능을 저하시키고 있었다.

deleteByUserQuery()

따라서, 해당 쿼리로 게시글을 삭제하는 메서드를 통한 실험을 진행했다.

삭제할 게시물을 먼저 조회해오는 것은 똑같지만, 해당 메서드는 단 하나만의 쿼리를 날린다.
내가 원했던 delete form posts where user_id=1 딱 하나만 날리는 것이다!

성능 측면에서도, 앞서 27.36s(27360ms) 보다 훨씬 빠른 286ms 가 찍힌 것을 확인할 수 있다.

deleteAllInBatch()

이 외에도, Spring Data JPA 가 제공해주는 deleteAllInBatch 삭제 기능이 존재한다.
해당 기능은, in query를 사용해 통째로 삭제해주는 것인데

@Override
@Transactional
public void deletePost(Long postId, User user) {
List<Post> posts = postRepository.findByUser(user);
        postRepository.deleteAllByIdInBatch(posts);
}

다음과 같이 작성될 수 있다.

하지만, 30만개의 데이터로 해당 기능을 실행하는 경우, StackOverFlow가 발생한다.

관련 링크: https://stackoverflow.com/questions/29644814/spring-jpa-deleteinbatch-causes-stackoverflow

정상 동작시, 쿼리는 다음과 같이 in 쿼리가 나가는 것을 확인할 수 있다!

SOF의 발생 이유는, 대충 해석해보자면
JPARepository에서 제공해주는 기능인 deleteAllInBatch에서 In Query를 작성할때 사용되는 HqlSqlBaseWalker 에서 in 쿼리 작성을 위한 모든 요소를 검색하다보니 JVM 측에서 계속된 메서드 호출로 인해 Stack 공간 부족으로 SOF를 뱉어낸다는 것이다.


02. 결론

관련 링크: https://jojoldu.tistory.com/235

해당 내용을 살펴보면, 다량의 객체 삭제시, 내부적으로 execute Query를 진행할 때 for 문을 통해 하나하나 객체가 삭제된다는 사실을 알 수 있다.

그래서, Spring Data JPA에서 제공해주는 삭제 기능을 사용할 경우 하나씩 삭제하는 쿼리가 나가게 된 것이고,
내가 원하는 쿼리는 단 하나이기 때문에, 중복해서 날리는 것이 아닌 직접 jpql 쿼리문을 날리도록 설정해두었다.


02. 오늘의 한 문단

계속해서 생각하고, 실험하자

profile
코드 중심보다는 느낀점과 생각, 흐름, 가치관을 중심으로 업로드합니다!

2개의 댓글

comment-user-thumbnail
2023년 1월 16일

퍼가요

1개의 답글