개요
최근 영속성 컨텍스트 내에서 flush 시점에 대해 잘못 짚고 있던 부분이 있어서, 영속성 컨텍스트와 flush 시점에 대해서 정리해보고 트러블 슈팅을 공유하려 한다.
엔티티와 생명 주기
JPA에서 엔티티는 DB 테이블과 매핑되는 자바 객체를 말한다.
엔티티는 다음과 같은 생명 주기를 갖는다.
1. 비영속
Member member = new Member();
member.setId(100L);
member.setName("HelloJPA");
엔티티가 처음 만들어졌을 때 상태이다.
2. 영속
EntityManager em = emf.createEntityManager();
//비영속
Member member = new Member();
member.setId(100L);
member.setName("HelloJPA");
//영속
em.persist(member);
엔티티가 영속성 컨텍스트에서 관리되는 상태이다.
3. 준영속
em.detach(member);
영속성 컨텍스트에 저장되었다가 분리된 상태이다.
4. 삭제
em.remove(member);
영속성 컨텍스트와 DB에서 삭제된 상태이다.
영속성 컨텍스트란?
영속성 컨텍스트란 엔티티를 영구 저장하는 환경으로, JPA가 엔티티 객체들을 관리하는 논리적인 공간이다.
EntityManager를 통해 영속성 컨텍스트에서 엔티티를 관리한다.
//엔티티 매니저
EntityManager em = emf.createEntityManager();
//비영속
Member member = new Member();
member.setId(100L);
member.setName("HelloJPA");
//영속
em.persist(member);
//조회
em.find(Member.class, member.getId());
영속성 컨텍스트의 이점
1. 1차 캐시

데이터 조회 시, 영속성 컨텍스트에서 우선 조회 한다. 동일한 트랜잭션 내에서 저장한 데이터거나 이미 조회한 데이터라면, 영속성 컨텍스트의 엔티티를 사용하여 DB에 중복된 쿼리로 불필요한 액세스를 줄일 수 있다.
2. 동일성 보장
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 true
같은 엔티티 조회 시, 동일한 인스턴스를 반환한다.
3. 쓰기 지연
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작함
transaction.begin();
//1차 캐시 저장, DB 쿼리 전송 X
em.persist(memberA);
em.persist(memberB);
//commit할 때, DB에 쿼리 전송
tx.commit();
데이터 삽입 SQL 문을 모아서, 트랜잭션이 커밋되는 시점에 한 번에 실행한다.
4. 변경 감지 (Dirty Checking)
// 영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");
// 영속 엔티티 데이터 수정 -> Dirty Checking
memberA.setUsername("hi");
memberA.setAge(10);
//Entitiy가 수정된다면, 왠지 이런 코드가 있어야할 것 같다.
//하지만 필요 없음
//em.update(member)
//트랜잭션 커밋시점에 update 쿼리
transaction.commit();
트랜잭션이 커밋되는 시점에 엔티티의 변경사항을 자동으로 데이터베이스에 반영한다.
단, 엔티티의 상태는 영속 상태여야 한다.
flush
flush는 영속성 컨텍스트의 변경 내용을 DB에 동기화 하는 과정이다.
엔티티가 영속상태라고 해서 바로 DB에 반영이 되는 것이 아니고, flush 과정을 거쳐야 영속 상태의 엔티티들이 DB에 반영이 된다.
헷갈리지 말 것은, 영속성 컨텍스트의 변경 내용을 동기화 하는 것이지 영속성 컨텍스트를 비우는 것은 아니다.
flush 하는 방법은 다음과 같다.
- em.flush() 와 같은 메서드 명시적 호출
- JPQL, Native 쿼리 실행 시 자동 호출
- 트랜잭션 커밋 직전 자동 호출
save() 메서드는 flush 하지 않는다.
기존 코드
@Transactional
public void example() {
//1. dirty checking
exchangeRequest.approve();
exchangeRequestPresentedProduct.approve();
.
.
//2. save()
productExchangeRepository.save(productExchange);
.
.
//3. bulk update query
// --> a. @Modifying(clearAutomatically = true)
productExchangeRequestPresentedProductRepository.updateRejectedOtherExchangeRequestProduct(
productExchange.getId(), exchangeRequestPresentedProduct.getId());
// --> b. @Modifying(clearAutomatically = true, flushAutomatically = true)
customerStoreCouponRepository.updateRejectedOtherExchangeRequestProduct(
productExchange.getId(), exchangeRequestPresentedProduct.getId());
}
다음과 같은 흐름의 코드가 있었다. (참고로 어떤 기능인지보다는 흐름에 집중해주면 좋겠다.)
흐름과 단계 별 나의 예상이다.
- 변경 감지
- save() 호출 -> flush, 변경감지와 영속화 반영
- bulk update -> bulk update 마무리 후 영속성 컨텍스트 clear
나는 이름 때문에 save 메서드에서 flush가 일어날 것으로 인지하고 있었다. 하지만 save 메서드는 flush가 일어나지 않는다. flush를 명시적으로 호출하고 있지도 않고, JPQL도 사용하는 것이 아닌, 그냥 영속화일 뿐이다.
실제 동작에서는 3번의 첫 번째 메서드에서 flush가 발생하지 않은채 영속성 컨텍스트가 초기화 되었고, 변경 감지와 save로 인한 데이터가 DB에 반영되지 않은 것이다.
수정
@Transactional
public void example() {
//1. dirty checking
exchangeRequest.approve();
exchangeRequestPresentedProduct.approve();
.
.
//2. saveAndFlush()
productExchangeRepository.saveAndFlush(productExchange);
.
.
//3. bulk update query
// --> a. @Modifying(clearAutomatically = true)
productExchangeRequestPresentedProductRepository.updateRejectedOtherExchangeRequestProduct(
productExchange.getId(), exchangeRequestPresentedProduct.getId());
// --> b. @Modifying(clearAutomatically = true, flushAutomatically = true)
customerStoreCouponRepository.updateRejectedOtherExchangeRequestProduct(
productExchange.getId(), exchangeRequestPresentedProduct.getId());
}
save() -> saveAdFlush() 로 변경하여서 flush를 명시적으로 호출하였다. 변경 감지와 데이터 저장이 잘 되었다.
한 번 더 생각해볼 것
Spring Data Jpa에서 제공하는 기능인 메서드 쿼리는 JPQL로 동작하기에 해당 메서드를 호출하는 시점에 flush가 발생한다.
하지만 findById(), findAll() 메서드는 JPQL이 아니기에 해당 메서드를 호출한다고 하여도 flush가 발생하지 않을 것이다.
정리
학습했었던 내용이지만, 조금 더 생각해보고 정리를 해놓았다면 헷갈리지 않았을 문제인 것 같다. 그럼에도 JPA 기초에 대해 다시 한 번 복습하는 좋은 시기가 되었다.
'JPA' 카테고리의 다른 글
| [JPA] saveAll() 문제점과 JDBC를 통한 해결 (0) | 2026.01.14 |
|---|---|
| [JPA] @SQLRestriction으로 soft delete 구현하기 (0) | 2025.12.21 |
| [JPA] 프록시 객체와 OneToOne 양방향 관계 이슈 (0) | 2025.12.21 |
| [JPA] 테스트를 통해 알아본 JPA와 영속성 컨텍스트 (0) | 2024.03.15 |
| [Query DSL] Expressions를 사용하여 여러 값에 대한 존재 여부 확인하기 (0) | 2023.12.14 |