
JPA에서는 EntityManager를 통해 엔티티 객체를 관리한다. 그리고 EntityManager는 영속성 컨텍스트라는 내부 공간을 유지하면서, 그 안에 올라온 엔티티의 생명주기와 변경 사항을 추적한다. 이 영속성 컨텍스트는 흔히 애플리케이션과 DB 사이에서 객체를 보관하는 가상의 DB(또는 1차 캐시) 역할을 한다.
예를 들어 JPA에서 엔티티를 find로 조회하면, 조회된 엔티티는 영속성 컨텍스트에 등록되어 영속 상태(managed) 가 된다. 이때 동일한 트랜잭션(즉 같은 영속성 컨텍스트) 안에서 같은 엔티티를 다시 조회하면, DB를 다시 조회하지 않고 영속성 컨텍스트의 1차 캐시에서 엔티티를 반환할 수 있다. 이 특성 덕분에 불필요한 쿼리를 줄이고, 동일성 보장 같은 이점도 얻는다.
영속성 컨텍스트는 엔티티를 상태(state)로 구분해서 관리한다.
관리(managed) 상태
persist(save)로 새롭게 등록한 엔티티, 또는 find로 조회한 엔티티는 영속성 컨텍스트가 관리하는 상태가 된다. 이 상태에서는 영속성 컨텍스트가 엔티티를 추적하고, 변경을 감지해서 DB 반영까지 자동으로 처리할 수 있다.
준영속(detached) 상태
detach, clear 등을 호출해 영속성 컨텍스트에서 분리된 상태다. 한 번 분리되면 JPA가 더 이상 변경을 추적하지 않기 때문에, 값이 바뀌어도 자동 업데이트가 일어나지 않는다. 실무에서 의도적으로 쓰는 경우는 많지 않지만, 동작 원리를 이해할 때 중요한 상태다.
비영속(new) 상태
아직 영속성 컨텍스트에 포함되지 않은 순수 객체 상태다. 이 상태에서는 JPA가 추적하지 않기 때문에, DB 반영을 원하면 persist(save)로 영속 상태로 만들어야 한다.
삭제(removed) 상태
remove(delete)가 호출되어 삭제가 예약된 상태다. 즉시 DB에서 지우는 것이 아니라, flush 시점에 DELETE SQL이 실행된다(트랜잭션 롤백 시 취소 가능).
영속성 컨텍스트가 “가상의 DB”처럼 동작한다고 하는 이유는, 단순히 객체를 담아두는 수준을 넘어 트랜잭션 동안의 데이터 변경을 관리하고 DB와 동기화하는 기능을 제공하기 때문이다. 대표적으로 변경 감지(Dirty Checking)와 쓰기 지연이 있다.


트랜잭션 안에서 영속 상태의 엔티티를 수정하면, 개발자가 update 쿼리를 직접 호출하지 않아도 JPA가 변경 사항을 감지해 반영한다. 이 메커니즘이 변경 감지(Dirty Checking)다.
여기서 중요한 포인트는, JPA가 “현재 엔티티 상태”를 DB에서 다시 읽어와 비교하는 방식이 아니라는 점이다. 영속성 컨텍스트는 엔티티가 처음 로딩된 시점의 값(초기 스냅샷)을 저장해두고, flush 시점에 스냅샷과 현재 값을 비교해서 변경 여부를 판단한다. 변경이 감지되면 그때 UPDATE SQL을 생성한다.

또 하나 정리할 부분은 “트랜잭션 커밋 시점에 반영된다”는 표현이다. 보통은 커밋 직전에 flush가 발생하므로 커밋 시점에 반영되는 것처럼 보이지만, 더 정확히 말하면 변경 감지는 flush 시점에 DB로 동기화된다. flush는 커밋 직전에 일어나는 경우가 가장 흔하지만, JPQL/Criteria 쿼리를 실행하기 전에 자동 flush가 발생하는 등 트랜잭션 중간에도 발생할 수 있다.
그리고 신규 데이터 등록의 경우를 같이 구분해두면 좋다. 영속 상태 엔티티의 수정은 save 없이도 변경 감지로 UPDATE가 가능하지만, 애초에 비영속(new) 객체는 JPA가 관리하지 않으므로 DB에 넣기 위해서는 persist(save)를 통해 영속 상태로 만들어야 하고, 그 결과로 INSERT가 발생한다.
쓰기 지연은 persist(save) 같은 작업을 수행했을 때 즉시 INSERT를 DB로 보내지 않고, 영속성 컨텍스트 내부의 “쓰기 지연 저장소”에 SQL을 모아두었다가 flush 시점에 한꺼번에 DB로 전송하는 개념이다. 그래서 여러 번 save를 호출해도 그 순간마다 매번 DB에 바로 반영되는 게 아니라, 보통은 커밋 직전 flush에서 일괄 실행되는 것처럼 보인다.
쓰기 지연은 성능적으로도 의미가 있다. SQL 실행을 트랜잭션 흐름에 맞춰 모아서 처리할 수 있어 네트워크 왕복 횟수를 줄일 수 있고, 같은 트랜잭션에서의 작업을 한 덩어리로 관리하기에도 유리하다.
그리고 트랜잭션 중간에 예외가 발생하면(그리고 롤백되면) flush로 DB에 반영되기 전이거나, 이미 일부 반영되었더라도 트랜잭션 특성상 전체가 롤백되어 결과적으로 작업이 취소된다. 이런 점에서 트랜잭션의 원자성을 지키는 흐름과도 자연스럽게 맞물린다(단, “쓰기 지연 자체가 원자성을 강화한다”기보다는 트랜잭션/DB의 롤백 메커니즘과 결합되어 안전하게 보장된다고 이해하는 편이 정확하다).
아래 예시는 스프링에서 @Transactional로 트랜잭션을 열고, find로 엔티티를 영속 상태로 만든 뒤 값을 변경만 해도 커밋 시점(정확히는 flush 시점)에 UPDATE가 나가는 흐름을 보여준다.
@Service
@RequiredArgsConstructor
public class AuthorService {
private final AuthorRepository authorRepository;
@Transactional
public void changeAuthorName(Long authorId, String newName) {
// 1) find: 엔티티 조회 -> 영속성 컨텍스트에 올라가며 managed(영속 상태)
Author author = authorRepository.findById(authorId)
.orElseThrow(() -> new IllegalArgumentException("author not found"));
// 2) 수정: update 쿼리를 직접 호출하지 않음
author.changeName(newName); // 또는 author.setName(newName);
// 3) save 호출 없음
// -> 트랜잭션 커밋 직전 flush 시점에 스냅샷과 비교하여 변경 감지 발생
// -> 변경이 있으면 UPDATE SQL 생성/실행
}
}
실제로는 메서드가 끝나면서 트랜잭션이 커밋되고, 그 직전에 영속성 컨텍스트가 flush 되면서 JPA가 스냅샷과 현재 값을 비교해 변경을 감지한다. 변경된 값이 있다면 그때 UPDATE author ... 같은 SQL이 실행되고 DB와 동기화된다.
참고로, 같은 트랜잭션 안에서 authorRepository.findById(authorId)를 한 번 더 호출하면 DB를 또 때리는 게 아니라(대부분의 경우) 1차 캐시에 있는 같은 엔티티를 반환한다. 그래서 “조회 → 수정 → 커밋 때 반영”이라는 흐름이 영속성 컨텍스트 기반으로 자연스럽게 이어진다.
영속성 컨텍스트는 애플리케이션과 DB 사이에서 엔티티를 보관하고 관리하는 공간으로, 1차 캐시, 변경 감지, 쓰기 지연 같은 기능을 제공한다. 트랜잭션 동안 엔티티 변경 사항을 추적해두었다가 flush 시점에 DB로 동기화하고, 트랜잭션이 끝나 영속성 컨텍스트가 종료되면 그 컨텍스트가 관리하던 엔티티들은 더 이상 영속 상태가 아니게 된다(일반적으로 준영속 상태로 전환된다고 이해하면 된다).