JPA flush() 자동 호출

unknown·2023년 11월 4일

0. 결론과 개요

결론

  1. 타입이 같은 엔티티에 대해 JPQL을 호출하면 flush()가 자동으로 호출된다.
  2. 타입이 다른 엔티티에 대해 JPQL을 발생시키면 flush()가 자동으로 호출되지 않는다.
  3. 조회가 아닌 JPQL 쿼리는 @Modifying을 사용해야만 하고, 1, 2번과 동일하게 작동한다.
    (다만, @Modifying(flushAutomatically = true) 옵션을 통해 flush() 자동 호출이 가능하다)

개요

JPA에서 flush() 자동 호출은 언제 일어날까? 내가 알고 있던 것은 다음과 같은 세 가지 상황이었다.

  1. 트랜잭션 커밋 시 호출
  2. JPQL 발생시 호출
  3. flush()를 수동으로 호출

이전에는 그저 JPQL 쿼리를 발생시키기 전에 SQL 쓰기 지연 저장소에 있는 변경 사항들을 데이터베이스에 맞춰주어 정합성을 맞춰주어야 정상적인 결과를 가져올 수 있기 때문에, flush()가 무조건 발생한다~ 라고 알고 있었다.

그런데 모든 JPQL에 대해서 flush()를 날려줄까?라는 의문이 들었다. A 엔티티를 1차 캐시에 올려 놓고, 수정 후…

  1. B 엔티티에 대한 JPQL을 호출해도?!
  2. 조회 JPQL이 아닌 (@Modifying을 사용한) 수정, 삭제여도?!

각 상황을 한 번 확인해보자!

1. flush()가 자동 호출되는 경우

1.1. 기본 코드

간단하게 Member 예제 코드를 작성하였다. 엔티티와 레포지토리를 작성하였다.

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

    private String name;

    public Member(String name) {
        this.name = name;
    }

    public void updateName(String name) {
        this.name = name;
    }
}

public interface MemberRepository extends JpaRepository<Member, Long> {

	@Query("select m from Member m where m.id = 1")
    void myQuery();
}

1.2. 테스트 코드

💡 참고
@DataJpaTest가 내부에 @Transactional을 가지고 있기 때문에, 테스트의 마지막에 rollback을 해버린다. 따라서 “==================” 아래에 아무런 쿼리가 나가지 않는 것이 정상일 것이다. 이후에는 예제를 간단히 하기 위해서 @Test 부분만 나타내겠다. 이제 테스트 코드를 살펴보자.

@DataJpaTest
class MemberTest {
    @Autowired private MemberRepository memberRepository;
    @Autowired private EntityManager em;

    @Test
    void flushTest() {
        Member member = memberRepository.save(new Member("member"));

        em.flush();
        em.clear();

        Member findMember = memberRepository.findById(member.getId()).get();
        findMember.updateName("update");
        memberRepository.myQuery();
        System.out.println("==================");
				
    }
}

1.3. 실행 결과

아래는 실행 결과이다. 간단하게 표시 부분 아래에 발생한 쿼리만 나타내었다.

==================
update
    member 
set
    name=? 
where
    id=?
---
select
    m1_0.id,
    m1_0.name 
from
    member m1_0 
where
    m1_0.id=1

Member를 1차 캐시에 올려놓고, 수정하고, Member에 대한 JQPL 조회 쿼리를 발생시키면 JPQL 쿼리를 날리기 전에 flush()하는 것을 볼 수 있다.

2. 다른 엔티티의 JPQL을 호출해도!?

2.1. 기본 코드

이번엔 추가로 Team 엔티티와 레포지토리를 작성하였다.

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

    private String name;

    public Team(String name) {
        this.name = name;
    }
}

public interface TeamRepository extends JpaRepository<Team, Long> {

    @Query("select t from Team t")
    List<Team> myQuery();
}

2.2. 테스트 코드

만약 Member를 1차 캐시에 올린 후, 수정하고, Team에 대한 JPQL을 호출하면 어떻게 될까?

@Test
void flushTest() {
    Member member = memberRepository.save(new Member("member"));

    em.flush();
    em.clear();

    Member findMember = memberRepository.findById(member.getId()).get();
    findMember.updateName("update");
    // memberRepository.myQuery();
    teamRepository.myQuery();
    System.out.println("==================");
}

2.3. 실행 결과

실행 결과는 다음과 같다. 이 상황에서는 flush()가 자동으로 호출되지 않음을 확인할 수 있다.

==================
select
    t1_0.id,
    t1_0.name 
from
    team t1_0

3. 조회가 아닌 JPQL을 호출해도?!

3.1. 기본 코드

조회가 아닌 쿼리를 JPQL로 작성한다는 것은 곧, INSERT, UPDATE, DELETE 쿼리를 작성한다는 것이다. 이를 사용하려면 @Modifying을 활용해야 할 것이다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Member m where m.id = 1")
    void myQuery();

    @Modifying
    @Query("update Member m set m.name = 'hubcreator'")
    void memberBulkQuery();
}

3.2. 테스트 코드

테스트 케이스를 작성해보자.

@Test
void flushTest() {
    Member member = memberRepository.save(new Member("member"));

    em.flush();
    em.clear();

    Member findMember = memberRepository.findById(member.getId()).orElseThrow();
    findMember.updateName("update");
    memberRepository.memberBulkQuery();
    System.out.println("==================");
}

3.3. 실행 결과1

쿼리를 확인해보면 “==================” 호출 이전에 flush()가 자동으로 호출됨을 확인할 수 있다. 이전에 설명했듯이 @ModifyingflushAutomatically 속성의 기본값이 false임에도 불구하고, 1차 캐시에 같은 타입의 엔티티가 있으면 자동으로 flush()가 호출된 것을 확인할 수 있다.

update
    member 
set
    name=? 
where
    id=?
---
update
    member 
set
    name='hubcreator'
==================

3.4. 실행 결과2

그렇다면 다른 타입의 엔티티를 호출하면?!

public interface TeamRepository extends JpaRepository<Team, Long> {

    @Query("select t from Team t")
    List<Team> myQuery();

    @Modifying
    @Query("update Team t set t.name = 'hubcreatorTeam'")
    void teamBulkQuery();
}
@Test
void flushTest() {
    Member member = memberRepository.save(new Member("member"));

    em.flush();
    em.clear();

    Member findMember = memberRepository.findById(member.getId()).orElseThrow();
    findMember.updateName("update");
    teamRepository.teamBulkQuery();
    System.out.println("==================");
}

실행 결과는 다음과 같다. Team 엔티티에 대한 UPDATE 쿼리가 발생했고, Member에 대한 flush()가 발생하지 않았음을 확인할 수 있다. 이런 경우에야말로, @Modifying(flushAutomatically = true)를 사용해야할 때인 것이다.

update
    team 
set
    name='hubcreatorTeam'
==================

4. 결론

이로써 모든 상황에서 JPQL을 호출한다고 flush()가 자동으로 호출되지 않음을 알 수 있었다. 앞으로 데이터의 정합성에 조금 더 신경쓸 수(!?!?) 있게 되었다 ㅎㅎㅎ.

5. 참고

https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#_code_auto_code_flush_on_jpql_hql_query

0개의 댓글