1. @Modifying 이란?
@Modifying 어노테이션은 @Query 어노테이션으로 작성된 수정, 삭제 쿼리 메소드를 사용할 때 필요하다.
즉, 조회 쿼리를 제외하고 데이터에 변경이 일어나는 INSERT, UPDATE, DELETE 쿼리에서 사용한다.
@Modifying 어노테이션은 @Query 와 같이 사용되며 벌크 연산을 하고 싶을 때 사용한다.
ex. 나이가 20살인 모든 회원의 주류 구매 가능 여부 업데이트
UPDATE SET isAvailiablePurchase WHERE age >= 20;
@Modifying 의 내부 옵션을 보면 flushAutomatically() 와 clearAutomatically() 가 존재하는 것을 알 수 있다.
flushAutomatically() 옵션
해당 옵션은 @Query 와 @Modifying 을 통한 쿼리 메서드를 사용할 때, 해당 쿼리를 실행하기전 영속성 컨텍스트의 변경 사항을 DB 에 flush 할 것인지를 결정한다. Default 값은 false 이다.
그렇다면 flushAutomatically() 옵션을 true 로 설정하지 않고 default 설정(false)으로 실행하면 쓰기지연저장소에 있는 쿼리들이 flush 되지 않는 걸까?
예상과 달리 항상 쿼리 실행시 flush 된다.
Spring Data JPA 의 구현체인 hibernate 의 FlushModeType enum class 의 기본 설정 때문에 flush 된다.
(참조: https://docs.jboss.org/hibernate/jpa/2.1/api/javax/persistence/FlushModeType.html)
기본 옵션이 AUTO 로 되어있기 때문에 flushAutomatically 가 false 여도 쿼리 실행 전에 flush 가 되게 된다.
1. insert 2. update 3. delete |
순서로 쿼리를 작성했을 때 1 -> 2 -> 3 순서대로 쿼리가 실행된다.
하지만 Hibernate 를 직접 사용하는 방향으로 코드를 구성하고 COMMIT 옵션을 사용하게 되면 3 -> 1 -> 2 순서로 쿼리가 실행된다.
해당 옵션은 application.properties 또는 yaml 파일에서 설정할 수 있다.
spring.jpa.properties.org.hibernate.flushMode=COMMIT
따라서 기본 설정 (AUTO) 일 때 @Modifying 에서 flushAutomatically() 옵션은 true 건, flase 건 아무 의미가 없다.
(참조: https://devhyogeon.tistory.com/5 -> 해당 블로그에서 자세한 테스트를 다루고 있습니다.)
clearAutomatically() 옵션
clearAutomatically() 옵션을 true 로 사용하게 되면 쿼리 실행전에 영속성 컨텍스트를 비우게 된다. 따라서 후에 변경된 객체를 조회할 때 DB 에 직접 접근하여 영속성 컨텍스트를 갱신한다.
만약에 이 옵션을 true 로 주지 않게 되면 JPA 의 동일성 보장 원칙 때문에 기존의 영속성 컨텍스트, 1차 캐시에 있는 엔티티를 사용하게된다. 따라서 데이터 변경된 객체를 조회할 수 없게된다. 이 동일성 보장 원칙으로 인해 예상치 못한 결과를 얻게될 수도 있으니 해당 옵션은 true 로 사용해 주는 것이 좋다.
2. @Query + @Modifying
JPA 에서 select 쿼리(JPQL)는 @Modifying 없이 @Query 로만 작성해도 되지만 만약 DML (insert, update, delete) 쿼리를 사용하게 된다면 반드시 @Modifying 어노테이션을 함께 사용해야한다.
만약 @Modifying 을 선언하지 않으면 QueryExecutionRequestException 에러가 발생한다.
@Query("delete from Account a where a.username = :username")
int deleteByUsername(String username);
따라서 @Query 어노테이션 선언을 통해 DML 문을 실행할 경우 반드시 @Modifying 어노테이션을 선언하는 것을 강제하고 있음을 알 수 있다.
그렇다면 반대로 @Modifying 은 선언하고 @Query 를 선언하지 않은 JPA 메서드를 정의하면 어떻게 될까?
다음은 예시로 Board 라는 테이블에 레코드를 insert 하고 update 하고 delete 하는 로직이다.
Board board = Board.of("heedoitdox", "description");
var savedBoard = boardRepository.save(board); // insert
Optional<Board> boardOptional = boardRepository.findById(savedBoard.getId());
var newAuthor = "changed_author";
boardOptional.get().changeAuthor(newAuthor); // update
int delete = boardRepository.deleteByAuthor(newAuthor); // delete
위 로직에서 deleteByAuthor 메서드는 아래와 같이 선언되어있다.
@Modifying
int deleteByAuthor(String username);
추측해보면 아래의 쿼리가 실행 할 것 같지만
delete from board where author = "";
실제 호출되는 쿼리는 아래처럼 select 쿼리로 먼저 삭제할 객체들을 구하고 후에 구한 객체의 id 로 삭제하는 쿼리를 실행한다.
위 설명에서 @Modifying 어노테이션은 @Query 와 같이 사용되며 벌크 연산을 하고 싶을 때 사용한다. 라고 얘기한 것이 바로 위와 같은 이유 때문이다. 만약 지워야할 레코드의 개수가 많아진다면 (delete where 조건에 존재하는 author 가 여러 건이라면) 실행되는 쿼리는 삭제할 레코드 만큼 발생하게 된다.
따라서 @Modifying 만 사용한다고 해서 벌크연산이 사용되는 것이 아니며 반드시 @Query 어노테이션으로 벌크연산할 쿼리를 명시해 주어야 한다.
많은 쿼리가 발생할 수 있다는 것 점 외에도 또 유의할 점이 있다.
만약에 author 가 unique key 가 됐다고 하자.
create unique index board_uk_01
on board (author);
그리고 Board 테이블에는 이미 author 가 heedoitdox 인 데이터가 존재한다.
그리고 다음과 같은 로직으로 테스트를 실행해보자.
@Modifying(flushAutomatically = true)
int deleteByAuthor(String author);
(위에서 flushAutomatically = true 는 default 값 (false) 와 차이가 없다고 했지만 확실히 하기위해 붙여서 테스트해봄)
@Test
@Rollback(value = false)
void delete() {
var author = "heedoitdox";
Optional<Board> boardOptional = boardRepository.findByAuthor(author);
if (boardOptional.isPresent()) {
int delete = boardRepository.deleteByAuthor(author);
}
Board board = Board.of("heedoitdox", "description");
var savedBoard = boardRepository.save(board);
}
author 가 heedoitdox 인 레코드를 찾아서 존재한다면 삭제한 후 같은 author 가 heedoitdox 인 레코드를 삽입하는 로직이다.
여기서 hibernate 동작과정을 잘 모른다면 위 로직이 의도한대로 잘 동작할 것 이라 착각할 수 있다.
하지만 결과는 아래와 같다.
1. findByAuthor 조회 쿼리 실행 2. deleteByAuthor 를 하기위한 조회 쿼리 실행 3. insert 쿼리 실행 4. duplicate key SqlException 발생 |
1,2 번 쿼리가 실행 된 후 우리는 delete 쿼리가 먼저 실행될 것 이라 예상했지만 3번 과정에서 insert 쿼리가 먼저 발생하고 그로인해 unique key 가 중복되어 duplicate key SqlException 이 발생했다.
이는 hibernate performExecutions 구동 방식에서 쿼리 실행 순서를 정해놓았기 때문이다.
따라서 DML 쿼리가 명시적으로 flush 되지 않으면 위와 같은 이슈를 겪을 수 있다.
@Modifying 과 @Query 를 함께 사용해주면 flush 되어서 기대한 결과대로 동작하고 duplicate key 에러 역시 발생하지 않는 것을 볼 수 있다.
@Modifying
@Query("delete from Board b where b.author = :author")
int deleteByAuthor(String author);
또한, 더티체킹의 경우에도 로직의 순서가 아닌 performExecutions 의 순서대로 쿼리가 실행되기 때문에 예상 외의 결과가 나올 수 있다.
var author = "heedoitdox";
Optional<Board> boardOptional = boardRepository.findByAuthor(author);
boardOptional.get().changeAuthor("changed_author");
Board board = Board.of("heedoitdox", "description");
var savedBoard = boardRepository.save(board);
1. author 가 heedoitdox 인 데이터 조회 (select) 2. 조회한 데이터의 author 를 변경 (update) 3. author 가 heedoitdox 인 레코드 생성 (insert) |
하지만 이 때 xxxRepository.flush() 를 사용 해 주면 기대한 대로 결과를 얻을 수 있다.
var author = "heedoitdox";
Optional<Board> boardOptional = boardRepository.findByAuthor(author);
boardOptional.get().changeAuthor("changed_author");
boardRepository.flush(); // 명시적인 flush
Board board = Board.of("heedoitdox", "description");
var savedBoard = boardRepository.save(board);
3. 정리
위에서 언급한 내용들을 정리해보자.
1. spring data jpa 의 구현체인 hibernate 의 FlushModeType 기본 설정은 AUTO 이기 때문에 @Modifying 에서의 flush 옵션이 ture 든 false 든 @Modifying + @Query 를 사용하면 무조건 flush 되도록 동작한다.
2. @Query 없이 @Modifying 을 사용하면 기대한대로 동작하지 않는다.
- 무조건 조회쿼리가 우선적으로 발생하고 그 뒤에 단건 쿼리(update, delete) 가 발생한다. 벌크연산이 작동하지 않는다.
3. 삭제, 갱신, 삽입 쿼리가 모두 존재하는 로직에서 DML 쿼리 실행 이전에 flush 하지 않는다면 hibernate 에서는
1) insert, 2) update, 3) delete 순서대로 쿼리가 실행된다.
위에서 예시로 들었던 duplicate key 이슈는 실제로 회사에서 발생한 이슈였다. 기존 기능에서 새로운 요구사항이 추가되면서 테이블에 unique key 를 생성하게 되었는데 마침 API 중에 존재하면 삭제하고 다시 새로 삽입하는 로직이 있었다. 해당 API 는 deprecated 될 예정이라 신경쓰지 않고 있었는데(내가 개발한 소스가 아니어서 몰랐던 것도 있다ㅠㅠ) 이전 버전 클라이언트에서 이 API 를 사용하게 되면서 duplicate key exception 이 마구 발생하여 소스코드를 보게되었다. 그러던중 jpa delete 메서드가 딱 위 예시와 같은 상황이었는데,
@Modifying(flushAutomatically = true)
int deleteByAuthor(String author);
그때는 @Modifying(flushAutomatically = true) 면 무조건 flush 되어야 하는 것 아니야? 라고 알고 있어서 도무지 이슈를 파악하기가 어려웠다. 항상 벌크연산을 의도하지않아도 @Modifying + @Query 어노테이션을 선언하고 flushAutomatically = true 옵션을 주는 것이 습관이었기 때문에 벌크연산을 하지 않는 JPA delete 메서드가 어떻게 동작하는지 모르고 있었다. 이 이슈를 계기로 해당 어노테이션에 대해서 깊게 리서치 하게 되었고 알고나니 허탈했다. 조금만 서치해도 나오는 개념들인데... 그리고 처음 JPA 를 배울 때 공부했던 기억이 스멀스멀 난다. 꺼진 불도 다시 보자!
'Spring > Spring' 카테고리의 다른 글
[Spring Boot 3.2 마이그레이션] @TransactionalEventListener 사용시 트랜잭션을 분리하고 싶다면 반드시 전파속성을 명시해라! (3) | 2024.03.23 |
---|---|
[Spring Boot 3.2 마이그레이션] hibernate @Type, @TypeDef deprecated (0) | 2024.03.22 |
[SpringCache] 쪼금의 개념 설명과 Caffeine maximumSize 옵션 테스트 (1) | 2023.11.01 |
[15] 기본적인 웹 게시물 관리 - 화면 처리 2 (0) | 2019.12.20 |
[14] 기본적인 웹 게시물 관리 - 화면 처리 (0) | 2019.12.19 |