스프링에서 JPA를 사용하다 보면 EntityManager라는 객체를 접하게 된다. EntityManager는 이름 그대로 엔티티(@Entity 어노테이션이 붙은 자바 객체)를 관리하고, 실제 데이터베이스(DB) 테이블과 매핑하여 데이터를 조회∙수정∙저장하는 중요한 역할을 수행한다. 이 글에서는 EntityManager와 이를 둘러싼 영속성 컨텍스트(Persistence Context)를 깊이 있게 살펴보고, 스프링 환경에서 어떻게 사용되는지 알아보고자 한다.

JPA는 META-INF/persistence.xml 또는 스프링 부트 환경이라면 application.yml(혹은 application.properties) 등에 정의된 설정 정보를 바탕으로 EntityManagerFactory를 생성한다. EntityManagerFactory는 말 그대로 EntityManager를 생성해주는 팩토리 클래스다.
스프링 컨테이너가 EntityManagerFactory를 관리하고, 필요에 따라 EntityManager를 생성해 의존성 주입해주는 방식을 의미한다.
@PersistenceContext
private EntityManager entityManager;
이처럼 @PersistenceContext 애노테이션을 통해 자동 주입받을 수 있으며, 스레드 안전성(Thread-Safe)을 보장하기 위해 실제로는 프록시 객체가 주입된다.
스프링 컨테이너로부터 EntityManagerFactory만 주입받은 뒤, 어플리케이션 코드에서 직접 EntityManager를 생성∙관리하는 방식을 의미한다. 좀 더 유연한 트랜잭션 관리가 가능하지만, 매번 EntityManager를 생성하고 종료해야 하므로 관리 부담이 생길 수 있다.
@Autowired
private EntityManagerFactory emf;
public void doSomething() {
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
// 엔티티를 조회하거나 persist 하는 로직
tx.commit();
} catch(Exception e) {
tx.rollback();
} finally {
em.close();
}
}
EntityManager는 내부적으로 영속성 컨텍스트(Persistence Context) 라는 논리적 공간을 활용한다. 영속성 컨텍스트는 엔티티를 관리하는 일종의 “캐시” 역할을 하며, 엔티티의 상태를 추적해 DB와 동기화한다.
EntityManager는 영속성 컨텍스트에 등록된 엔티티를 Map<Id, Entity> 형태로 보관한다. 같은 트랜잭션 내에서 동일한 엔티티를 여러 번 조회하더라도, 이미 캐싱된 엔티티가 있다면 DB 쿼리를 재실행하지 않는다. 이는 조회 성능 최적화에 큰 이점이 있다.
EntityManager는 트랜잭션이 커밋되기 전까지 쿼리를 모아두었다가, 커밋 시점에 한 번에 DB로 전송한다. 이로 인해 트랜잭션 도중에 발생한 여러 변경사항을 한꺼번에 적용할 수 있으며, 문제 발생 시 롤백이 용이하다.
영속성 컨텍스트에 등록된 엔티티가 수정되면, 트랜잭션 커밋 시점에 1차 캐시와 비교하여 변경 사항이 있을 경우 자동으로 UPDATE 쿼리를 수행한다. 개발자는 persist()를 한 번만 호출해도, 엔티티 필드 변경 시 자동으로 DB에 반영되므로 편리하다.
@ManyToOne, @OneToMany 등 연관 관계를 맺은 엔티티를 지연 로딩(Lazy Loading)으로 설정할 경우, 실제로 해당 엔티티의 데이터에 접근하는 순간 쿼리가 실행된다. 이를 통해 불필요한 쿼리 발생을 줄이고, 필요한 시점에만 DB 조회를 수행할 수 있다.
엔티티는 영속성 컨텍스트와의 관계에 따라 다음과 같은 네 가지 상태를 가진다.
자바 객체가 새로 생성되었지만, 아직 EntityManager로 관리되지 않는 상태다. 예를 들어,
Member member = new Member("산초");
이 시점에는 DB와 전혀 연결되어 있지 않은 순수 자바 객체다.
EntityManager에 의해 관리되는 상태다.
em.persist(member); // 비영속 -> 영속
em.find(Member.class, 1L); // DB에서 조회되어 영속 상태로 등록
이 상태에서는 엔티티 변경 사항이 자동으로 DB에 반영된다.
한 번 영속 상태였던 엔티티가 분리된 상태다.
em.detach(member); // 영속 -> 준영속
em.clear(); // 모든 엔티티 준영속화
em.close(); // 영속성 컨텍스트 종료
준영속 상태에서는 더 이상 엔티티 변경 사항이 DB에 반영되지 않는다.
엔티티가 삭제 대상이 되어, 영속성 컨텍스트에서 제거된 상태다.
em.remove(member); // 영속 -> 삭제
이후 트랜잭션 커밋 시점에 DB에서도 해당 데이터가 삭제된다.
persist()
비영속 상태의 엔티티를 영속성 컨텍스트에 등록한다.
find() / JPQL / Native Query
엔티티를 조회할 수 있으며, 먼저 1차 캐시를 확인한 뒤 캐시가 없으면 DB에 쿼리를 수행한다.
merge()
준영속 상태의 엔티티를 다시 영속성 컨텍스트에 병합한다.
remove()
엔티티를 삭제 상태로 만들어, 트랜잭션 커밋 시점에 DB에서 삭제한다.
flush()
쓰기 지연 중인 SQL 쿼리를 DB에 즉시 반영한다.
clear() / close()
영속성 컨텍스트를 초기화하거나 종료한다. 이때 관리되던 엔티티들은 준영속 상태가 된다.
@Repository
public class JpaRepository {
@PersistenceContext
private EntityManager em;
public Long save(Member member) {
// 1. new 상태 (비영속)
em.persist(member); // 2. managed 상태 (영속)
em.detach(member); // 3. detached 상태 (준영속)
return member.getId();
}
}
위 예시에서 Member 객체는 처음에는 비영속 상태였으나 em.persist()를 통해 영속 상태로 전환되었다. 이후 em.detach(member) 호출로 인해 준영속 상태가 된다.
정리하자면, EntityManager는 스프링과 JPA 환경에서 엔티티를 관리하고 DB와 매핑해주는 핵심적인 인터페이스다. 영속성 컨텍스트를 활용해 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등 다양한 최적화 기능을 제공하며, 엔티티의 생애주기를 관리한다.
비영속: 순수 자바 객체, DB와 무관
영속: 엔티티 매니저가 관리하는 상태
준영속: 영속성 컨텍스트에서 분리된 상태
삭제: DB에서 제거될 대상이 된 상태
개발자는 EntityManager를 통해 엔티티를 persist, merge, remove 등으로 제어하고, 트랜잭션 커밋 시점에 DB에 반영되도록 설계할 수 있다. 특히 스프링 부트 환경에서는 @PersistenceContext 또는 @Autowired를 사용해 쉽게 의존성 주입을 받을 수 있어 편리하다.
JPA의 작동 원리와 EntityManager의 기능을 제대로 이해하면, 복잡한 SQL 작성 없이도 객체지향적인 코드로 효율적인 데이터 접근 로직을 구현할 수 있다. 앞으로 JPA를 사용할 때, 영속성 컨텍스트와 EntityManager의 특성을 잘 활용해보자.