스프링 데이터 JPA 환경에서 데이터를 삭제할 때에 보통 기본적으로 제공되는 deleteBy~~
메서드를 사용하면 될 것이다. 하지만 @Query
를 사용하여 직접 JPQL을 작성하는 것과 어떤 차이점이 있을까?
이 둘의 차이점을 알아보기 위해 간단한 예제 엔티티를 작성하였다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
String title;
}
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
쿼리를 날려야 하는 것은 매우 비효율적일 것이다.
위의 문제를 해결하기 위해 @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
쿼리를 날리지 않아도 된다.
만약 위의 상황에서 @Modifying
애너테이션을 빼먹으면 어떻게 될까?
InvalidDataAccessApiUsageException
이 발생한다. 이 예외가 발생한 이유는 (SELECT
를 제외한) INSERT
, UPDATE
, DELETE
쿼리를 @Query
에서 JPQL을 사용하여 작성할 경우에는 Hibernate에서 이 애너테이션을 붙이도록 강제하고 있기 때문이다.
@Modifying
애너테이션에는 두 가지 옵션이 있다. 다음은 이 옵션들에 대한 자세한 설명이다. 기본적으로 @Modifying
은 아래 두 옵션에 대해서 false
값을 가지고 있다.
flushAutomatically
clearAutomatically
@Query
에서 JPQL을 작성한 쿼리는 벌크성 쿼리이다. 벌크성 쿼리란 하나의 쿼리로 데이터베이스의 여러 레코드에 영향을 미치는 작업을 말한다. 이런 작업을 영속성 컨텍스트를 거치지 않고 바로 데이터베이스에 반영하다보니 이미 쓰기 지연 SQL 저장소
에서 대기하고 있는 쿼리들과 충돌이 발생할 수 있다.
@Modifying
애너테이션에서는 flushAutomatically
를 제공하는데, 쉽게 생각해서 이는 em.flush()
라고 생각하면 된다. 즉, 벌크성 쿼리를 데이터베이스에 반영하기 이전에 flush()
를 통해 쓰기 지연 SQL 저장소
에 있는 쿼리를 먼저 실행시켜주는 것이다. 그 이후에 벌크성 쿼리가 실행된다.
clearAutomatically
옵션 또한 em.clear()
라고 생각하면 된다. 즉, 벌크성 쿼리 실행 직후 em.clear()
를 호출하여 영속성 컨텍스트에 있는 엔티티들을 모두 비우는 것이다. 벌크성 쿼리는 데이터베이스에 바로 값을 반영하기 때문에 영속성 컨텍스트에 있는 기존의 값들은 변경 사항을 알 수 없다. 따라서 이후의 로직에서 변경된 사항을 반영한 엔티티를 사용하기 위해 clearAutomatically = true
를 사용하면 데이터베이스에서 새로운 엔티티를 가져올 것이기 때문에 데이터의 정합성에 문제가 생기지 않는다.
오늘의 주제는 회원 탈퇴 로직을 구현하다가 발생한 문제를 파악하다가 알게된 내용이었다. 회원 탈퇴 요청이 들어오면 해당 회원이 가지고 있던 모든 데이터를 삭제해주어야 했다.
이때 네임드 쿼리를 사용했더니 예상치 못한 다량의 SELECT
쿼리가 발생했고, 쿼리의 길이가 거의 두 배 이상 되어서 조금 더 깊게 알아보는 계기가 되었다. 이제 @Query
와 @Modifying
을 통해 쿼리를 반 정도 줄일 수 있게 되었다.
내가 구성한 회원 탈퇴 로직에서는 영속성 컨텍스트에 올라와 있는 엔티티를 가지고 값을 수정하는 등의 비즈니스 로직이 없고, 그저 데이터베이스에 있는 값들을 삭제만 해주면 됐기 때문에 flushAutomatically
나 clearAutomatically
옵션을 사용해주진 않았다.
마냥 쉬울 것이라고 생각했던 회원 탈퇴 기능이 제법 오래 걸려서 멘탈이 약간 흔들렸다. 중간중간에 @OneToOne
단방향으로 연결된 엔티티들에 대해 참조 무결성 예외가 터지지 않게 삭제하는 것이나, 조회 쿼리를 줄이는 부분, 그리고 이에 대한 테스트 케이스와 실제 환경에서 잘 동작하는지 파악하는 데에 시간을 많이 잡아 먹힌 것 같다.
그래도 문제가 무엇이었는지 파악하고, 쿼리를 조금 더 최적화 했다는 것이 기억에 남을 것 같다ㅏ..
오.. 이런게 있었군여 잘배우고 갑니다 bb