JPA와 영속성 컨텍스트 (Persistence Context)

StrayCat·2026년 2월 15일

영속성 컨텍스트란, Entity를 영구 저장 (Commit)하기 전 JPA가 관리하는 "1차 메모리 공간" 개념이다. (DB와 애플리케이션 사이 중간 계층)


JPA 전체 구조에서의 위치

애플리케이션 코드
    ↕
EntityManager  ←── 영속성 컨텍스트 (1차 캐시, 변경 감지, 쓰기 지연 등)
    ↕
JDBC / SQL
    ↕
데이터베이스
  • 영속성 컨텍스트는 EntityManager를 통해 관리됨을 기억
  • em.persist(), em.find() 등의 메서드로 호출된 객체는 바로 DB에 가지 않고 1차적으로 영속성 컨텍스트를 거치게 됨

엔티티의 4가지 생명주기

 비영속 (new/transient)
   │  em.persist()
   ▼
 영속 (managed)  ←──── em.find(), JPQL 조회
   │                    
   │  em.detach() / clear() / close()
   ▼
 준영속 (detached)
   
 영속 → em.remove() → 삭제 (removed)
상태설명
비영속new Member()만 한 상태. JPA는 모름
영속영속성 컨텍스트에 등록된 상태. 변경 감지 (Dirty Checking) 대상에 포함
준영속영속성 컨텍스트에서 분리된 상태. 더 이상 관리되지 않음
삭제삭제 요청이 등록된 상태. flush()시 DELETE SQL로 삭제

Q. 비영속 상태와 준영속 상태는 어떤 차이가 있나요?

  • 둘 다 "영속성 컨텍스트가 관리하지 않는 상태"라는 점은 동일
  • 둘 다 변경감지 대상에 포함되지 않음
  • 차이는 과거 영속성 컨텍스트 포함 이력의 유무

비영속

Member member = new Member();
member.setName("홍길동");
// 그냥 자바 객체일 뿐. JPA는 이 객체의 존재를 모릅니다.
  • new로 생성만 한 순수 자바 객체
  • 식별자(@Id)가 매핑되지 않았을 수 있음
  • DB에 한 번도 저장된 적 없음
  • 영속성 컨텍스트를 거친 적이 없는 상태

준영속 (Detached)

Member member = em.find(Member.class, 1L); // 영속 상태
em.detach(member);                         // 준영속 상태로 전환
// 또는 em.clear(), em.close()
  • 한 때 영속 상태였던 객체
  • 식별자 (@Id)값이 확실히 있음 (DB에서 조회 성공으로 가져오거나 persiste 된 적이 있으므로)
  • 영속성 컨텍스트에서는 분리된 상태

비교 정리

구분비영속준영속
JPA 관리 이력없음있음
@Id값없을 수도 있음반드시 있음
DB 대응 데이터없을 가능성 높음있을 가능성 높음
변경 감지 대상 여부XX
지연 로딩 대상 여부XX
em.merge() 호출 시INSERTUPDATE(또는 SELECT 후 병합)

em.merge()로 보는 실질적 차이

// 비영속 객체 merge → ID가 없으므로 INSERT
Member newMember = new Member();
newMember.setName("새 회원");
em.merge(newMember); // → INSERT

// 준영속 객체 merge → ID가 있으므로 SELECT + UPDATE
Member detached = em.find(Member.class, 1L);
em.detach(detached);
detached.setName("수정된 이름");
em.merge(detached); // → SELECT 후 변경사항 병합 → UPDATE
  • JPA는 merge()시 @Id값의 존재 여부를 체크 후 이 객체가 새로운 것인지, 기존 데이터인지를 판단

영속성 컨텍스트의 핵심 기능

1. 1차 캐시

Member member = em.find(Member.class, 1L); // DB 조회 → 1차 캐시에 저장
Member member2 = em.find(Member.class, 1L); // DB 안 감. 캐시에서 반환

System.out.println(member == member2); // true (동일성 보장)
  • 같은 트랜잭션 내에서 같은 엔티티를 반복 조회해도 SELECT는 1번만 나감

2. 쓰기 지연 (Write-Behind)

em.persist(memberA);  // INSERT SQL을 바로 보내지 않음
em.persist(memberB);  // 아직 안 보냄

transaction.commit(); // 이 시점에 INSERT 2개가 한꺼번에 flush
  • SQL을 모아두었다 커밋 시점에 한 번에 전송. 네트워크 비용 절감

3. 변경 감지 (Dirty Checking)

Member member = em.find(Member.class, 1L);
member.setName("변경된 이름");  // setter만 호출

// em.update() 같은 메서드가 필요 없습니다! (실제로 존재하지 않음)
transaction.commit(); // flush 시점에 스냅샷과 비교 → UPDATE SQL 자동 생성

4. 지연 로딩 (Lazy Loading)

  • 연관된 엔티티를 실제로 접근하는 시점에 쿼리를 날림
  • 이는 영속성 컨텍스트가 프록시 객체를 관리하기 때문에 가능한 기능

Q. 프록시 객체란 무엇인가?

A. 실제 엔티티를 상속한 "가짜 객체"로, 실제 데이터가 필요한 시점까지 DB조회를 미루기 위해 존재함

프록시 객체는 왜 필요한가?

  • 예를 들어, MemberTeam을 참조하고 있다고 가정
@Entity
public class Member {
    @ManyToOne(fetch = FetchType.LAZY)
    private Team team;
}
Member member = em.find(Member.class, 1L);
// 이 시점에 Team을 같이 조회할 필요가 있을까?

// 만약 member.getName()만 쓴다면 Team 조회는 낭비
// 그래서 team 자리에 "프록시 객체"를 넣어둠

member.getTeam();           // 아직 프록시 객체 상태. SELECT 안 나감
member.getTeam().getName(); // 이 시점에 비로소 SELECT 실행!

결국, 불필요한 쿼리를 줄이기 위한 지연 로딩(Lazy Loading)의 핵심 수단이 프록시

  • 프록시는 실제 엔티티 클래스를 상속받아 만들어짐 (Hibernate가 런타임에 자동 생성)
  • 내부에 실제 엔티티를 가지는 target 참조를 가지고 있음
  • 처음에는 target이 비어있고, 실제 데이터에 접근하는 순간 DB를 조회하여 채움 -> 이를 프록시 초기화라고 함

EntityManager

EntityManager란?

영속성 컨텍스트를 조작하는 인터페이스. 엔티티의 생명주기를 관리하고, DB와의 상호작용을 담당

EntityManager 생성 방법

순수 JPA환경

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myUnit");
EntityManager em = emf.createEntityManager();
EntityTransaction et = em.getTransaction();

EntityManagerFactory는 애플리케이션 전체에서 하나만 생성하고, EntityManager는 요청(트랜잭션)마다 생성 -> EMF(EntityManagerFactory) 생성 비용이 크기 때문

  • 따라서 EMF는 싱글톤으로 1개만, EM은 트랜잭션/요청마다 새로 생성하는 패턴

트랜잭션과 영속성 컨텍스트의 관계

기본 작동 흐름

et.begin()
    │
    ├── em.persist(entity)   ─┐
    ├── em.find(...)          │→ 모두 영속성 컨텍스트 안에서 동작
    ├── entity.setName(...)   │
    ├── em.remove(entity)    ─┘
    │
    ├── em.flush()  ← SQL 전송 (아직 확정은 아님)
    │
et.commit()  ← flush + DB 확정
    또는
et.rollback()  ← 모든 변경 취소
  • 핵심 원칙 : JPA의 모든 데이터 변경은 트랜잭션 안에서 이루어져야 한다

Action Queue (쓰기 지연 SQL 저장소)

영속성 컨텍스트 내부에는 Action Queue 공간이 존재함

영속성 컨텍스트
┌──────────────────────────────────┐
│                                 │
│   1차 캐시                        │
│   ┌────────────┬──────────┐      │
│   │  @Id       │ Entity   │      │
│   │  1L        │ memberA  │      │
│   │  2L        │ memberB  │      │
│   └────────────┴──────────┘      │
│                                  │
│   Action Queue (쓰기 지연 저장소)  │
│   ┌──────────────────────┐       │
│   │ INSERT INTO member...│       │
│   │ INSERT INTO member...│       │
│   │ UPDATE member SET... │       │
│   │ DELETE FROM member...│       │
│   └──────────────────────┘       │
│                                  │
└──────────────────────────────────┘

persist(), remove(), 변경 감지 (Dirty Checking)로 생성된 SQL들은 즉시 DB에 보내지 않고 Action Queue에 쌓임. 그 후 flush() 시점에 한꺼번에 DB로 전송됨

  • 트랜잭션의 본질은 DB에서 '이 작업들을 하나의 단위로 묶어서, 전부 성공하거나 전부 실패하게 보장한다라는 것

EntityTransaction 메서드

et.begin();
  • 트랜잭션을 시작합니다. 이 시점부터 영속성 컨텍스트가 활성화되고, 이후 모든 작업이 하나의 트랜잭션 단위로 묶입니다.
et.commit(); // 내부적으로 flush 호출 -> DB 확정
  • flush + DB 확정을 수행. Action Queue의 SQL을 db에 보내고, DB 트랜잭션을 commit합니다. 이 이후에는 되돌릴 수 없습니다.
et.rollback();
  • 트랜잭션 내 모든 변경을 취소합니다. Action Queue의 SQL도 버려지고, DB에 아무런 영향을 주지 않습니다. 예외 발생 시 반드시 호출해야 합니다.
EntityTransaction et = em.getTransaction();
try {
    et.begin();
    // 비즈니스 로직
    et.commit();
} catch (Exception e) {
    et.rollback(); // 문제 발생 시 롤백
} finally {
    em.close();
}

EntityManager 메서드 상세정리

// em.persist(entity)

Member member = new Member();
member.setName("홍길동");

em.persist(member); // 비영속 → 영속
  • 엔티티를 영속성 컨텍스트에 등록합니다
  • 1차 캐시에 저장하고, Action Queue에 INSERT SQL을 예약합니다
  • 이 시점에는 DB에 SQL을 적용하지 않음
  • @GeneratedValue(stratege = IDENTITY) 전략의 경우 예외적으로 persist 시점에 즉시 INSERT가 실행됨 (DB에서 ID를 받아와야 1차 캐시에 저장할 수 있기 때문)
// em.find(Class, id)

Member member = em.find(Member.class, 1L);
  • 1차 캐시를 먼저 확인
  • 캐시에 없으면 DB에 SELECT -> 결과를 1차 캐시에 저장 -> 반환
  • 캐시에 있으면 DB 조회 없이 바로 반환 (성능 이점)
// em.remove(entity)

Member member = em.find(Member.class, 1L);
em.remove(member); // 영속 → 삭제
  • 엔티티를 삭제 상태로 전환 (삭제가 아님)
  • Action Queue에 DELETE SQL을 예약
  • flush 시점에 DELETE가 DB로 전송
  • 영속 상태의 엔티티만 remove 할수 있음
// em.flush()

em.flush();
  • Action Queue에 쌓인 SQL을 DB에 전송
  • 변경 감지 (Dirty Checking)도 이 시점에 동작
  • 단, DB 트랜잭션을 커밋하지는 않음
  • 영속성 컨텍스트는 close되지 않고, 그대로 유지

flush()가 자동 호출되는 시점은 아래 3가지임

    1. et.commit() 호출 시
    1. JPQL 쿼리 실행 시 (데이터 정합성을 위해)
    1. em.flush() 직접 호출 시
// em.detach(entity)

Member member = em.find(Member.class, 1L); // 영속
em.detach(member); // 영속 → 준영속

member.setName("변경"); // 변경 감지 안 됨. UPDATE 안 나감.
  • 특정 엔티티 하나만 영속성 컨텍스트에서 분리
  • 1차 캐시에서 제거되고, Action Queue에 해당 엔티티 관련 SQL도 제거
  • 이후 변경 감지, 지연 로딩 등 모든 JPA 기능이 동작하지 않음
// em.merge(entity)

Member detached = em.find(Member.class, 1L);
em.detach(detached);

detached.setName("새 이름");
Member merged = em.merge(detached); // 준영속 → 새로운 영속 엔티티 반환
  • 준영속 (또는 비영속)엔티티를 영속 상태로 병합
  • 동작 과정 :
    1. 1차 캐시에서 해당 ID의 엔티티를 찾음
    1. 없으면 DB에서 SELECT
    2. 조회된 영속 엔티티에 파라미터 엔티티의 값을 복사
    3. 그 영속 엔티티를 반환
  • 주의 : 원본 detached객체는 여전히 준영속 상태. 반환된 merged객체가 영속 상태임을 기억
em.merge(detached);
System.out.println(em.contains(detached)); // false (여전히 준영속)
System.out.println(em.contains(merged));   // true  (이게 영속)
// em.contaions(entity)

boolean isManaged = em.contains(member);
  • 해당 엔티티가 현재 영속성 컨텍스트에서 관리되고 있는지 확인
  • 영속 상태면 true, 그 외(비영속, 준영속, 삭제)면 false 반환
  • 디버깅이나 상태 확인 시 유용
// em.clear()

em.clear();
  • 영속성 컨텍스트를 완전히 초기화
  • 모든 엔티티가 준영속 상태로 전환
  • 1차 캐시, Action Queue 모두 비워짐
  • EntityManager 자체는 살아있으므로 이후 재사용 가능
// em.close()

em.close();
  • EntityManager 자체를 종료
  • 영속성 컨텍스트도 함께 소명
  • 이후 이 EntityManager로는 작업 불가능

헷갈리기 쉬운 비교

flush vs commit

구분flushcommit
SQL 전송가능가능 (내부에서 flush 호출)
DB 확정불가능가능
롤백 가능 여부가능불가능
영속성 컨텍스트유지유지 (해당 트랜잭션은 종료)
em.clear(); // 캐시만 비움. em은 재사용 가능
em.find(Member.class, 1L); // ✅ 정상 동작 (DB에서 새로 조회)

em.close(); // em 자체가 죽음
em.find(Member.class, 1L); // 💥 예외 발생

전체 흐름

EntityManagerFactory emf (애플리케이션당 1)
    │
    │  emf.createEntityManager()EntityManager em (요청/트랜잭션마다 생성)
    │
    │  em.getTransaction().begin()
    ▼
┌─── 영속성 컨텍스트 활성화 ──────────────────┐
│                                             │
│  em.persist(entity)1차 캐시 + INSERT 예약 │
│  em.find(id)         → 캐시 우선 → DB 조회   │
│  entity.setXxx()     → 변경 감지 대상        │
│  em.remove(entity)   → DELETE 예약           │
│                                             │
│  em.flush()          → SQL 전송 (미확정)     │
│  em.detach(entity)   → 특정 엔티티 분리      │
│  em.merge(entity)    → 준영속 → 영속 병합    │
│  em.contains(entity) → 영속 여부 확인        │
│  em.clear()          → 컨텍스트 초기화       │
│                                             │
└─────────────────────────────────────────────┘
    │
    ├── et.commit()    → flush + DB 확정
    │   또는
    ├── et.rollback()  → 모든 변경 취소
    │
    ▼
  em.close()EntityManager 종료

Spring 환경에서는?

일련의 과정을 Spring이 자동 처리

@Service
@Transactional // → begin + commit/rollback + close 자동
public class MemberService {

    @PersistenceContext
    private EntityManager em; // → 자동 주입

    public void updateMember(Long id, String name) {
        Member member = em.find(Member.class, id); // 영속
        member.setName(name); // 변경 감지 → 트랜잭션 끝나면 자동 UPDATE
        // persist나 merge 호출 불필요
    }
}
  • @Transactionalbegin → commit / rollback → close를 모두 감싸주기 때문에, 순수 JPA의 보일러플레이트 코드가 사라짐
  • 하지만 내부에서 무슨 일이 일어나는지 이해하고 있어야 문제 상황에서 올바르게 대처할 수 있음
profile
알면 좋은 것보단 잊어버리기 싫은 것들을 기록합니다.

0개의 댓글