JPA flush() 자동 호출

헙크·2023년 11월 4일
1

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개의 댓글