springboot

[SpringBoot] 영속성 컨텍스트, 그리고 EntityManager

inhooo00 2025. 1. 9. 02:46

📍영속성 컨텍스트와 EntityManager? 왜 공부해야하지?

영속성 컨텍스트와 EntityManager은 JPA의 핵심 개념이다.
이 둘을 모른다면 JPA가 주는 이점을 100% 누릴 수 없다.
(SQL을 사용하는 것보다 못한 상황이 일어난다)

 

 

 

📍영속성 컨텍스트(PersistenceContext)

Entity를 영구 저장하는 환경이라는 뜻

  • 눈에 보이지 않는 논리적 개념
  • 애플리케이션과 DB 사이 객체를 보관하는 가상의 DB로 생각하기
  • EntityManager를 통해 Entity를 영속성 컨텍스트에 보관, 관리

 

 

 

📍EntityManager

EntityManager를 통해 영속성 컨텍스트에 접근하고 관리

  • EntityManager를 생성하면 그 안에 영속성 컨텍스트 있음
    • EntityManagerFactory통해 요청이 올 때 마다 EntityManager 생성
    • EntityManager는 내부적으로 Connection 사용하여 DB 접근

spring에서 EntityManager 여러 개, 영속성 컨텍스트 1개 존재

  • 엔티티 매니저:영속성 컨텍스트 = N : 1

 

 

📍엔티티의 생명주기

entity는 EntityManager를 통해 영속성 컨텍스트에 보관, 관리, 제거된다. 그에 따른 엔티티에 생명주기를 알아보자.

👀 EntityManager 생성

  • EntityManagerFactory
    • 설정 파일에서(ex. Persistence.xml) 작성한 psersistence-unit(DB 설정 정보)으로 생성한다.
  • EntityManager
    • 위에서 생성한 EntityManagerFactory에서 생성이 가능하다.
// hello는 psersistence-unit 이름
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

// 이를 통해 엔티티를 영속성 컨텍스트에 관리 가능
EntityManager em = emf.createEntityManager();

 

👀 엔티티의 생명주기

비영속(new)

Member member = new Member(); // 엔티티(객체) 생성
  • 객체 생성 단계.
  • 엔티티가 영속성 컨텍스트에 없는 상태 (= 영속성 컨텍스트와 관계 x)

 

영속(managed)

em.persist(member);
  • 엔티티가 영속성 컨텍스트에 저장된다.
  • 즉, 영속성 컨텍스트가 엔티티를 관리하는 로직.

 

준영속(detached)

em.detach(member);
  • 엔티티가 영속성 컨텍스트에 저장되었다가 분리된 상태
    • 엔티티 영속 상태에서 준영속 상태로 간 상태!
  • 영속성 컨텍스트가 엔티티 관리하지 않음
    • 영속성 컨텍스트가 제공하는 기능 사용하지 못함
  • 준영속 상태로 만드는 법
    • em.detach(entity);
      • 특정 엔티티만 준영속 상태로 변환
    • em.clear()
      • 영속성 컨텍스트 초기화(다 지움)
    • em.close()
      • 영속성 컨텍스트 종료

 

삭제(removed)

em.remove(member);
  • 영속성 컨텍스트에 있는 엔티티를 삭제
  • 이때 DB에서도 삭제
    • 주의: 트랜잭션이 끝나거나 EntityManger를 플러시 하여 삭제 쿼리가 나가야 DB에서 삭제된다.

 

 

📍뭔지는 알겠는데 왜 사용해야 할까?

영속성 컨텍스트의 장점은 다음과 같다. 하나씩 알아보자.
1. 1차 캐시
2. 동일성(identity) 보장
3. 트랜잭션을 지원하는 쓰기 지연
4. 변경 감지
5. 지연 로딩

 

 

 

✔️ 1차 캐시

영속성 컨텍스트에 1차 캐시가 존재한다.
  • 캐시는 map 형태로 key-value 저장되어진다.
    • key: DB의 PK(기본 키)
    • value: 객체
  • JPA의 역할은 영속성 컨텍스트에 1차 캐시를 확인하는 것이다.
  • 1차 캐시에 없다면 그 때 DB로 가서 조회를 진행한다.
  • DB에 데이터 있으면 가져와서 1차 캐시에 저장 후 데이터를 반환해준다.
사실 1차 캐시는 큰 도움이 되지 않는다.
이유1. 1차 캐시가 있는 EntityManager는 트랜잭션 단위로 만들고 사라진다. 즉, 1차 캐시가 살아있는 시간은 매우 짧아 성능에 큰 효과는 없다.
이유2. 트랜잭션마다 각자 EntityManger를 사용한다. 즉, 각자 다른 영속성 컨텍스트와 1차 캐시를 가진다.

 

 

 

✔️ 동일성(identity) 보장

같은 트랜잭션 안에서 같은 객체는 == 비교를 보장해준다.
// 같은 PK 값 데이터 찾음
// 같은 데이터이지만 저장된 객체는 member1과 member2로 다름
Member member1 = em.find(Member.class, 1L);
Member member2 = em.find(Member.class, 1L);

// 이 때 == 비교를 하면?
System.out.println("result: "+(member1 == member2));

// 결과
result: true
JPA에서 동일성 보장은 같은 트랜잭션 안에서 동일한 엔티티 객체를 두 번 이상 조회할 때, 해당 엔티티가 동일한 객체로 처리된다는 것을 의미한다.
이렇게 함으로써 데이터의 일관성을 유지하고 불필요한 중복 객체 생성이나 데이터 변경 충돌을 방지한다!

 

 

 

✔️ 트랜잭션을 지원하는 쓰기 지연

"데이터베이스에 쿼리를 바로 보내지 않고, 한 트랜잭션 안에서 쿼리를 모아서 한 번에 보낸다."
feat: 트랜잭션(Transaction)은 데이터베이스에서 하나의 작업 단위를 의미하며, 데이터의 일관성과 무결성을 보장하기 위해 사용된다.

이 과정은 성능 최적화네트워크 부하 감소를 위해 사용되며 여기서 중요한 역할을 하는 것이 쓰기 지연 SQL 저장소다.

JPA는 필요할 때까지 DB와 통신하지 않는다. 대신 아래 두 가지 상황에서 쿼리를 실제로 DB로 보내게 된다.

 

 

  • 트랜잭션 커밋 시점 (transaction.commit())
    • 트랜잭션이 끝나면서 JPA가 쓰기 지연 SQL 저장소에 모아둔 모든 쿼리를 DB로 보냄.
    • 예를 들어, INSERT, UPDATE, DELETE 쿼리들이 한꺼번에 실행된다.
  • 플러시 시점 (em.flush())
    • 개발자가 강제로 em.flush()를 호출하면, 영속성 컨텍스트에 쌓인 SQL이 DB로 보내진다.
    • JPA가 필요하다고 판단하면 자동으로 flush를 호출하기도 함 (예: JPQL 실행 전).

 

 

✔️ 변경 감지(Dirty checking)

영속성 컨텍스트는 그 안에서 보관하는 데이터에 변경이 일어났는지 확인해준다.
  • 데이터 변경이 일어났다면?
    • JPA가 UPDATE 쿼리문 날려준다.
    • 즉, 값 변경 후 update 쿼리문 날릴 코드가 필요가 따로 없다.

!작동 순서!


트랜잭션 커밋이 발생하면

  1. flush() 호출
  2. 1차 캐시에서 entity와 스냅샷 비교
    • 스냅샷: 최초로 영속성 컨텍스트에 들어온 객체 상태
    • entity: 실제 값
  3. entity와 스냅샷이 다르다면 쓰기 지연 SQL 저장소에 update 쿼리문 추가
  4. DB에 update 쿼리문 반영 후 commit

 

 

✔️ 지연 로딩

  • 엔티티를 DB에서 가져올 때 만약 연관 관계를 가진 다른 객체를 가지고 있다면?
    • 즉시 로딩: 엔티티 조회할 때 연관된 객체도 함께 DB에서 조회
    • 지연 로딩: 연관된 객체는 조회x, 나중에 필요하면 그 때 DB에서 가져옴
  • 각 연관 관계마다 즉시 로딩, 지연 로딩 설정이 가능하다.

 

 

 

📍그래서 SpringBoot 안에서는 어떻게 쓰는건데?

import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
    // 필요한 경우 커스텀 쿼리 메서드 추가 가능
    Member findByName(String name);
}
스프링 부트를 사용하는 경우, JPA의 엔티티 매니저는 보통 위 코드처럼 Repository Layer에서 사용한다.
스프링 데이터 JPA가 내부적으로 EntityManager를 사용해 리포지토리를 구현하므로 개발자는 복잡한 엔티티 매니저 작업을 신경 쓸 필요가 없다! 
하지만 어떻게 굴러가는지 정확히 알고 쓰는 게 중요하겠지? ㅎㅎ