3. 영속성 관리
1. 영속성 컨텍스트
JPA를 공부하면서 가장 먼저 마주치게 되는 개념 중 하나가 영속성 컨텍스트(Persistence Context) 라고 해요.
영속성 컨텍스트는 "엔티티를 영구적으로 저장하고 관리하는 공간"입니다.
쉽게 말해, 자바 객체(Entity)를 데이터베이스와 연결된 상태로 관리해주는 JPA의 메모리 상 컨테이너 같은 거예요.
예를 들어 EntitiyManager.persist(entity); 코드를 통해 엔티티를 영속화(영속성 컨텍스트에 저장)할 수 있어요.
엔티티 매니저 팩토리와 엔티티 매니저
JPA에서 영속성 컨텍스트에 접근하려면 EntityManager라는 인터페이스를 사용해야 해요.
- EntityManager.persist(entity) → 객체를 영속성 컨텍스트에 저장(영속화)
- 내부적으로 DB 커넥션 풀과 연결되어 있지만,
- 영속성 컨텍스트 자체는 논리적인 메모리 공간이기 때문에 직접 보이진 않아요.
그리고 이 EntityManager는 EntityManagerFactory에서 만들어집니다.
EntityManagerFactory는 이름 그대로 엔티티 매니저를 만드는 공장이에요 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유해도 되지만 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안돼요!
즉, 전체 흐름은 이렇게 됩니다:
EntityManagerFactory → EntityManager → Persistence Context
환경에 따라 다른 구조
1. J2SE 환경 (일반 자바 애플리케이션)
- EntityManager 1개 → 영속성 컨텍스트 1개 (1:1)
- EntityManager를 새로 만들면 그 안에 새로운 컨텍스트도 같이 만들어집니다.
📌 즉, 요청마다 EntityManager를 새로 만들면 각각 독립된 컨텍스트를 갖게 돼요.
☕ 2. J2EE / 스프링 같은 컨테이너 환경
- 여러 EntityManager → 하나의 영속성 컨텍스트 (N:1)
- 트랜잭션 범위 안에서는 여러 EntityManager가 같은 컨텍스트를 공유합니다.
📌 이 구조 덕분에 AOP 기반 트랜잭션이나 스프링의 @Transactional 같은 기능이 깔끔하게 작동할 수 있대요 (고마워요 gpt!)
2. 엔티티의 생명주기 🌱
앞서 영속성 컨텍스트에 대해 알아봤다면, 이제부터는 엔티티가 어떤 상태로 변화하면서 관리되는지를 살펴볼 차례입니다.
JPA에서 엔티티는 총 4가지 상태를 가질 수 있어요
- 비영속 (new / transient)
- 영속 (managed)
- 준영속 (detached)
- 삭제 (removed)
1️⃣ 비영속 (new / transient)
아직 영속성 컨텍스트에 등록되지 않은, 그냥 자바 객체 상태예요. 이와 같이 회원 객체를 생성만 해둔 상태라고 보면 됩니다
Member member = new Member();
member.setName("코헤");
persist()를 호출하지 않았다면 이 상태는 비영속이에요. JPA 입장에서는 "이 친구는 내가 관리하는 애가 아니야~" 라는 상태죠.
2️⃣ 영속 (managed)
영속성 컨텍스트에 등록된 상태로, JPA가 직접 관리하는 상태예요.
em.persist(member);
이렇게 persist()를 호출하면, 이제 JPA는 해당 엔티티를 트랜잭션 동안 관리해요. 이를 EntityManager을 이용해서 영속화 해 둔 상태라고 보면 됩니다.
이 상태가 되면 1차 캐시에 올라가고, 변경 감지(Dirty Checking) 도 가능해져요.
참고: persist()했다고 해서 바로 DB에 INSERT 되는 건 아니고, 트랜잭션 커밋 시점에 flush 됩니다.
3️⃣ detached
원래 JPA가 관리하던 엔티티였는데, 이제는 연결이 끊긴 상태예요.
em.detach(member);
이 상태에서는 아무리 값을 바꿔도 JPA가 모른 척해요.
🧠 왜 필요하냐고요?
예를 들어 유저 정보를 DB에서 꺼내서 클라이언트 응답으로 넘기거나, 다른 계층으로 넘겨야 할 때,굳이 JPA가 관리할 필요가 없는 객체로 만들기 위해 준영속 상태로 둡니다.
🙃 파차 왈: "준영속은 쓰레기야."
→ 왜냐하면 관리도 안 되고, 다시 저장하려면 merge()나 persist()를 다시 써야 하니까요.
- ex. 유저 테이블에 코헤라는 데이터가 있다.
- remove는 db에서 지움
- 다시 managed를 해야지 insert가 되어야함
- but 코헤를 detached로 넣으면 따로 DB를 통해 넣을 필요가 없게끔~~
- 보통 DB 작업이 끝났으니 DB로 만든 entity를 쓸 다른 계층으로 쓸려면 detached를 씀
4️⃣ 삭제 (removed)
삭제 예약 상태입니다. DB에서 제거될 예정이에요.
em.remove(member);
삭제 요청을 넣으면 해당 엔티티는 removed 상태가 되고,
트랜잭션이 커밋되면 실제로 DB에서 DELETE가 발생합니다.
✍️ 참고: Hibernate 기준으로는 new 상태와 removed 상태를 굳이 구분하지 않습니다.
3. 영속성 컨텍스트의 특징
앞서 살펴본 것처럼 JPA의 핵심은 영속성 컨텍스트이고,
이 컨텍스트가 존재하기 때문에 JPA가 단순한 ORM을 넘어 성능 최적화나 트랜잭션 일관성까지 보장해줄 수 있어요.
(* 애플리케이션과 데이터베이스 사이에 영속성 컨텍스트라는 중간 계층이 하나 있다고 보면 된다.)
그럼, 영속성 컨텍스트가 우리에게 어떤 기능들을 제공하는지 하나씩 정리해볼게요.
1️⃣ 1차 캐시 (First-Level Cache)
영속성 컨텍스트(엔티티 매니저)는 내부에 1차 캐시를 갖고 있어요. em.persist()를 통해 엔티티를 영속화하면, 해당 객체는 메모리 상 1차 캐시에 저장됩니다.
em.persist(member); // 1차 캐시에 저장됨
이후 같은 엔티티를 조회할 때 DB에 바로 접근하지 않고, 1차 캐시에서 먼저 조회합니다.
Member findMember = em.find(Member.class, "member1"); // 1차 캐시 hit!
🔍 만약 1차 캐시에 없다면?
DB에서 조회하고, 조회한 결과를 다시 1차 캐시에 저장해요.
💡 주의: 트랜잭션 단위로 관리되기 때문에, 트랜잭션이 끝나면 1차 캐시도 날아갑니다. 즉, 애플리케이션 전역 캐시는 아니고, 그것은 2차 캐시라고 부릅니다.
2️⃣ 동일성 보장 (Identity Guarantee)
JPA는 동일한 트랜잭션 내에서 같은 엔티티는 동일 객체로 보장합니다.
(*영속 엔티티의 동일성을 보장함)
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); // true!
이건 JPA가 1차 캐시를 활용해 같은 객체를 반환하기 때문이에요.
→ 이 기능 덕분에 Repeatable Read (반복 읽기 등급) 수준의 트랜잭션 고립성도 애플리케이션 레벨에서 보장할 수 있어요.
3️⃣ 트랜잭션을 지원하는 쓰기 지연 (Transactional Write-Behind)
JPA는 데이터를 저장할 때마다 SQL을 바로 DB에 날리지 않아요.
쓰기 지연 저장소라는 영속성 컨텍스트 내부 버퍼에 INSERT/UPDATE 쿼리를 모아두고, 트랜잭션 커밋 시점에 한꺼번에 DB에 반영합니다.
매번 SQL을 싱행하게 되면 성능이 떨어져요.
여기서 작업공간이 두 개 있어요
- 영속성 콘텍스트 (JVM)
- 트랜잭션 맥락 (RDB) ⇒ 여기까지 가야 쿼리 가능. 이를 위해 flush()
커밋까지 가지 않고 flush()하는 이유에요
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 DB에 보내지 않는다.
transaction.commit(); // 트랜잭션 커밋
// flush = 트랜잭션을 커밋하는 순간 DB에 INSERT SQL을 보낸다.
💡 참고: hibernate.jdbc.batch_size 설정을 통해 배치 크기를 조절할 수 있어요.
대용량 처리 시 성능에 큰 영향을 줍니다.
4️⃣ 변경 감지 (Dirty Checking)
영속성 컨텍스트의 진짜 힘은 바로 이 기능!
영속성 컨텍스트에서 조회한 엔티티의 값을 바꾸면 트랜잭션이 커밋되는 시점에 알아서 update sql을 실행해줘요. 자바 컬렉션에서 하는 것처럼 편하게 코드를 작성하기만 하면 됩니다
Member member = em.find(Member.class, "member1");
member.setUsername("코헤"); // setter만 호출
// 별도 persist나 update 안 해도...
transaction.commit(); // 자동으로 UPDATE SQL 실행됨!
이게 가능한 이유는 다음과 같은 변경 감지 프로세스 때문이에요:
- 트랜잭션 커밋 시 flush() 호출
- 초기 상태 스냅샷과 현재 엔티티를 비교
여기서 스냅샷은 값을 읽어온 최초시점의 값을 의미해요 (DB에서 바로 꺼낸 따끈한 데이터~) - 변경된 필드가 있으면 UPDATE 쿼리 생성
- 쓰기 지연 저장소에 저장
- flush → commit!
덕분에 개발자는 em.update() 같은 걸 직접 호출할 필요가 없고,
마치 자바 객체(컬렉션) 쓰듯이 편하게 상태를 바꾸면 JPA가 알아서 반영해줘요.
5️⃣ 지연 로딩 (Lazy Loading)
이건 프록시 객체와 연관관계 설정에 대한 이야기라서,
별도로 다음 글에서 다룰 예정입니다 😎
4. 플러시
플러시(Flush) 는 영속성 컨텍스트의 변경 내용을 DB에 반영(동기화)하는 작업입니다. (커밋 아님)
JPA는 em.persist()나 em.remove() 같은 메서드를 호출해도 바로 SQL을 실행하지 않아요. 내부에 있는 쓰기 지연 저장소에 모아두었다가, 트랜잭션 커밋 시점에 한 번에 실행합니다.
- 위에서 말한 변경 감지(Dirty Checking)가 발생하고, 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록해요. 이후 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송해요. 이후 트랜잭션이 커밋됩니다.
- 플러시를 해도 영속성 컨텍스트와 1차 캐시가 날아가진 않아요. 위에 말한 것들이 DB에 반영되는 것뿐!
- 트랜잭션이라는 작업 단위가 중요하기 때문에 커밋 직전에만 동기화하면 됩니다.
em.persist(memberA);
em.persist(memberB);
// 아직 INSERT SQL은 실행되지 않음
transaction.commit();
// 커밋 시점에 flush → DB에 INSERT SQL 전송!
✍️ 플러시가 발생하는 시점
* 직접 쓸 일은 거의 없지만 테스트 할 때 쓸 수도 있으니 알아두면 좋겠져
- em.flush() 직접 호출 시
- 트랜잭션 커밋 시 (자동 발생)
- JPQL 쿼리 실행 시 (자동 발생)
em.persist(memberA);
em.persist(memberB);
// 중간에 JPQL 실행
List<Member> result = em.createQuery("select m from Member m", Member.class)
.getResultList();
// 이 시점에도 flush 자동 발생
💡 왜 JPQL 실행 시 자동 flush가 발생할까?
persist()만 하고 flush를 안 하면 아직 DB에 insert가 되지 않았기 때문에 JPQL로 select를 날렸을 때 데이터가 보이지 않는 문제가 생길 수 있어요. 그래서 JPQL 실행 전 flush를 강제하는 거예요.
참고 flush 옵션 설명
FlushModeType.AUTO (기본값) | 커밋 or 쿼리 실행 시 flush 발생 |
FlushModeType.COMMIT | 커밋 시에만 flush 발생
|
⚠️ 플러시는 동기화만 한다
flush()는 단순히 DB에 반영하는 작업일 뿐, 영속성 컨텍스트 자체(1차 캐시)는 그대로 유지됩니다. 즉, flush 후에도 관리 중인 엔티티는 계속 사용 가능해요.
5. 준영속 상태란? ( detached )
준영속(detached)은 원래 영속 상태였던 엔티티가 더 이상 영속성 컨텍스트에서 관리되지 않는 상태를 말해요.
* 영속 상태는 1차 캐시에 저장된 상태로 이해하면 됩니다~
언제 준영속 상태가 되나?
- em.detach(entity) → 특정 엔티티만 준영속화
- em.clear() → 영속성 컨텍스트 전체 초기화 (모든 엔티티가 detached)
- em.close() → 영속성 컨텍스트 종료 (컨텍스트 자체가 닫힘)
🔄 다시 영속 상태로 전환하려면?
em.merge(entity); // detached → managed
또는 새로 생성해서 persist()하면 되지만, 이미 DB에 존재하는 엔티티라면 merge()가 적절합니다.
⚠️ 준영속 상태의 특징
- 변경 감지 안 됨
- 1차 캐시에서 관리되지 않음
- DB와 동기화 X (flush 시 영향 없음)
🧠 그래서 실무에서는 보통 조회 후 트랜잭션 밖에서 다른 계층에 전달할 때 (예: 컨트롤러 → 뷰로 DTO처럼 사용) 준영속 상태로 쓰기도 합니다.
'Spring, SpringBoot, JPA' 카테고리의 다른 글
3. 엔티티 매핑 (1) | 2025.08.16 |
---|---|
1 JPA 소개 및 시작 (4) | 2025.08.12 |
Spring Security와 사용자 역할 관리: 오늘의 학습 내용 정리 (0) | 2024.08.27 |
JWT를 이용한 Spring Security 인증 구현하기 (0) | 2024.08.27 |
SpringBoot Project 게시판 만들기 2 (1) | 2024.05.24 |