데이터 삭제

스프링 데이터 JPA에서 데이터를 삭제하는 방법

Chocomilk
Dong-gle
7 min readSep 30, 2023

--

1. 엔티티 삭제시 네임드 쿼리 vs @Query

1.1. 개요

스프링 데이터 JPA 환경에서 데이터를 삭제할 때에 보통 기본적으로 제공되는 deleteBy~~ 메서드를 사용하면 될 것이다. 하지만 @Query를 사용하여 직접 JPQL을 작성하는 것과 어떤 차이점이 있을까?

이 둘의 차이점을 알아보기 위해 간단한 예제 엔티티를 작성하였다.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

String title;
}

1.2. 네임드 쿼리

Book을 저장 후 삭제하는 테스트를 짜보겠다.

@Test
void deleteTest () {
//given
Book book = new Book("book");
bookRepository.save(book);
em.flush();
em.clear();

//when
bookRepository.delete(book);
em.flush();
em.clear();

//then
assertThat(bookRepository.findByTitle("book")).isEmpty();
}

쿼리를 확인해보면 다음과 같다.

--- #1
insert
into
book
(title,id)
values
('book',default)

--- #2
select
b1_0.id,
b1_0.title
from
book b1_0
where
b1_0.id=1

--- #3
delete
from
book
where
id=1

--- #4
select
b1_0.id,
b1_0.title
from
book b1_0
where
b1_0.title='book'

눈 여겨볼 점은 #2#3이다. em.flush()em.clear()쓰기 지연 SQL 저장소에 있는 쿼리들을 데이터베이스에 반영하고, 영속성 컨텍스트를 모두 삭제해 준 이후에, 기본적으로 제공되는 쿼리 메서드를 사용하여 삭제를 하였더니 일단 먼저 엔티티를 가져오고(#2) 삭제를 한다(#3).

기본적으로 Spring Data JPA에서 제공하는 네임드 쿼리를 사용하면, 데이터의 정합성을 위해 값들을 일단 모두 영속성 컨텍스트에 올리기 위해 SELECT문이 나가게 된다. 단건에서는 용납할 수 있을 정도이지만, 대량의 데이터를 삭제하는 상황에서 매 번 SELECT 이후에 DELETE 쿼리를 날려야 하는 것은 매우 비효율적일 것이다.

1.3. @Query + @Modifying

위의 문제를 해결하기 위해 @Query@Modifying 애너테이션을 사용하여 해결해보겠다.

@Query는 익히 알고 있듯이, JPQL을 직접 작성할 때에 사용하는 애너테이션이다. @Modifying은 영속성 컨텍스트를 거치지 않고, 바로 데이터베이스에 쿼리를 반영하도록 해주는 애너테이션이다. 따라서 위와 같은 상황을 이 두 애너테이션을 사용하여 BookRepository를 다음과 같이 구성할 수 있다.

public interface BookRepository extends JpaRepository<Book, Long> {
@Override
@Modifying
@Query("delete from Book b where b = :book")
void delete(@Param("book") Book book);
}

아래는 실행된 쿼리의 결과이다.

--- #1
insert
into
book
(title,id)
values
('book',default)

--- #2
delete
from
book
where
id=1

--- #3
select
b1_0.id,
b1_0.title
from
book b1_0
where
b1_0.title='book'

이제 전처럼 DELETE를 하기 위해 먼저 SELECT 쿼리를 날리지 않아도 된다.

2. @Modifying 심화

2.1. @Modifying을 빼먹으면?

만약 위의 상황에서 @Modifying 애너테이션을 빼먹으면 어떻게 될까?

InvalidDataAccessApiUsageException이 발생한다. 이 예외가 발생한 이유는 (SELECT를 제외한) INSERT, UPDATE, DELETE 쿼리를 @Query에서 JPQL을 사용하여 작성할 경우에는 Hibernate에서 이 애너테이션을 붙이도록 강제하고 있기 때문이다.

@Modifying 애너테이션에는 두 가지 옵션이 있다. 다음은 이 옵션들에 대한 자세한 설명이다. 기본적으로 @Modifying은 아래 두 옵션에 대해서 false 값을 가지고 있다.

  1. flushAutomatically
  2. clearAutomatically

2.2. flushAutomatically = true

@Query에서 JPQL을 작성한 쿼리는 벌크성 쿼리이다. 벌크성 쿼리란 하나의 쿼리로 데이터베이스의 여러 레코드에 영향을 미치는 작업을 말한다. 이런 작업을 영속성 컨텍스트를 거치지 않고 바로 데이터베이스에 반영하다보니 이미 쓰기 지연 SQL 저장소에서 대기하고 있는 쿼리들과 충돌이 발생할 수 있다.

@Modifying 애너테이션에서는 flushAutomatically를 제공하는데, 쉽게 생각해서 이는 em.flush()라고 생각하면 된다. 즉, 벌크성 쿼리를 데이터베이스에 반영하기 이전에 flush()를 통해 쓰기 지연 SQL 저장소에 있는 쿼리를 먼저 실행시켜주는 것이다. 그 이후에 벌크성 쿼리가 실행된다.

2.3. clearAutomatically = true

clearAutomatically 옵션 또한 em.clear()라고 생각하면 된다. 즉, 벌크성 쿼리 실행 직후 em.clear()를 호출하여 영속성 컨텍스트에 있는 엔티티들을 모두 비우는 것이다. 벌크성 쿼리는 데이터베이스에 바로 값을 반영하기 때문에 영속성 컨텍스트에 있는 기존의 값들은 변경 사항을 알 수 없다. 따라서 이후의 로직에서 변경된 사항을 반영한 엔티티를 사용하기 위해 clearAutomatically = true를 사용하면 데이터베이스에서 새로운 엔티티를 가져올 것이기 때문에 데이터의 정합성에 문제가 생기지 않는다.

3. 정리

오늘의 주제는 회원 탈퇴 로직을 구현하다가 발생한 문제를 파악하다가 알게된 내용이었다. 회원 탈퇴 요청이 들어오면 해당 회원이 가지고 있던 모든 데이터를 삭제해주어야 했다.

이때 네임드 쿼리를 사용했더니 예상치 못한 다량의 SELECT 쿼리가 발생했고, 쿼리의 길이가 거의 두 배 이상 되어서 조금 더 깊게 알아보는 계기가 되었다. 이제 @Query@Modifying을 통해 쿼리를 반 정도 줄일 수 있게 되었다.

내가 구성한 회원 탈퇴 로직에서는 영속성 컨텍스트에 올라와 있는 엔티티를 가지고 값을 수정하는 등의 비즈니스 로직이 없고, 그저 데이터베이스에 있는 값들을 삭제만 해주면 됐기 때문에 flushAutomaticallyclearAutomatically 옵션을 사용해주진 않았다.

마냥 쉬울 것이라고 생각했던 회원 탈퇴 기능이 제법 오래 걸려서 멘탈이 약간 흔들렸다. 중간중간에 @OneToOne 단방향으로 연결된 엔티티들에 대해 참조 무결성 예외가 터지지 않게 삭제하는 것이나, 조회 쿼리를 줄이는 부분, 그리고 이에 대한 테스트 케이스와 실제 환경에서 잘 동작하는지 파악하는 데에 시간을 많이 잡아 먹힌 것 같다.

그래도 문제가 무엇이었는지 파악하고, 쿼리를 조금 더 최적화 했다는 것이 기억에 남을 것 같다ㅏ..

4. 참고

--

--