오늘날 많은 애플리케이션이 복잡한 비즈니스 로직과 대용량 데이터를 다루면서,
데이터의 일관성과 효율적인 엔티티 관리가 매우 중요해졌습니다.
이런 상황에서 JPA는 객체와 관계형 데이터베이스 간의 매핑을 쉽게 해주어 개발 생산성을 높여줍니다.
특히 영속성 컨텍스트는 단순한 캐시 이상의 역할을 합니다.
영속성 컨텍스트는 단일 트랜잭션 내에서 엔티티를 관리하고,
변경 사항을 감지하여 데이터 동기화를 자동으로 처리합니다.
이 글에서는 영속성 컨텍스트의 주요 기능과 동작 원리를 설명 드리겠습니다.
📌 영속성 컨텍스트의 개념과 역할
영속성 컨텍스트는 JPA가 제공하는 메모리 기반 저장소로,
어플리케이션 내에서 엔티티를 효과적으로 관리하는 역할을 합니다.
데이터베이스와 상호작용하기 전에 엔티티 객체를 메모리에 보관하여,
반복 조회 시 데이터베이스 부하를 줄여줍니다.
또한 엔티티의 상태를 추적하고, 변경 감지 기능을 통해 트랜잭션 종료 전에 데이터베이스와 동기화합니다.
🔍 1차 캐시와 엔티티 관리
영속성 컨텍스트의 가장 큰 장점 중 하나는 바로 1차 캐시 역할 입니다.
같은 영속성 컨텍스트 내에서 동일한 엔티티를 조회하면,
DB에서 다시 조회하지 않고 1차 캐시에 보관된 엔티티를 반환합니다.
1개의 EntityManager는 1개의 영속성 컨텍스트(1차 캐시)를 유지합니다.
따라서 EntityManager에서 같은 엔티티를 다시 조회하면,
DB 조회 없이 메모리에 있는 객체를 재사용할 수 있습니다.
이를 통해 이미 조회된 엔티티는 DB를 재접근할 필요 없이 영속성 컨텍스트의 캐시에서 바로 가져올 수 있어,
조회 성능이 향상됩니다.
같은 트랜잭션 내에서는 동일한 엔티티 객체의 인스턴스를 사용하기 때문에
객체의 동등성과 동일성을 일관적으로 유지할 수 있습니다.
동작원리
엔티티 메니저에 엔티티를 조회(find)하거나 저장(persist)하면,
해당 엔티티가 1차 캐시에 저장됩니다.
동일한 엔티티를 다시 조회할 땐, DB를 조회하지 않고 1차 캐시를 확인하여 사용합니다.
만약 1차 캐시에 없다면, DB에 쿼리를 통해 엔티티를 가져오고, 다시 1차 캐시에 저장합니다.
엔티티의 상태를 자동으로 관리하여,
변경 사항이 있을 경우 이를 감지해 데이터베이스에 반영합니다.
이 기능 덕분에 개발자는 직접 SQL다루지 않아도 되고,
자동으로 엔티티 관리와 변경 감지를 활용할 수 있습니다.
🔍 트랜잭션 범위 내 동작 원리
영속성 컨텍스트는 트랜잭션 범위 내에서 작동합니다.
트랜잭션이란?
트랜잭션은 DB 작업(입출력, 상태 변경등)을 하나의 논리적 단위로 묶은 것입니다.
트랜잭션은 코드의 실행이 모두 성공하거나, 모두 실패라는 ACID 원칙을 지킵니다.
트랜잭션이 시작되면 컨텍스트가 생성되고, 종료 시 소멸됩니다.
따라서 같은 트랜잭션 내에서는 같은 영속성 컨텍스트가 유지되므로, 엔티티 인스턴스가 재사용됩니다.
따라서, 하나의 트랜잭션 안에서 엔티티를 조회하거나 수정하면,
영속성 컨텍스트가 해당 엔티티를 1차 캐시로 관리하고,
트랜잭션이 끝날 때(DB에 커밋되는 시점) 변경사항을 DB에 반영(Flush)합니다.
영속성 컨텍스트가 트랜잭션에서 동작해야 하는 이유는 다음과 같습니다.
만약 트랜잭션이 롤백되면, 모든 변경사항이 무효화되므로,
엔티티 상태가 안정적으로 유지되고, 데이터의 일관성이 보장됩니다.
영속성 컨텍스타 엔티티를 관리하며 변경사항을 추적하고,
트랜잭션 종료 시점에서 Flush를 수행하여 필요한 UPDATE 쿼리를 발생시키기 때문에 변경을 감지(Dirty Checking)합니다.
또한, 트랜잭션 범위 내에서만 동시성을 제어할 수 있습니다.
🔍 쓰기 지연(Write-Deley)
영속성 컨텍스트에 변경된 엔티티를 즉시 DB에 반영하지 않고,
트랜잭션이 커밋될 때, 한번에 DB에 반영하는 방식입니다.
SQL이 여러번 실행되지 않고, 하나의 트랜잭션으로 처리하여 성능을 최적화 할 수 있습니다.
엔티티가 변경되면, 쓰기 지연 SQL 저장소에 SQL을 저장하고
트랜잭션이 커밋되는 시점에 쓰기 지연 SQL 저장소의 SQL을 한번에 실행합니다.
🔍 변경 감지(Dirty Checking)와 자동 Flush
변경 감지는 영속성 컨텍스트가 관리하는 엔티티의 상태변화를 추적하여,
변경된 엔티티를 데이터베이스에 반영하는 기법입니다.
엔티티가 영속된 상태라면, JPA는 엔티티의 스냅샷(snapshot, 복사본)을 내부적으로 보관합니다.
이후, 엔티티의 필드가 변경될 때,
JPA가 트랜잭션 커밋 또는 Flush 시점에 기존상태(스냅샷)과 현재 상태를 비교하여,
어떤 부분이 변경되었는지 식별합니다.
자동 Flush 는 트랜잭션 커밋 또는 쿼리를 실행할 때,
영속성 컨텍스트 내에 변경된 엔티티를 자동으로 DB에 반영(flush)하는 과정입니다.
이 기능을 통해 코드의 간결함은 유지하면서,
개발자가 데이터 동기화에 신경 쓰지 않아도 되도록 도와줍니다.
🔍 지연 로딩과 성능 최적화
지연 로딩은 엔티티를 실제로 사용할 때까지 데이터베이스에서 조회하지 않고 미뤄두는 전략입니다.
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> orderItems = new ArrayList<>();
...
}
Order 엔티티가 OrderItem 컬렉션을 가져야 하는 경우,
`fetch = FetchType.LAZY` 로 설정해두면 실제로 orderItems에 접근하기 전까지는 DB에서 불러오지 않습니다.
연관된 엔티티나 컬렉션을 즉시 로딩(Eager Loading)으로 설정하면,
해당 엔티티를 조회할 때마다 모두 한번에 가져오기 때문에 초기 로딩 속도가 느려질 수 있습니다.
반대로, 지연 로딩(Lazy Loading)은 요청이 발생하기 전까지 SQL을 실행하지 않으므로,
불필요한 데이터 조회를 피하고, 초기 로딩 시간을 단축시킵니다.
사용하지 않는 엔티티까지 미리 가젼다면, 네트워크 트래픽과 서버 메모리를 불필요하게 소모할 수 있습니다.
하지만 지연 로딩은 실재로 필요한 데이터만 가져오기 때문에
애플리케이션 전반의 리소스 사용량을 줄이고 효율성을 높일 수 있습니다.
⚠️ N+1 문제 발생 위험
지연 로딩을 사용하면 필요한 시점에 각 엔티티를 개별적으로 조회하게 됩니다.
예를 들어, Order 10건을 조회한 뒤,
각 Order마다 OrderItem을 지연 로딩으로 가져와야 한다면,
10건 각각에 대해 쿼리가 1번씩 추가로 실행됩니다.
결과적으로 총 1(최초 Order 조회) + 10(OrderItem 조회) = 11번의 쿼리가 실행되어 ‘N+1 문제’가 발생하게 됩니다.
영속성 컨텍스트는 JPA의 핵심 기능으로, 1차 캐시, 엔티티 관리, 변경 감지, 자동 Flush, 지연 로딩등 다양한 기능을 제공합니다.
이 기능들을 잘 활용하면 불필요한 DB 접근을 줄여 성능을 향상시키고,
데이터 동기화를 자동으로 처리하여 개발 효율성을 크게 향상시킬 수 있습니다.