HJW's IT Blog

JPA - EntityManager와 영속성 컨텍스트 이해하기 본문

JAVA

JPA - EntityManager와 영속성 컨텍스트 이해하기

kiki1875 2025. 2. 25. 11:33

0. 들어가며

JPA(Java Persistence API)는 Java 애플리케이션에서 데이터베이스와의 상호작용을 단순화하는 ORM(Object-Relational Mapping) 기술이다. 그중 EntityManager는 엔티티를 관리하고 데이터베이스의 CRUD 작업을 수행하는 핵심 객체이다. 이 글에서는 EntityManager의 역할과 특징, 그리고 영속성 컨텍스트의 동작 방식에 대해 다뤄보겠다.

1. EntityManager 란?

JPA 에서 Entity 를 관리하는 객체로, 데이터베이스의 CRUD 작업을 수행하는 역할을 한다. 다음과 같은 주요 기능을 수행한다.

1.1 핵심 기능

EntityManager 는 Entity 의 생명 주기를 관리한다.

  • 영속화 (Persist) : 엔티티 객체를 DB에 저장
  • 조회 (Find) : 데이터베이스에서 특정 엔티티를 조회
  • 수정(Merge) : 엔티티 객체의 변경사항을 DB에 반영
  • 삭제 (Remove) : DB에서 엔티티 삭제
  • 분리 (Detach) : 엔티티 객체를 EntityManager의 관리 대상에서 제외한다.
  • 병합 (Merge ) : 분리된 엔티티 객체의 상태를 영속성 컨텍스트에 반영한다.
  • 비우기 (Clear ) : 영속성 컨텍스트 초기화

EntityManager 의 주요 특징

  • 인터페이스 : 실제 구현은 JPA 구현체에 의해 제공된다 (ex. hibernate)
  • Not Thread-Safe : 각 쓰레드, 요청마다 별도의 EntityManager 를 사용해야 안전하다
  • 영속성 컨텍스트 : EntityManager는 영속성 컨텍스트를 통해 엔티티를 관리한다

1.2 EntityManagerFactory 란?

EntityManager 객체를 생성하는 Factory 역할을 한다. DB와의 연결을 유지하며 여러 EntityManager 인스턴스를 생성할 수 있다.

주요 특징

  • 어플리케이션 실행시, 한번만 생성후, 이후 공유 자원으로써 사용된다
  • Thread - Safe 하다
  • 비용이 크기 때문에, 어플리케이션 종료시 close 해주어야 한다
EntityManagerFactory emf = Persistence.createEntityManagerFactory("myPersistenceUnit");
EntityManager em = emf.createEntityManager();

em.getTransaction().begin();  // 트랜잭션 시작

// 엔터티 저장
User user = new User();
user.setName("John Doe");
em.persist(user);  

em.getTransaction().commit(); // 트랜잭션 커밋
em.close();  // EntityManager 종료
emf.close(); // EntityManagerFactory 종료

2. 영속성 컨텍스트 (Persistence Context)

영속성 컨텍스트는 JPA 에서 엔티티를 효율적으로 관리하기 위해 제공하는 핵심적 개념이다. 즉, 엔티티 객체드를 관리하는 논리적인 캐시 또는 작업 공간이다.

2.1 핵심 역할

  • 1차 캐시
    • 영속성 컨텍스트는 엔티티 객체를 메모리에 캐싱한다
    • 같은 Transaction 내에서 동일한 엔티티를 여러 번 조회할 때, DB에 직접 접근하는 것이 아닌 캐시에서 가져온다
  • 변경 감지 (Dirty Checking)
    • 영속성 컨텍스트는 엔티티의 변경 사항을 추적한다
    • Transaction Commit 시점에 변경된 엔티티만 DB에 업데이트 한다
  • 쓰기 지연 (Write Behind)
    • 엔티티를 DB에 즉시 반영하는 것이 아닌, Transaction 커밋 시점까지 영속성 컨텍스트에 보관한다
    • Transaction Commit 시점에 한번에 여러 쿼리를 실행하여 DB 부하를 줄인다
  • 동일성 보장 (Identity Management)
    • 영속성 컨텍스트 내에서는 같은 엔티티를 나타내는 객체가 항상 동일함을 보장한다.
  • 트렌젝션 지원 (Transaction support )
    • 영속성 컨텍스트는 Transaction 범위 내에서 동작하며, Transaction 의 ACID 속성을 보장한다
  • 영속 상태 관리
    • 엔티티 생명 주기 를 관리하고 각 상태에 따른 적절한 동작을 수행한다.

2.2 영속성 컨텍스트의 생명 주기

  1. EntityManager 생성: Entity Manager를 생성하면 영속성 컨텍스트가 함께 생성된다.
  2. 트랜잭션 시작: 트랜잭션을 시작하면 영속성 컨텍스트가 활성화된다.
  3. 엔티티 관리: EntityManager를 통해 엔티티를 영속성 컨텍스트에 저장, 조회, 수정, 삭제한다.
  4. 트랜잭션 커밋/롤백:
    • 커밋: 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하고 트랜잭션을 종료
    • 롤백: 영속성 컨텍스트의 변경 내용을 무효화하고 트랜잭션을 종료
  5. EntityManager 종료: EntityManager를 종료하면 영속성 컨텍스트도 함께 종료

2.3 영속성 컨텍스트의 범위

웹 요청에선 Request 하나당 영속성 컨텍스트 하나를 사용하는것이 일반적이다. 영속성 컨텍스트를 어디까지 유지할 지에 따라 전략 (OSIV 등 ) 이 달리질 수 있다.

  • OSIV : 일반적으로 JPA에선 트렌젝션이 종료되면, 영속성 컨텍스트도 종료된다.
  • 하지만, OSIV가 활성화되면, 트렌젝션이 끝나도 영속성 컨텍스트가 유지된다.
  • 따라서, Lazy Loading 등이 controller 나 view 에서 가능해진다.
    • DB Connection 부족 문제, 대량의 Lazy loading 시 성능 저하
  • 하지만 OSIV가 활성화되면 트랜잭션이 종료된 후에도 DB 커넥션이 유지될 수 있어 성능에 악영향을 미칠 수 있다.

예를 들어

@RestController
public class OrderController {
    @PersistenceContext
    private EntityManager em;

    @GetMapping("/orders")
    public List<Order> getOrders() {
        return em.createQuery("SELECT o FROM Order o", Order.class).getResultList();
    }
}

와 같은 상황에서, Order 엔티티가, User , Product 등을 Lazy Loading 으로 참조하고 있다면? -> 각각 추가적인 쿼리가 실행된다. OSIV가 활성화된 상태에서는 Lazy Loading이 가능하지만, 트랜잭션이 종료된 후 추가적인 쿼리가 실행될 수 있어 주의가 필요하다.

4. 엔티티의 생명 주기

4.1 비영속 상태 (New / Transient)

이 시점의 객체는 JPA 의 영속성 컨텍스트에 의해 관리되지 않는 상태를 의미한다.

즉 EntityManager 와 연결되지 않은 상태로, 아직 DB와 아무 관계가 없는 상태이다.

특징

  • new 키워드로 생성했지만, persist() 를 호출하지 않은 상태
  • Transaction 과도 무관하며, 언제든지 GC 의 대상이 될 수 있다.

4.2 영속 상태 (Managed / Persistent)

영속 상태란 엔티티가 영속성 컨텍스트에 의해 관리되는 상태를 의미한다. 이 상태에선, 1차 캐시, 변경 감지, 쓰기 지연, 지연 로딩 등 다양한 기능이 동작한다. 즉, Entity Manager 가 해당 객체를 추적 하는 상태이며, Transaction 이 종료될때, 변경 사항을 자동으로 반영한다.

✔ JPA가 엔티티를 관리하는 상태
persist(), find(), merge() 를 통해 영속성 컨텍스트에 등록 할 수 있으며, 영속 상태가 된다
✔ Transaction Commit -> Dirty Checking 을 통한 DB 반영
remove() , detach() 를 호출하기 전까지 엔티티는 관리된다.

// 1. 비영속 상태
User user = new User();
user.setName("John Doe"); // 아직 DB와 관계없음 (비영속 상태)

// 2. persist()를 호출하면 영속 상태로 변경됨
em.persist(user);

System.out.println(em.contains(user)); // true (영속 상태)

4.2.1 영속상태의 주요 특징

✅ 1차 캐시

  • 엔티티를 조회할때, 우선 영속성 컨텍스트 에서 찾는다.
  • 동일한 Transaction내에서 같은 엔티티를 다시 조회해도 DB에 접근하는 것이 아닌 영속성 컨텍스트에서 찾는다.

✅ 변경 감지 (Dirty Checking)

  • 영속 상태의 엔티티 값이 변경되면, 자동으로 UPDATE SQL 이 실행된다.
    User user = em.find(User.class, 1L); // 영속 상태
    user.setName("Updated Name"); // 값 변경
    em.getTransaction().commit(); // 변경 감지 → UPDATE 실행
     
  • 즉, em.update() 와 같은 메서드가 필요하지 않다. 

 

 

✅ 지연 로딩 (Lazy Loading)
- `fetch = FetchType.LAZY` 로 설정하여 연관된 엔티티는 실제 사용할 때 까지 쿼리를 실행하지 않는다. 필요한 시점에 **Proxy객체** 를 통해 DB 에서 데이터를 가져온다

User user = em.find(User.class, 1L); // User 엔티티 조회 (DB 접근)
System.out.println(user.getOrders()); // 이 시점에서 ORDER 테이블 조회

 

✅ 쓰기 지연 (Write - Behind)

  • em.persist() 호출시 ,즉시 DB 엔 반영되는 것이 아닌, commit 시점에 INSERT 문이 실행된다.
    EntityTransaction tx = em.getTransaction();
    tx.begin(); 
    

User user1 = new User("Alice");
User user2 = new User("Bob");

em.persist(user1);
em.persist(user2);

// 아직 INSERT SQL 실행 X (쓰기 지연 저장소에 쌓임)

tx.commit(); // 여기서 한 번에 INSERT 실행 🚀



✅ Flush
- Flush 와 Commit 의 차이
    - Flush : Transaction 유지, DB 반영
    - Commit : DB 반영, Transaction 종료 
- 영속성 컨텍스트의 내용을 DB에 반영하는 작업
- Transaction이 커밋되지 않아도 특정 시점에서 자동 실행될 수 있음
    - commit 호출시
    - JPQL 실행 시점
    - flush 강제 호출시
        - `flush()` 이후 `rollback()` 호출시 변경사항 취소
- Flush 시점 설정하기
    - AUTO : 기본값
    - COMMIT : 기존에 JPQL 실행시 자동으로 되던 FLUSH 를 방지한다



### 4.3 준영속 상태 (Detached)

이 상태의 객체는 JPA 의 영속성 컨텍스트에서 분리되어 있는 상태를 의미한다.

즉 더이상 Dirty Checking, 1차 캐시, Lazy Loading 등의 기능이 동작하지 않는다

`detach()` : 특정 엔티티를 준영속 상태로 만든다
`clear()` : 영속성 컨텍스트 초기화, 모든 엔티티가 준영속 상태가 된다
`close()` : 영속성 컨텍스트 종료, 모든 엔티티가 준영속 상태가 된다.

`merge()` : 준영속 상태의 객체를 다시 영속 상태로 만든다. 

**주의** : merge 는 준영속 상태의 엔티티를 받아 새로운 영속 상태의 엔티티를 반환하는 것이다. 즉, merge 이후엔, 반환된 영속 엔티티를 사용해야 한다. 

```java
User detachedUser = em.find(User.class, 1L); // 영속 상태
em.detach(detachedUser); // 준영속 상태

User mergedUser = em.merge(detachedUser); // 새로운 영속 상태 객체 반환

System.out.println(em.contains(detachedUser)); // false (여전히 준영속)
System.out.println(em.contains(mergedUser)); // true (새로운 영속 객체)

4.4 삭제 상태 (Removed)

이 상태의 객체는 더이상 영속성 컨텍스트에서 관리되지 않으며, Transaction commit 시, 실제로 DB 에서 삭제 되는 상태가 된다.

remove(entity) 를 호출하면, 엔티티가 removed 상태가 된다.

Transaction commit 시, 실제 DELETE SQL 이 실행된다.

1차 캐시에서도 삭제된다.