JPA 플러쉬(flush)

4 분 소요


RECOMMEND POSTS BEFORE THIS

1. What is flush?

플러쉬(flush) 작업은 무엇일까? 정의를 살펴보자.

영속성 컨텍스트(persistence context)에서 관리하는 엔티티(entity)의 변경 내용을 데이터베이스에 동기화하는 작업이다.

JPA는 영속성 컨텍스트에서 관리 중인 엔티티의 모습을 즉시 데이터베이스에 반영하지 않는다. 1차 캐시로서 기능을 수행하면서 필요하다고 판단되는 시점에 변경 쿼리를 수행한다. JPA에서 플러쉬(flush)가 작동하는 시점은 다음과 같다.

  • 엔티티 매니저(entity manager)의 flush 메소드를 명시적으로 호출
  • 엔티티 매니저의 트랜잭션(transaction) 객체의 commit 메소드 호출
  • FlushModeType.AUTO 설정인 경우 JPQL(Java Persistence Query Language) 쿼리(Query) 실행 시점

flush, commit 메소드처럼 명시적인 호출은 이해하기 쉽다. 기능을 제대로 파악하지 못 해서 발생하는 실수를 줄이기 위해 세 번째 조건에 대한 내용을 좀 더 자세히 살펴보자.

2. FlushModeType Options and JPQL(Java Persistence Query Language)

FlushModeType 설정은 Flush 기능이 수행되는 시점을 결정하는 옵션이다. FlushModeType 옵션은 플러쉬 기능이 수행되는 시점을 결정한다. 다음과 같은 옵션이 있다.

  • FlushModeType.AUTO
    • 디폴트 값으로 커밋(commit) 또는 JPQL 쿼리 실행 시 플러쉬 기능을 수행한다.
  • FlushModeType.COMMIT
    • 커밋 시점에만 flush 기능을 수행한다. 해당 옵션은 성능 최적화를 위해 필요한 경우에만 사용한다.

JPQL은 JPA에서 지원하는 객체 지향적 쿼리 문법을 의미한다. 테이블을 기준으로 질의문을 작성하는 것이 아닌 엔티티 객체를 기준으로 작성한다. 때문에 일반적인 SQL과 다른 문법적 차이점들을 지닌다.

3. Practices

JPQL은 영속성 컨텍스트의 지연 쓰기처럼 최대한 실행을 늦추지 않고 즉시 수행한다. JPQL 쿼리가 영속성 컨텍스트에 담긴 엔티티들의 모습을 고려하지 않고 데이터베이스를 직접 변경하게 되면 데이터의 최종적인 모습이 순서에 맞지 않게 변경되면서 원치 않은 모습을 갖게 될 수 있다. 예를 들면 다음과 같은 상황을 들 수 있다.

  1. 먼저 영속성 컨텍스트에 관리 중인 엔티티에 오염(dirty)이 발생했다.
  2. JPQL 쿼리가 이를 고려하지 않고 데이터베이스를 업데이트 했다.
  3. 트랜잭션 마지막에 커밋이 발생하면서 영속성 컨텍스트의 오염된 엔티티가 오염 확인(dirty checking)에 의해 업데이트 된다.

순서대로라면 JPQL 업데이트가 데이터베이스에 담긴 데이터의 최종 모습이어야 하지만, 결과는 먼저 발생한 오염된 엔티티의 모습이 반영된다. 이런 현상을 방지하기 위해 영속성 컨텍스트에서 관리 중인 엔티티들의 변경을 플러쉬 수행으로 먼저 데이터베이스에 동기화한다. 테스트 코드를 통해 다음과 같은 내용을 확인해봤다.

  • FlushModeType.AUTO 옵션 사용 시 JPQL 쿼리 사용 전에 플러쉬 되는가?
  • FlushModeType.COMMIT 옵션 사용 시 JPQL 쿼리 사용 전에 플러쉬 되는가?

3.1. FlushModeType.AUTO Option

FlushModeType.AUTO 옵션을 사용할 때 동작을 살펴본다.

  1. 이름이 Junhyun Kang인 사용자를 추가한다.
    • 해당 사용자의 최초 연락처는 이메일 한 개이다.
  2. 사용자 정보를 변경한다.
    • 이름은 Junhyun으로 변경한다.
    • 휴대폰 번호를 추가한다.
  3. JPQL 업데이트 쿼리를 수행한다.
    • 조회 조건은 이름이 Junhyun인 사용자이다.
    • 연락처를 다른 휴대폰 번호로 업데이트한다.
  4. JPQL 업데이트 쿼리가 정상적으로 수행되어 변경한 데이터는 1건으로 예상한다.
  5. clear 메소드로 영속성 컨텍스트를 비우고 재조회한다.
    • 데이터베이스를 직접 변경했기 때문에 영속성 컨텍스트의 엔티티와 데이터베이스의 데이터 모습이 다르다.
    • 영속성 컨텍스트에 해당되는 엔티티가 없어야 엔티티 매니저는 데이터베이스에 변경된 최신 데이터를 재조회한다.
    • 조회된 사용자의 연락처는 한 개이다.
    • 연락처는 JPQL 업데이트에 의해 변경된 휴대폰 번호이다.
package blog.in.action.flush;

import blog.in.action.entity.Member;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import javax.persistence.*;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;

@Log4j2
@DataJpaTest
public class AutoOptionTest {

    @PersistenceUnit
    private EntityManagerFactory factory;

    void transaction(Consumer<EntityManager> consumer) {
        EntityManager entityManager = factory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            consumer.accept(entityManager);
        } catch (Exception ex) {
            throw ex;
        } finally {
            transaction.rollback();
            entityManager.close();
        }
    }

    @Test
    void how_to_work_when_auto_option() {
        transaction(entityManager -> {
            entityManager.setFlushMode(FlushModeType.AUTO);
            Member member = Member.builder()
                .id("Junhyunny")
                .name("Junhyun Kang")
                .contacts(Arrays.stream(new String[]{"kang3966@naver.com"}).collect(Collectors.toList()))
                .build();
            entityManager.persist(member);
            member.setName("Junhyun");
            member.appendContact("010-1234-1234");

            log.info("===== BEFORE JPQL =====");
            String jpqlQuery = "update Member m set m.contacts = '010-3214-3214' where m.name = 'Junhyun'";
            int resultCnt = entityManager.createQuery(jpqlQuery).executeUpdate();
            log.info("===== AFTER JPQL =====");

            entityManager.clear();
            Member result = entityManager.find(Member.class, "Junhyunny");
            List<String> contacts = result.getContacts();
            assertThat(resultCnt, equalTo(1));
            assertThat(contacts.size(), equalTo(1));
            assertThat(contacts.get(0), equalTo("010-3214-3214"));
        });
    }
}

다음과 같은 수행 결과를 얻는다.

  • JPQL 업데이트 쿼리 수행 전에 영속성 컨텍스트의 엔티티의 모습이 데이터베이스에 반영된다.
    • 추가(insert), 변경(update) 쿼리가 수행된다.
  • 변경된 이름인 Junhyun에 해당하는 데이터의 연락처를 변경한다.
2023-01-28 00:42:09.847  INFO 24300 --- [           main] blog.in.action.flush.AutoOptionTest      : ===== BEFORE JPQL =====
Hibernate: insert into tb_member (contacts, name, id) values (?, ?, ?)
Hibernate: update tb_member set contacts=?, name=? where id=?
Hibernate: update tb_member set contacts='010-3214-3214' where name='Junhyun'
2023-01-28 00:42:09.963  INFO 24300 --- [           main] blog.in.action.flush.AutoOptionTest      : ===== AFTER JPQL =====
Hibernate: select member0_.id as id1_0_0_, member0_.contacts as contacts2_0_0_, member0_.name as name3_0_0_ from tb_member member0_ where member0_.id=?

3.2. FlushModeType.COMMIT Option

FlushModeType.COMMIT 옵션을 사용할 때 동작을 살펴본다.

  1. 이름이 Junhyun Kang인 사용자를 추가한다.
    • 해당 사용자의 최초 연락처는 이메일 한 개이다.
  2. 사용자 정보를 변경한다.
    • 이름은 Junhyun으로 변경한다.
    • 휴대폰 번호를 추가한다.
  3. JPQL 업데이트 쿼리를 수행한다.
    • 조회 조건은 이름이 Junhyun인 사용자이다.
    • 연락처를 다른 휴대폰 번호로 업데이트한다.
  4. JPQL 업데이트 쿼리가 정상적으로 수행되지 않아 변경한 데이터는 0건으로 예상한다.
  5. 사용자를 조회한다.
    • 조회된 사용자의 연락처는 두 개이다.
    • 초기에 설정한 이메일과 다음에 추가한 휴대폰 번호이다.
package blog.in.action.flush;

import blog.in.action.entity.Member;
import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import javax.persistence.*;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;

@Log4j2
@DataJpaTest
public class CommitOptionTest {

    @PersistenceUnit
    private EntityManagerFactory factory;

    void transaction(Consumer<EntityManager> consumer) {
        EntityManager entityManager = factory.createEntityManager();
        EntityTransaction transaction = entityManager.getTransaction();
        transaction.begin();
        try {
            consumer.accept(entityManager);
        } catch (Exception ex) {
            throw ex;
        } finally {
            transaction.rollback();
            entityManager.close();
        }
    }

    @Test
    void how_to_work_when_commit_option() {
        transaction(entityManager -> {
            entityManager.setFlushMode(FlushModeType.COMMIT);
            Member member = Member.builder()
                .id("Junhyunny")
                .name("Junhyun Kang")
                .contacts(Arrays.stream(new String[]{"kang3966@naver.com"}).collect(Collectors.toList()))
                .build();
            entityManager.persist(member);
            member.setName("Junhyun");
            member.appendContact("010-1234-1234");

            log.info("===== BEFORE JPQL =====");
            String jpqlQuery = "update Member m set m.contacts = '010-3214-3214' where m.name = 'Junhyun'";
            int resultCnt = entityManager.createQuery(jpqlQuery).executeUpdate();
            log.info("===== AFTER JPQL =====");

            Member result = entityManager.find(Member.class, "Junhyunny");
            List<String> contacts = result.getContacts();
            assertThat(resultCnt, equalTo(0));
            assertThat(contacts.size(), equalTo(2));
            assertThat(contacts.get(0), equalTo("kang3966@naver.com"));
            assertThat(contacts.get(1), equalTo("010-1234-1234"));
        });
    }
}

다음과 같은 수행 결과를 얻는다.

  • JPQL 업데이트 쿼리 수행 전에 영속성 컨텍스트의 엔티티의 모습이 데이터베이스에 반영되지 않는다.
    • 추가, 변경 쿼리가 수행되지 않는다.
  • 변경된 이름인 Junhyun에 해당하는 데이터를 변경하지 못 한다.
    • JPQL 업데이트 쿼리를 수행하는 시점에 데이터베이스엔 조건에 해당되는 데이터가 없다.
2023-01-28 00:53:13.667  INFO 10436 --- [           main] blog.in.action.flush.CommitOptionTest    : ===== BEFORE JPQL =====
Hibernate: update tb_member set contacts='010-3214-3214' where name='Junhyun'
2023-01-28 00:53:13.748  INFO 10436 --- [           main] blog.in.action.flush.CommitOptionTest    : ===== AFTER JPQL =====

CLOSING

개발자가 기술에 대해 이해도가 낮은 경우 의도치 않은 버그를 유발한다. 만약 "엔티티 매니저는 JPQL 쿼리 수행 전에 플러쉬를 하지 않으면 업데이트 순서가 꼬인다."는 사실을 몰랐다면 찾기 힘든 버그가 숨어들 가능성이 높다. 의도치 않게 발생할 수 있는 문제를 피하기 위해 꼼꼼히 공부하자는 차원에서 정리하였다.

TEST CODE REPOSITORY

RECOMMEND NEXT POSTS

REFERENCE

댓글남기기