영속성 컨텍스트란, Entity를 영구 저장 (Commit)하기 전 JPA가 관리하는 "1차 메모리 공간" 개념이다. (DB와 애플리케이션 사이 중간 계층)
애플리케이션 코드
↕
EntityManager ←── 영속성 컨텍스트 (1차 캐시, 변경 감지, 쓰기 지연 등)
↕
JDBC / SQL
↕
데이터베이스
EntityManager를 통해 관리됨을 기억em.persist(), em.find() 등의 메서드로 호출된 객체는 바로 DB에 가지 않고 1차적으로 영속성 컨텍스트를 거치게 됨 비영속 (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로 생성만 한 순수 자바 객체Member member = em.find(Member.class, 1L); // 영속 상태
em.detach(member); // 준영속 상태로 전환
// 또는 em.clear(), em.close()
| 구분 | 비영속 | 준영속 |
|---|---|---|
| JPA 관리 이력 | 없음 | 있음 |
| @Id값 | 없을 수도 있음 | 반드시 있음 |
| DB 대응 데이터 | 없을 가능성 높음 | 있을 가능성 높음 |
| 변경 감지 대상 여부 | X | X |
| 지연 로딩 대상 여부 | X | X |
em.merge() 호출 시 | INSERT | UPDATE(또는 SELECT 후 병합) |
// 비영속 객체 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
merge()시 @Id값의 존재 여부를 체크 후 이 객체가 새로운 것인지, 기존 데이터인지를 판단Member member = em.find(Member.class, 1L); // DB 조회 → 1차 캐시에 저장
Member member2 = em.find(Member.class, 1L); // DB 안 감. 캐시에서 반환
System.out.println(member == member2); // true (동일성 보장)
em.persist(memberA); // INSERT SQL을 바로 보내지 않음
em.persist(memberB); // 아직 안 보냄
transaction.commit(); // 이 시점에 INSERT 2개가 한꺼번에 flush
Member member = em.find(Member.class, 1L);
member.setName("변경된 이름"); // setter만 호출
// em.update() 같은 메서드가 필요 없습니다! (실제로 존재하지 않음)
transaction.commit(); // flush 시점에 스냅샷과 비교 → UPDATE SQL 자동 생성
Q. 프록시 객체란 무엇인가?
A. 실제 엔티티를 상속한 "가짜 객체"로, 실제 데이터가 필요한 시점까지 DB조회를 미루기 위해 존재함
프록시 객체는 왜 필요한가?
Member가 Team을 참조하고 있다고 가정@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)의 핵심 수단이 프록시
영속성 컨텍스트를 조작하는 인터페이스. 엔티티의 생명주기를 관리하고, DB와의 상호작용을 담당
순수 JPA환경
EntityManagerFactory emf = Persistence.createEntityManagerFactory("myUnit");
EntityManager em = emf.createEntityManager();
EntityTransaction et = em.getTransaction();
EntityManagerFactory는 애플리케이션 전체에서 하나만 생성하고, EntityManager는 요청(트랜잭션)마다 생성 -> EMF(EntityManagerFactory) 생성 비용이 크기 때문
et.begin()
│
├── em.persist(entity) ─┐
├── em.find(...) │→ 모두 영속성 컨텍스트 안에서 동작
├── entity.setName(...) │
├── em.remove(entity) ─┘
│
├── em.flush() ← SQL 전송 (아직 확정은 아님)
│
et.commit() ← flush + DB 확정
또는
et.rollback() ← 모든 변경 취소
영속성 컨텍스트 내부에는 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로 전송됨
et.begin();
et.commit(); // 내부적으로 flush 호출 -> DB 확정
et.rollback();
EntityTransaction et = em.getTransaction();
try {
et.begin();
// 비즈니스 로직
et.commit();
} catch (Exception e) {
et.rollback(); // 문제 발생 시 롤백
} finally {
em.close();
}
// em.persist(entity)
Member member = new Member();
member.setName("홍길동");
em.persist(member); // 비영속 → 영속
@GeneratedValue(stratege = IDENTITY) 전략의 경우 예외적으로 persist 시점에 즉시 INSERT가 실행됨 (DB에서 ID를 받아와야 1차 캐시에 저장할 수 있기 때문)// em.find(Class, id)
Member member = em.find(Member.class, 1L);
// em.remove(entity)
Member member = em.find(Member.class, 1L);
em.remove(member); // 영속 → 삭제
// em.flush()
em.flush();
flush()가 자동 호출되는 시점은 아래 3가지임
et.commit() 호출 시em.flush() 직접 호출 시// em.detach(entity)
Member member = em.find(Member.class, 1L); // 영속
em.detach(member); // 영속 → 준영속
member.setName("변경"); // 변경 감지 안 됨. UPDATE 안 나감.
// em.merge(entity)
Member detached = em.find(Member.class, 1L);
em.detach(detached);
detached.setName("새 이름");
Member merged = em.merge(detached); // 준영속 → 새로운 영속 엔티티 반환
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();
// em.close()
em.close();
| 구분 | flush | commit |
|---|---|---|
| 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이 자동 처리
@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 호출 불필요
}
}
@Transactional이 begin → commit / rollback → close를 모두 감싸주기 때문에, 순수 JPA의 보일러플레이트 코드가 사라짐