Home JPA & Hibernate Flush Mode
Post
Cancel

JPA & Hibernate Flush Mode

Flush

플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는것을 의미합니다. 플러시는 다음과 같은 경우에 발생합니다.

  • EntityManager의 flush 메소드를 호출했을 때
  • 트랜잭션 커밋시
  • JPQL 쿼리 실행시

영속성 컨텍스트에 있는 엔티티를 지우고 db에 저장하는 개념이 아니라 영속성 컨텍스트의 변경 내용을 db와 동기화하는 것이 플러시 임을 잊지 마세요.

Flush Mode in JPA & Hibernate

JPA는 AUTO, COMMIT 2개의 플러시 모드를 지원하고 Hibernate는 AUTO, COMMIT, ALWAYS, MANUAL 4개의 플러시 모드를 지원합니다.

AUTO (JPA & Hibernate)

JPA 명세는 FlushModeType.AUTO 를 기본 타입으로 정의합니다. AUTO는 다음 2가지 상황에서 영속성 컨텍스트를 플러시 합니다.

  • 트랜잭션이 커밋되기 이전
  • 영속성 컨텍스트에 보류 중인 변경이 포함된 데이터베이스 테이블을 조회하는 쿼리를 실행하기 전

모든 JPQL 또는 Criteria Query에 대해 하이버네이트는 SQL문을 생성합니다. 따라서 어떤 데이터베이스 테이블이 사용되는지 미리 알수 있습니다. 하이버네이트는 그정보를 영속성 컨텍스트의 모든 엔티티 객체에 대해 더티 체크를 수행할 때 사용할 수 있습니다. 만약 쿼리에 의해 참조되는 테이블 중 하나에 매핑된 더티 엔티티가 발견되면 조회하기 전에 변경사항을 플러시해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "soccer_team")
public class SoccerTeam {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;

    String name;
}

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "soccer_team")
public class SoccerTeam {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Long id;

    String name;
}

@Test
void flushTest() {
    EntityManager em = emf.createEntityManager();
    em.getTransaction().begin();

    var soccerPlayer = new SoccerPlayer();
    soccerPlayer.setName("손흥민");
    em.persist(soccerPlayer);

    em.createQuery("SELECT t FROM SoccerTeam t").getResultList();

    Query q = em.createQuery("SELECT p FROM SoccerPlayer p WHERE p.name = :name");
    q.setParameter("name", "손흥민");

    q.getResultList();

    em.getTransaction().commit();
    em.close();
}

예제 코드입니다. SoccerTeamSoccerPlayer의 부모 엔티티 입니다. em.createQuery("SELECT t FROM SoccerTeam t").getResultList(); 쿼리를 수행하여도 SoccerTeam 엔티티는 SoccerPlayer 엔티티를 참조하지 않기 떄문에 insert 쿼리가 날라가지 않습니다.

마지막에서 세번째 q.getResultList(); 를 수행할때야 비로소 insert 쿼리가 데이터베이스로 날라가게 됩니다. 하이버네이트가 insert 구문의 수행 시간을 지연시킴으로서 성능상의 이점을 만들수도 있게되는 것입니다.

만약 네이티브 SQL 쿼리를 사용한다면 조금 더 복잡해집니다. 하이버네이트는 JPQL이 아닌 네이티브 SQL 쿼리가 사용하는 테이블을 판별할 수 없기 때문에, 각 네이티브 쿼리의 대상을 만들어야 합니다. 그렇지 않으면 영속성 컨텍스트를 플러시해야 하는지 여부를 결정할 수 없게 됩니다.

COMMIT (JPA & Hibernate)

플러시 타입 COMMIT은 트랜잭션을 커밋하기 전에 플러시가 필요하지만 쿼리를 실행하기 전에 수행해야할 작업을 정의하지 않습니다. 하이버네이트 5, 6 에서는 아무 쿼리나 수행하는 것만으로는 보류중인 수정사항을 플러시하지 않습니다. 간단히 말해 flush()를 직접 호출하거나 르랜잭션이 커밋되는 경우에만 플러시 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
void flushCOMMIT() {
    EntityManager em = emf.createEntityManager();
    em.setFlushMode(FlushModeType.COMMIT);
    em.getTransaction().begin();
    
    var soccerPlayer = new SoccerPlayer();
    soccerPlayer.setName("손흥민");
    em.persist(soccerPlayer);
    
    em.createQuery("SELECT f FROM SoccerTeam f").getResultList();
    
    Query q = em.createQuery("SELECT p FROM SoccerPlayer p WHERE p.name = :name");
    q.setParameter("name", "손흥민");
    
    q.getResultList();
    
    em.getTransaction().commit();
    em.close();
}

예제 코드입니다. AUTO 때의 예제코드에 em.setFlushMode(FlushModeType.COMMIT); 한줄을 추가하여 플러시 모드를 COMMIT 으로 설정했습니다. 예제를 실행시켜보면 위 AUTO때와 다르게 em.getTransaction().commit(); 코드가 실행될때 insert 쿼리가 데이터베이스에 날라가게 됩니다.

실제 프로덕션 레벨의 어플리케이션에서는 AUTO 보다는 COMMIT 모드를 권장합니다.

참고로 AUTO, COMMIT 모드의 경우 위의 예제처럼 ‘entityManager.setFlushMode(FlushModeType.COMMIT);’ 형식으로 모드를 지정할 수 있습니다.

ALWAYS (Hibernate)

ALWAYS 모드는 하이버네이트 명세에만 존재하기 때문에 만약 Spring Data JPA를 사용한다면 사용할수 없는 모드입니다. 이 모드는 하이버네이트에게 쿼리를 실행하기 전에 영속성 컨텍스트를 플러시하라고 지시합니다. 이 모드를 사용한다면, 하이버네이트는 플러시가 필요한지 여부를 확인하지 않고 모든 유형의 쿼리를 동일한 방식으로 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
void flushALWAYS() {
    EntityManager em = emf.createEntityManager();
    Session session = em.unwrap(Session.class);
    session.setHibernateFlushMode(FlushMode.ALWAYS);
    em.getTransaction().begin();

    var soccerPlayer = new SoccerPlayer();
    soccerPlayer.setName("손흥민");
    em.persist(soccerPlayer);

    Query q = em.createQuery("SELECT f FROM SoccerTeam f");
    q.getResultList();

    var insertedPlayer = em.createQuery("SELECT p FROM SoccerPlayer p").getResultList().stream().findFirst().orElse(null);

    Assert.notNull(insertedPlayer, "One soccer player should be inquired.");

    em.getTransaction().commit();
    em.close();
}

예제 코드입니다. SoccerTeam 조회 쿼리의 플러시 모드를 ALWAYS로 설정하였습니다. 예제를 실행시켜보면 SoccerTeamSoccerPlayer의 존재를 모름에도 불구하고 플러시 모드를 ALWAYS로 세팅하였기 때문에 select 쿼리가 날라가기 전에 insert 쿼리가 수행되는것을 확인할 수 있습니다. 테스트 또한 정상적으로 통과됩니다.

MANUAL (Hibernate)

마지막으로 MANUAL 모드입니다. MANUAL은 ALWAYS와 마찬가지로 하이버네이트 스펙에서만 지원하는 모드입니다. 이 모드는 모든 자동 플러시가 비활성화되고 개발자가 명시적으로 플러시 코드를 작성해야 플러시가 동작합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
void flushMANUAL() {
    EntityManager em = emf.createEntityManager();
    Session session = em.unwrap(Session.class);
    session.setHibernateFlushMode(FlushMode.MANUAL);
    em.getTransaction().begin();

    var soccerPlayer = new SoccerPlayer();
    soccerPlayer.setName("손흥민");
    session.save(soccerPlayer);

    session.flush();

    var insertedPlayer = em.createQuery("SELECT p FROM SoccerPlayer p").getResultList().stream().findFirst().orElse(null);

    Assert.notNull(insertedPlayer, "One soccer player should be inquired.");

    em.getTransaction().commit();
    em.close();
}

예제 코드입니다. SoccerPlayer 엔티티를 저장하는 코드인데요, 한번 실행시켜 본 후에 session.flush(); 코드를 제거하고 다시 실행시켜 보세요. 플러시를 명시적으로 작성하지 않았기 때문에 플러시가 일어나지 않아 테스트는 실패하게 됩니다.

결론

오늘은 JPA & Hibernate의 4가지 플러시 모드를 사펴보았습니다. 개인적으로는 Spring Data JPA가 MANUAL 모드를 지원하지 않아 조금 아쉽습니다. Spring Data JPA가 나오기 이전 Hibernate / Session 으로 이리저리 놀았던 때에는 비즈니스 로직에 따라 세세하게 설정할수 있었던 점이 좋았던것 같습니다.

그렇다고 이제와서 Spring Data JPA를 쓰지 않기엔 너무 편리한것도 사실입니다. 그런날이 올지는 모르겠지만 JPA 명세에 하이버네이트의 스펙이 추가되었으면 좋겠네요.

This post is licensed under CC BY 4.0 by the author.