티스토리 뷰

Spring Data JPA를 사용하다 보면 영속성 컨텍스트라는 개념을 피해서 이해하기는 어렵습니다. CRUD API 자체는 비교적 단순해 보이지만, 트랜잭션 내부에서 엔티티가 어떤 상태를 거치며 언제 SQL이 실행되는지에 따라 실제 애플리케이션의 동작은 크게 달라집니다. 특히 히스토리성 데이터를 다루는 도메인에서는 flush 시점과 엔티티 상태 관리 방식이 설계에 직접적인 영향을 미치기도 합니다.

 

이 글에서는 JPA의 핵심 개념 중 하나인 영속성 컨텍스트를 중심으로, 엔티티 생명주기와 더티체크가 어떤 맥락에서 동작하는지 정리합니다. 그리고 flush 모드 중 FlushMode.COMMIT을 활용해 변경 이전 데이터를 스냅샷하는 전략을 실무 관점에서 정리해봅니다. 모든 설명은 JPA 스펙과 Hibernate 공식 문서를 기준으로 확인 가능한 범위 내에서만 다룹니다.

 

 

영속성 컨텍스트를 이해해야 하는 이유

 

영속성 컨텍스트는 단순히 “엔티티를 캐싱하는 공간” 정도로 설명되곤 합니다. 하지만 JPA 스펙에서 정의하는 영속성 컨텍스트의 역할은 그보다 조금 더 넓습니다. 영속성 컨텍스트는 엔티티의 생명주기를 관리하고, 동일성(identity)을 보장하며, 트랜잭션 단위의 변경 사항을 추적하는 역할을 담당합니다.

 

Spring Data JPA 환경에서는 보통 하나의 트랜잭션 범위 안에서 하나의 영속성 컨텍스트가 생성되고 종료됩니다. 이 구조 덕분에 개발자는 명시적으로 SQL을 호출하지 않아도, 엔티티 상태 변경만으로 데이터베이스 반영을 기대할 수 있습니다. 다만 이 편의성 뒤에는 flush와 commit이라는 명확히 구분되는 단계가 존재하며, 이 차이를 이해하지 못하면 예상과 다른 동작을 경험하게 됩니다.

 

 

엔티티 생명주기와 상태 전이

 

JPA 스펙에서는 엔티티의 상태를 네 가지로 정의합니다. 비영속(Transient), 영속(Managed), 준영속(Detached), 삭제(Removed) 상태입니다. 이 상태들은 서로 고정된 것이 아니라, 트랜잭션 흐름과 EntityManager의 호출에 따라 자연스럽게 전이됩니다.

 

비영속 상태는 단순히 new 키워드로 생성된 객체 상태를 의미합니다. 아직 영속성 컨텍스트와는 아무런 관계가 없는 상태입니다. persist가 호출되면 해당 엔티티는 영속 상태로 전이되며, 이 시점부터 영속성 컨텍스트의 관리 대상이 됩니다. 이때 중요한 점은 persist 호출 시점에 즉시 INSERT SQL이 실행된다고 보장되지 않는다는 점입니다. SQL 실행 여부는 flush 시점에 의해 결정됩니다.

 

영속 상태의 엔티티는 동일성 보장이라는 중요한 특징을 가집니다. 동일한 식별자를 가진 엔티티를 여러 번 조회하더라도, 영속성 컨텍스트 내부에서는 항상 동일한 인스턴스를 반환합니다. 이는 객체 그래프를 다루는 애플리케이션 코드의 안정성을 높이는 중요한 기반이 됩니다.

 

준영속 상태는 영속성 컨텍스트에서 분리된 상태를 의미합니다. detach 호출이나 트랜잭션 종료 시점에 엔티티는 준영속 상태가 되며, 이 시점부터는 변경 사항이 자동으로 추적되지 않습니다. 삭제 상태는 remove 호출 이후의 상태로, flush 시점에 DELETE SQL이 실행될 대상으로 관리됩니다.

 

 

더티체크의 의미와 동작 맥락

 

더티체크는 영속 상태의 엔티티가 변경되었는지를 감지하는 메커니즘입니다. Hibernate 구현에서는 스냅샷 방식으로 이를 수행합니다. 엔티티가 영속성 컨텍스트에 처음 진입할 때의 상태를 기준으로 스냅샷을 저장하고, flush 시점에 현재 상태와 비교하여 변경 여부를 판단합니다.

 

중요한 점은 더티체크가 “값이 변경되는 순간”에 즉시 SQL을 생성하는 방식이 아니라는 점입니다. 더티체크는 flush 시점에 수행되며, flush는 commit과 동일한 개념이 아닙니다. 이 차이를 이해하지 않으면, 트랜잭션 중간에 실행되는 조회 쿼의 결과가 왜 예상과 다른지 이해하기 어렵습니다.

 

 

Flush와 Commit의 구분

 

JPA에서 flush는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 작업을 의미합니다. 반면 commit은 데이터베이스 트랜잭션을 실제로 확정하는 작업입니다. flush는 여러 번 발생할 수 있지만, commit은 트랜잭션 당 한 번만 발생합니다.

 

기본 설정에서는 FlushMode.AUTO가 적용됩니다. 이 모드에서는 JPQL이나 Criteria 쿼리 실행 전에 자동으로 flush가 발생할 수 있습니다. 이는 쿼리 결과의 정합성을 보장하기 위한 동작입니다. 아직 반영되지 않은 변경 사항이 쿼리 결과에 포함되지 않는 상황을 방지하기 위함입니다.

 

다만 이 자동 flush 특성은 히스토리성 데이터를 다루는 경우에는 오히려 제약이 되기도 합니다. 변경 이전 상태를 별도의 테이블에 저장하려는 경우, 예상보다 이른 flush로 인해 스냅샷 타이밍을 제어하기 어려워질 수 있습니다.

 

 

FlushMode.COMMIT의 특성

 

FlushMode.COMMIT은 트랜잭션 commit 시점에만 flush를 수행하도록 제한합니다. 즉, JPQL 조회나 기타 쿼리 실행 시에는 자동 flush가 발생하지 않습니다. Hibernate 공식 문서에서도 이 모드는 flush 타이밍을 명시적으로 제어하고 싶은 경우에 사용할 수 있다고 설명합니다.

 

이 모드를 사용하면 트랜잭션 내부에서 엔티티를 변경한 뒤에도, commit 이전까지는 데이터베이스에 변경 사항이 반영되지 않습니다. 따라서 동일 트랜잭션 내에서 변경 이전 상태를 안전하게 조회하거나 복사할 수 있는 여지가 생깁니다.

 

 

히스토리성 데이터의 before data 스냅샷 전략

 

히스토리 테이블을 운영하는 시스템에서는 “변경 이후 데이터”뿐 아니라 “변경 이전 데이터”를 저장해야 하는 요구사항이 자주 등장합니다. 이때 가장 단순한 접근은 UPDATE 전에 SELECT를 한 번 더 수행하는 방식입니다. 하지만 영속성 컨텍스트를 사용하는 환경에서는 이미 엔티티가 변경된 상태일 수 있으며, 자동 flush가 개입하면 이 전략은 깨질 수 있습니다.

 

FlushMode.COMMIT을 사용하는 전략은 이러한 문제를 완화하는 하나의 선택지입니다. 트랜잭션 시작 시 조회한 엔티티를 기준으로, 변경 작업을 수행하기 전에 해당 엔티티의 상태를 별도의 히스토리 엔티티로 복사합니다. 이 시점에는 아직 flush가 발생하지 않았으므로, 영속성 컨텍스트 내부 스냅샷과 동일한 before data를 확보할 수 있습니다.

 

이 접근 방식의 장점은 SQL 실행 타이밍을 예측 가능하게 만든다는 점입니다. 변경 로직과 히스토리 저장 로직을 하나의 트랜잭션 안에서 관리하면서도, 의도하지 않은 flush로 인해 before data가 오염되는 상황을 줄일 수 있습니다.

 

다만 이 전략에도 주의할 점은 존재합니다. FlushMode.COMMIT 환경에서는 트랜잭션 중간에 실행되는 조회 쿼리가 최신 변경 사항을 반영하지 않을 수 있습니다. 이는 의도된 동작이지만, 조회 일관성을 전제로 한 로직이 존재한다면 설계 단계에서 충분한 검토가 필요합니다.

 

 

마무리하며

 

영속성 컨텍스트, 더티체크, flush는 각각 분리된 개념처럼 보이지만, 실제로는 하나의 트랜잭션 흐름 안에서 유기적으로 연결되어 있습니다. 히스토리성 데이터를 다루는 시나리오에서는 이 연결 구조를 어떻게 제어할 것인지가 설계의 핵심이 됩니다.

 

FlushMode.COMMIT을 활용한 before data 스냅샷 전략은 모든 상황에 대한 정답이라기보다는, flush 타이밍을 명확히 인지하고 있는 경우에 선택할 수 있는 하나의 방법에 가깝습니다. 개인적으로는 영속성 컨텍스트의 동작을 한 단계 더 의식하게 만드는 계기가 되었고, 트랜잭션 경계와 데이터 일관성에 대해 다시 생각해보는 계기가 되었습니다.