영속성 컨텍스트란?
JPA에서 가장 핵심적이면서도 이해하기 어려운 개념이 바로 영속성 컨텍스트(Persistence Context)입니다. 영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라는 뜻으로, 애플리케이션과 데이터베이스 사이에서 엔티티 객체를 관리하는 논리적인 공간입니다. EntityManager를 통해 엔티티를 영속성 컨텍스트에 저장하거나 조회하면, JPA는 다양한 최적화 기법을 자동으로 적용합니다.
// EntityManager를 통해 영속성 컨텍스트에 접근
@PersistenceContext
private EntityManager em;
// 엔티티를 영속성 컨텍스트에 저장
em.persist(member);
// 영속성 컨텍스트에서 조회
Member findMember = em.find(Member.class, 1L);
1. 엔티티의 생명주기
엔티티는 영속성 컨텍스트와의 관계에 따라 4가지 상태를 가집니다.
| 상태 | 설명 | 영속성 컨텍스트 관리 |
|---|---|---|
| 비영속 (New/Transient) | 영속성 컨텍스트와 전혀 관계없는 새로운 상태 | X |
| 영속 (Managed) | 영속성 컨텍스트에 의해 관리되는 상태 | O |
| 준영속 (Detached) | 영속성 컨텍스트에 저장되었다가 분리된 상태 | X |
| 삭제 (Removed) | 삭제된 상태 | O (삭제 예약) |
// 비영속 상태 - 순수한 자바 객체
Member member = new Member();
member.setName("홍길동");
// 영속 상태 - 영속성 컨텍스트에 저장
em.persist(member);
// 준영속 상태 - 영속성 컨텍스트에서 분리
em.detach(member);
// 삭제 상태 - 삭제 요청
em.remove(member);
2. 1차 캐시
영속성 컨텍스트 내부에는 1차 캐시가 존재합니다. em.persist()를 호출하면 엔티티가 1차 캐시에 저장되고, em.find()를 호출하면 먼저 1차 캐시에서 엔티티를 찾습니다. 1차 캐시에 없을 때만 데이터베이스에 SQL을 날립니다.
// 데이터베이스에서 조회 -> 1차 캐시에 저장
Member member1 = em.find(Member.class, 1L); // SELECT 쿼리 실행
// 1차 캐시에서 바로 반환 -> SQL 실행 안 함
Member member2 = em.find(Member.class, 1L); // SQL 실행 X
System.out.println(member1 == member2); // true
1차 캐시의 핵심적인 특징은 같은 트랜잭션 내에서 동일한 엔티티를 반복 조회해도 데이터베이스 접근이 한 번만 발생한다는 것입니다. 이를 통해 불필요한 쿼리를 줄이고 성능을 최적화합니다.
Spring에서의 1차 캐시 범위
Spring에서 EntityManager는 트랜잭션 단위로 생성됩니다. 따라서 1차 캐시의 유효 범위는 트랜잭션이 시작되고 종료될 때까지입니다. 트랜잭션이 끝나면 1차 캐시도 함께 소멸합니다.
@Transactional
public void businessLogic() {
// 이 트랜잭션 동안 1차 캐시가 유효
Member member = memberRepository.findById(1L).get(); // DB 조회
Member sameMember = memberRepository.findById(1L).get(); // 1차 캐시 반환
// member == sameMember (동일 객체)
}
// 트랜잭션 종료 -> 1차 캐시 소멸
3. 동일성(Identity) 보장
영속성 컨텍스트는 같은 엔티티를 조회하면 항상 같은 인스턴스를 반환합니다. 이를 반복 가능한 읽기(Repeatable Read) 등급의 트랜잭션 격리 수준을 애플리케이션 레벨에서 제공한다고 합니다.
@Test
void identityTest() {
Member member1 = em.find(Member.class, 1L);
Member member2 = em.find(Member.class, 1L);
// == 비교로 동일성 확인 (같은 인스턴스)
assertThat(member1).isSameAs(member2); // 통과
}
이 특성은 엔티티 비교 시 equals()가 아닌 == 연산자로도 동일성을 검증할 수 있다는 것을 의미합니다. 단, 이는 같은 영속성 컨텍스트(같은 트랜잭션) 내에서만 보장됩니다.
4. 쓰기 지연 (Transactional Write-Behind)
영속성 컨텍스트는 엔티티를 영속 상태로 만들 때 즉시 INSERT SQL을 데이터베이스에 보내지 않습니다. 대신, SQL을 쓰기 지연 SQL 저장소에 차곡차곡 모아두었다가, 트랜잭션 커밋 시점 또는 플러시 시점에 한꺼번에 데이터베이스에 전송합니다.
@Transactional
public void saveMembersExample() {
Member memberA = new Member("회원A");
Member memberB = new Member("회원B");
em.persist(memberA); // INSERT SQL을 쓰기 지연 저장소에 저장
em.persist(memberB); // INSERT SQL을 쓰기 지연 저장소에 저장
// 여기까지 DB에 SQL을 보내지 않음
// 트랜잭션 커밋 시점에 쓰기 지연 저장소의 SQL을 한꺼번에 전송
// -> INSERT memberA
// -> INSERT memberB
}
쓰기 지연의 장점은 여러 SQL을 모아서 한 번에 보내므로, 네트워크 왕복 횟수를 줄일 수 있다는 것입니다. JDBC 배치 기능과 결합하면 더욱 효과적입니다.
# application.yml - JDBC 배치 크기 설정
spring:
jpa:
properties:
hibernate:
jdbc:
batch_size: 50
order_inserts: true
order_updates: true
5. 변경 감지 (Dirty Checking)
변경 감지는 JPA의 가장 강력한 기능 중 하나입니다. 영속 상태의 엔티티 값을 변경하면, 별도로 update()나 save()를 호출하지 않아도 트랜잭션 커밋 시점에 자동으로 UPDATE SQL이 생성됩니다.
@Transactional
public void updateMemberName(Long memberId, String newName) {
Member member = em.find(Member.class, memberId);
member.setName(newName); // 값만 변경
// em.update(member) 같은 코드가 필요 없음!
// 트랜잭션 커밋 시 자동으로 UPDATE SQL 실행
}
변경 감지 동작 원리
변경 감지의 내부 동작 과정은 다음과 같습니다.
- 1) 엔티티를 영속성 컨텍스트에 최초 저장할 때, 해당 시점의 스냅샷(원본 복사본)을 생성하여 보관합니다.
- 2) 트랜잭션 커밋(또는 플러시) 시점에, 현재 엔티티와 스냅샷을 필드 단위로 비교합니다.
- 3) 변경된 필드가 있으면 UPDATE SQL을 생성하여 쓰기 지연 SQL 저장소에 추가합니다.
- 4) SQL 저장소의 쿼리를 데이터베이스에 전송합니다.
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public MemberResponse updateMember(Long id, MemberUpdateRequest request) {
Member member = memberRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("회원을 찾을 수 없습니다."));
// 변경 감지 활용 - setter 대신 의미있는 비즈니스 메서드 사용 권장
member.changeName(request.getName());
member.changeEmail(request.getEmail());
// save() 호출 불필요 - 변경 감지에 의해 자동 UPDATE
return MemberResponse.from(member);
}
}
참고: Spring Data JPA의 save() 메서드는 내부적으로 엔티티가 이미 영속 상태인지 확인하고, 영속 상태이면 merge()를 호출합니다. 변경 감지를 활용하면 불필요한 save() 호출과 merge() 동작을 피할 수 있습니다.
6. 플러시 (Flush)
플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업입니다. 플러시가 발생하면 변경 감지가 동작하고, 쓰기 지연 SQL 저장소의 쿼리가 데이터베이스에 전송됩니다. 중요한 점은 플러시는 영속성 컨텍스트를 비우는 것이 아니라, 변경 내용을 데이터베이스에 반영하는 것입니다.
플러시가 발생하는 3가지 경우
em.flush()직접 호출- 트랜잭션 커밋 시 자동 호출
- JPQL 쿼리 실행 시 자동 호출
@Transactional
public void flushExample() {
Member member = new Member("홍길동");
em.persist(member); // INSERT SQL은 아직 DB로 안 감
// JPQL 실행 시 자동 플러시 발생
// -> member의 INSERT가 먼저 DB에 반영된 후 JPQL 실행
List<Member> members = em.createQuery("select m from Member m", Member.class)
.getResultList();
}
플러시 모드 설정
// 기본값: 커밋 또는 쿼리 실행 시 플러시 (권장)
em.setFlushMode(FlushModeType.AUTO);
// 커밋할 때만 플러시
em.setFlushMode(FlushModeType.COMMIT);
FlushModeType.COMMIT은 JPQL 실행 시 플러시를 건너뛰므로 데이터 정합성 문제가 발생할 수 있습니다. 특별한 성능 최적화가 필요한 경우가 아니라면 기본값인 AUTO를 사용하세요.
7. 준영속 상태와 실무에서의 주의점
준영속 상태는 영속성 컨텍스트가 더 이상 관리하지 않는 엔티티입니다. 변경 감지, 1차 캐시 등 영속성 컨텍스트의 모든 기능을 사용할 수 없습니다.
// 특정 엔티티를 준영속 상태로 전환
em.detach(member);
// 영속성 컨텍스트 전체 초기화
em.clear();
// 영속성 컨텍스트 종료
em.close();
OSIV(Open Session In View)와 준영속 문제
Spring Boot에서 spring.jpa.open-in-view의 기본값은 true입니다. 이 설정이 활성화되면 영속성 컨텍스트가 뷰 렌더링이 완료될 때까지 유지되어, 지연 로딩이 뷰에서도 가능합니다. 하지만 데이터베이스 커넥션을 오래 점유하게 되므로, 실시간 트래픽이 많은 서비스에서는 비활성화하는 것이 좋습니다.
# application.yml
spring:
jpa:
open-in-view: false # 실시간 트래픽 서비스에서 권장
OSIV를 끄면 트랜잭션 밖에서 지연 로딩을 시도할 때 LazyInitializationException이 발생합니다. 이를 방지하려면 서비스 계층에서 필요한 데이터를 모두 로딩하거나, Fetch Join을 활용해야 합니다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
@Transactional(readOnly = true)
public OrderDetailResponse getOrderDetail(Long orderId) {
// Fetch Join으로 필요한 연관 엔티티를 함께 로딩
Order order = orderRepository.findByIdWithMemberAndItems(orderId)
.orElseThrow(() -> new EntityNotFoundException("주문을 찾을 수 없습니다."));
return OrderDetailResponse.from(order);
}
}
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.member JOIN FETCH o.orderItems WHERE o.id = :id")
Optional<Order> findByIdWithMemberAndItems(@Param("id") Long id);
}
마치며
JPA 영속성 컨텍스트는 단순한 캐시를 넘어, 엔티티의 생명주기를 관리하고 다양한 성능 최적화를 자동으로 수행하는 핵심 메커니즘입니다. 1차 캐시로 반복 조회 성능을 높이고, 쓰기 지연으로 SQL 전송을 최적화하며, 변경 감지로 명시적인 UPDATE 호출 없이도 데이터를 변경할 수 있습니다. 이러한 내부 동작 원리를 정확히 이해하고 있어야 JPA를 올바르게 활용하고, 성능 문제를 효과적으로 해결할 수 있습니다. 특히 플러시 타이밍, 준영속 상태, OSIV 설정은 실무에서 자주 마주치는 이슈이므로 반드시 숙지하시기 바랍니다.
'JPA' 카테고리의 다른 글
| Spring Data JPA 고급 기능 - Specification, Projection, Auditing (0) | 2026.04.03 |
|---|---|
| JPA 성능 최적화 실전 - 벌크 연산부터 2차 캐시까지 (0) | 2026.04.02 |
| QueryDSL 실전 가이드 - 동적 쿼리부터 페이징까지 (0) | 2026.03.26 |
| JPA N+1 문제 완벽 정리 - 원인부터 해결까지 실무 가이드 (0) | 2026.03.24 |