Spring JPA, @Modifying 어노테이션
clearAutomatically, flushAutomatically
JPA를 사용하여 개발하다보면 DB의 데이터를 수정 또는 삭제하기 위해 벌크 연산을 사용해야하는 상황이 종종 발생한다.
이때 여러분들은 @Modifying 어노테이션을 붙여주어야 한다는 이야기를 들어봤을 것이다.
벌크연산이란?
하나의 데이터가 아닌 여러 데이터를 한번에 수정하거나 삭제하는 연산
위 사진과 같이 JpaRepository를 상속받은 Repository 인터페이스의 메서드 중,
벌크연산을 수행하는 메서드의 상단에
@Modifying 어노테이션을 붙여주어야 한다.
실제로 Update 또는 Delete 쿼리에 해당 어노테이션이 붙어있지 않는다면
QueryExecutionRequestException: Not supported for DML operations [UPDATE sopt.org.thirdSeminar.domain.User
위와 같은 오류가 발생한다.
그럼 @Modifying 어노테이션만 붙인다면 문제가 해결되는가?
맞긴하지만 그렇다고 완전 정답은 아니다!
@Modifying 어노테이션 내부로 들어가보면 2가지 속성이 존재한다
✨ clearAutomatically ✨
clearAutomatically 설정을 사용하게 되면,
@Modifying 어노테이션이 붙은 쿼리를 수행한 후 영속성 컨텍스트를 clear한다.
✨ flushAutomatically ✨
flushAutomatically 설정을 사용하게 되면,
@Modifying 어노테이션이 붙은 쿼리를 수행하기 전
쓰기지연 SQL 저장소에서 flush 대기 중인 쿼리를 모두 flush 한다.
이러한 속성을 대체 어느때 사용할까? 라는 의문이 들겠지만
제공되는 2가지 속성없이 어노테이션만 사용하게 되면
종종 나의 의도와는 다른 결과가 도출되는 문제에 직면하게 될 수 있다.
어떤 문제상황인지 아래 예시를 통해 알아보자!
🚨 clearAutomatically 미사용 시
clearAutomatically는 보통 UPDATE 연산에서 다음과 같은 문제를 발생시킨다.
JPA를 사용하는 환경에서
데이터를 조회한 뒤에, 업데이트 JPQL 구문을 통해 해당 데이터를 수정한다고 가정하자!
이때 수정한 데이터를 다시 조회했을 경우, 어떤 결과가 출력될까?
이상하게도 데이터는 수정하기 이전과 같은 결과로 출력될 것이다.
이에 대한 테스트 케이스를 작성하여 결과를 출력해보았다.
@Test
@Transactional
@DisplayName("clearAutomatically 오류 테스트")
void clearAutomaticallyTest() {
// nickname이 Pgmjun 인 유저 생성 후 INSERT
UserRequestDto request = new UserRequestDto("pgmjun@test.co.kr", "Pgmjun", "qwer1234@");
UserResponseDto response = userService.create(request);
// userId를 사용하여 유저정보 조회
UserResponseDto findUser = userService.findById(response.getUserId());
System.out.println("findUser.getNickname() = " + findUser.getNickname());
// nickname을 Pgmjun에서 Pgmjunn으로 UPDATE
UserRequestDto updateRequest = new UserRequestDto("pgmjun@test.co.kr", "Pgmjunn", "qwer1234@");
userService.updateNickname(updateRequest);
// userId를 사용하여 다시 유저정보 조회
UserResponseDto updatedUser = userService.findById(response.getUserId());
System.out.println("updatedUser.getNickname() = " + updatedUser.getNickname());
// 두 값이 모두 수정 전의 닉네임과 같은지 검증
Assertions.assertThat(findUser.getNickname()).isEqualTo(updatedUser.getNickname());
}
분명 update 쿼리를 통해 값을 수정했다!!!
근데 값은 변경되지 않았다..
"대체 왜 그럴까? 🤔"
JPA의 영속성 컨텍스트는, 한번 조회한 데이터들은 영속성 컨텍스트 내부의 1차 캐시라는 곳에 저장한다.
그리고 변경 감지라는 기술로 인해 엔티티의 값이 변경되면 1차 캐시에서도 변경되며
해당 변경사항 적용 쿼리가 영속성 컨텍스트 내의 쓰기지연 SQL 저장소에 저장되었다가 한번에 flush되어 DB로 전송되어 변경된다.
하지만 변경감지로 인한 UPDATE가 아닌, JpaRepository 내에서
@Query와 @Modifying 어노테이션을 통해 벌어지는 update 상황에선
영속성 컨텍스트를 거치지 않고 바로 DB로 변경 사항에 대한 sql구문이 전송된다.
"때문에 1차 캐시를 비우고 다시 조회하지 않으면
영속성 컨텍스트에 저장되어 있는 변경 이전의 값이 계속해서 출력되는 것이다 🔥"
이때 필요한 것이 @Modifying 어노테이션의
clearAutomatically=true 속성이다.
이 Attribute는 @Modifying 이 붙은 해당 쿼리 메서드 실행 직 후,
영속성 컨텍스트를 clear 할 것인지를 지정하는 Attribute로
이를 사용하면 벌크 연산 수행 후 1차 캐시를 비워주기 때문에,
벌크 연산 이후 변경한 데이터를 조회하면
DB에서 다시 조회하여 1차 캐시에 새로 저장한다.
때문에 변경사항 반영이 되는 것이다.
이를 테스트를 통해 직관적으로 알아보자
@Modifying(clearAutomatically = true)
@Query("UPDATE User u SET u.nickname = ?2 WHERE u.email = ?1")
void updateNickname(String email, String nickname);
아까와 같은 코드에서 @Modifying 어노테이션에 clearAutomatically 속성을 true로 변경하였다.
Assertions.assertThat(findUser.getNickname()).isNotEqualTo(updatedUser.getNickname());
그리고 테스트 케이스에서 isEqualTo였던 검증부를 isNotEqualTo로 변경했다.
결과는 어떨까?
결과는 다음과 같이 UPDATE된 닉네임이 호출되어 성공적이었다.
🚨 flushAutomatically 미사용 시
flushAutomatically는 이론상 DELETE 연산에서 다음과 같은 문제를 발생시킨다.
flushAutomatically는 @Query와 @Modifying을 통한 쿼리 메서드를 사용할 때,
해당 쿼리를 실행하기 전, 영속성 컨텍스트의 변경 사항을 DB에 flush할 것인지를 결정하는 Attribute 이다.
Default 값은 clearAutomatically와 마찬가지로 false인 것을 확인할 수 있었다.
만약 위와 같이 flushAutomatically 가 false인 상황에서
변경감지를 통해 A라는 테이블의 a컬럼 값을 영속성 컨텍스트 내에서 true -> false로 변경하고
@Modifying과 @Query 어노테이션을 통해 작성된 DELETE 쿼리로
테이블에서 a컬럼의 값이 false인 것들을 모두 삭제하라는 쿼리문을 날렸을 때, 어떤 결과를 예상하게 될까?
여러분은 clearAutomatically 예제를 통하여 영속성 컨텍스트의 동작에 대해 어느정도 학습했기 때문에
false로 변경된 내용이 아직 DB에 커밋되지 않고 영속성 컨텍스트 내에서만 변경이 되어 있어
실제로 a는 삭제되지 않고 남아있다는 결과를 예측할 수 있을 것이다.
테스트를 통해 직관적으로 결과를 확인해보자!
@Modifying(clearAutomatically = true)
@Query("DELETE FROM User u WHERE u.alive = false")
void deleteUserByAlive();
위와 같이 alive라는 컬럼의 상태가 false 라면 삭제하는 DELETE 쿼리가 존재한다.
확실한 테스트를 위해 DELETE 쿼리 수행 후,
영속성 컨텍스트에 남아있는 값을 삭제하고 다시 불러오기 위해
clearAutomatically 속성을 추가해주었다
해당 쿼리를 통해 아래와 같은 테스트를 작성해보았다.
@Test
@DisplayName("flushAutomatically 테스트")
void flushAutomaticallyTest() {
User user = User.builder()
.email("pgmjun@test.co.kr")
.nickname("Pgmjun")
.password("qwer1234@")
.alive(true)
.build();
userRepository.save(user);
// 변경감지를 통해 alive 상태를 false로 변경
user.changeAlive(false);
// alive 상태가 false 라면, DB 에서 삭제
userRepository.deleteUserByAlive();
// 조회 시, 삭제되지 않고 데이터가 잘 보존되어 있는지 테스트
Assertions.assertThat(userRepository.findById(user.getId()).isPresent()).isTrue();
}
예상대로라면,
변경감지에 의해 alive값이 영속성 컨텍스트 내에서만 false이므로
DB 내에서 alive값이 false인 데이터를 삭제하는 deleteUserByAlive() 메서드로
테스트에서 생성한 user 를 삭제할 수 없다.
어라..?!
하지만 실패했다..!
왜 이런 결과가 도출되는 걸까??🤔
이유는 다음과 같다.
사실 위에서 설명했던 flushAutomatically에 대한 설명은 맞는 설명이지만 이론적인 내용이다.
해당 속성값을 true로 하던 false로 하던간에 Jpa의 구현체인 Hibernate에서
flushAutomatically와 같은 역할을 하는 FlushModeType의 Default값이 True이기 때문에
어떤 상황에도 쿼리 실행전에 flush가 수행된다!
하지만 명시적으로라도 flushAutomatically의 값을 true로 설정해주어,
내부에서 어떻게 작동하는 지 이해하고 코드를 작성하는 습관을 들이자!
긴 글 읽어주셔서 감사합니다!
'BackEnd > Spring' 카테고리의 다른 글
LogBack을 통한 SpringBoot 로깅(Logging) 처리 (0) | 2023.07.25 |
---|---|
스프링부트 예외 발생 시, Slack으로 알림 보내기 (feat. Slack WebHooks) (2) | 2023.06.29 |
스프링부트에서 Response 시, JSON의 한글이 깨지는 오류 해결 (0) | 2023.03.22 |
JPA 영속성 관리 - 내부 동작 방식 (1) | 2023.03.20 |
React, SpringBoot 사이의 CORS 에러 해결 (3) | 2023.03.15 |