ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • @Modifying 옵션 알아보기 (flush, clear)
    개발/JPA & Hibernate 2021. 8. 14. 01:28

    JPA Spring Data 에서 벌크성 update, delete를 할 땐, @Modifying 애노테이션을 이용해야 합니다..

    @Modifying 에는 flushAutomatically, clearAutomatically 라는 두 가지 옵션이 있는데, 이에 대한 내용을 간단하게 정리해보겠습니다.

    @Modifying 옵션 살펴보기

    flushAutomatically

    해당 쿼리를 실행했을 때, flush()를 발생시킵니다. 요건 뒤에서 알아보겠습니다.

    지금 결론만 간단하게 말하자면, true든 false든 항상 true 처럼 동작되므로 무시해도 됌. (hibernate 설정 때문)

     

    clearAutomatically

    중요한건 이 옵션입니다.

    // 영속성 컨텍스트에만 존재
    Team save = teamRepository.save(new Team(1L)); 
    
    // 영속성 컨텍스트를 무시하고 DB에 있는 레코드들만 업데이트
    String newName = "new name";
    int updated = teamRepository.update(newName); 
    
    // 영속성 컨텍스트에만 있어서 무시당한 객체가 조회됌
    Team team = teamRepository.findById(1L).get();
    
    // 벌크 업데이트가 반영되지 않았음
    assertThat(team.getName()).isNotEqualTo(newName);

    위 처럼, 벌크 연산은 영속성 컨텍스트를 무시하고 바로 DB로 통하기 때문에, 영속성 컨텍스트에 존재하는 객체들은 변경이 전혀 반영되지 않습니다.

     

    이런 현상을 모르고 벌크 연산을 하면 예상치 못한 문제를 겪을 수 있습니다.

    이를 해결하기 위해선 다음과 같은 방법이 있습니다.

    1. 벌크 연산을 먼저 실행 후, 영속성 컨텍스트를 이용한다.
    1. 영속성 컨텍스트를 비운 후 벌크 연산을 한다. 영속성 컨텍스트가 비어있으니 벌크 연산 후 조회하면 무조건 DB에서 조회하게 된다.

    1번은 구현으로 해결할 부분이고, 2번의 경우 clearAutomatically 설정을 true로 설정하면 해결됩니다.

     

    flushAutomatically 부연 설명

    이 옵션은 쿼리를 실행하기 전에 write-behind(쓰기 지연) 쿼리들을 flush 해줄 것 인지 설정합니다.

    JPA의 영속성 컨텍스트는 실행할 쿼리들을 모아놨다가 한번에 전송하는 쓰기지연 기능이 있습니다.

    이 옵션이 false이면 DB 쿼리 순서가 이상해질 수 있습니다.

    예를들어, insert into Team (name) values('old')update Team set name = 'new' where id=1 순으로 쿼리를 작성했을 때, 우리가 기대하는 것은 당연히 Team을 삽입한 후, 이 팀의 이름이 'new' 로 변하길 기대할 것입니다.

     

    Data JPA(@Modifying)를 사용하지 않고 EntityManager를 이용해 Bulk update 코드를 한 줄 작성해보면

    em.createQuery("update Team t set t.name = 'new'").executeUpdate()

    위와 같이 createQuery().excuteUpdate() 를 사용하게 될 것입니다.

    그러나 executeUpdate() 는 영속성 컨텍스트를 무시하고 바로 데이터베이스에 쿼리를 보냅니다.

    그래서 이론대로라면 insert 쿼리는 아직 영속성 컨텍스트에 남아있고, 아무것도 없는 DB에 update 쿼리를 날리게 됩니다.

    (언젠가 insert 쿼리는 flush 될것이고, 최종적으로 Team의 name은 'old'가 됩니다)

     

    이론상으론 그런데, 실제로 해보면 다르다!?

    과연 진짜 그런지 확인해보겠습니다.

    // save - 영속성 컨텍스트에 저장됌. DB에는 아직X
    Team save = teamRepository.save(new Team()); 
    
    // bulk update - 영속성 컨텍스트를 거치지 않고 바로 DB 쿼리를 보냄.
    int updatedCount = 
    	em.createQuery("update Team t set t.name = 'new'").executeUpdate();
    
    // 영속성 컨텍스트에 저장한 'save'가 DB에는 없으므로, bulk update가 반영된 레코드가 0 이어야 함
    assertThat(updatedCount).isEqualTo(0); 
    
    // 그러나 FAILED !? 
    // updatedCount = 1 이 되어버렸다.
    Hibernate: 
        insert 
        into
            team
            (name, id) 
        values
            (?, ?)
    Hibernate: 
        update
            team 
        set
            name='new'

    로그를 보면 예상과 달리 update 전에 flush가 발생했습니다.

    그래서 영속성 컨텍스트에만 남아있던 team 객체가 DB에 insert 되었습니다.

     

    띠용. 제가 한 말대로라면 위의 테스트는 성공해야하지만, 실패합니다. 제가 뭔가 잘못 알고 있는걸까요?

    이유는 JPA의 구현체 Hibernate의 설정 때문입니다.

    무슨 이유인지는 모르겠지만, Hibernate 에서 FlushMode가 AUTO가 디폴트인 듯 합니다.

    AUTO는 특정 쿼리 실행시에 flush 하는 레벨입니다.

     

    // Hibernate 구현체에서 Session을 꺼낸다. 
    // Session은 Hibernate에서 엔티티 관리에 사용하는 API다.
    Session unwrap = em.unwrap(Session.class);
    unwrap.setHibernateFlushMode(FlushMode.COMMIT);

    위 처럼 Hibernate 설정을 COMMIT 시에만 flush하도록 바꾸면 bulk update 실행시에 flush가 발생하지 않습니다.

     

    hibernate 설정을 바꿈으로써 이제 우리는 '이론'대로 결과를 볼 수 있게 되었습니다.

    (하지만 딱히 저렇게 사용할 이유는 없어보이긴 합니다.)

     

     

    댓글

Designed by Tistory.