영속성 컨텍스트 구조 && 장점

- JPA가 제공하는 기능은 크게 엔티티와 테이블을 매핑하는 설계 부분과 매핑한 엔티티를 실제 사용하는 부분으로 나눌 수 있다.
- 엔티티를 엔티티 매니저를 통해 어떻게 사용하는지 한 번 알아보자
엔티티 매니저 → 엔티티를 저장, 수정, 삭제, 조회 등 엔티티와 관련된 모든 일을 처리
개발자 입장에서 엔티티 매니저는 엔티티를 저장하는 가상의 DB라고 생각해라
- 밑 코드를 통해 다시 분석
https://github.com/whitecy01/codyssey/tree/main/springAndJPA/pureJPA/pureJPA
codyssey/springAndJPA/pureJPA/pureJPA at main · whitecy01/codyssey
나만의 백엔드 세계를 탐험하는 여정 기초부터 실전까지 한 걸음씩 쌓아가는 개발자의 기록. Contribute to whitecy01/codyssey development by creating an account on GitHub.
github.com
엔티티 매니저 팩토리와 엔티티 매니저
DB를 하나만 사용하는 애플리케이션 → 하나의 EntityManagerFactory를 하나만 생성
//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pure-jpa");
- 한 개만 만들어서 애플리케이션에서 전체 공유하도록 설계됨
엔티티 매니저는 다음과 같다.
//엔티티 매니저 - 생성
EntityManager em = emf.createEntityManager();
→ 엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하여 서로 다른 스레드 간에 공유됨
→ 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하여 스레드 간에 절대 공유하면 안된다.

- 엔티티 매니저는 DB 연결이 꼭 필요한 시점까지 커넥션을 얻지 않음 → 트랜잭션 시작할 때 커넥션 획득
- 하이버네이트를 포함한 JPA 구현체들은 EntityManagerFactory를 생성할 때 커넥션 풀도 만드는데(persistence.xml에 보면 DB 접속 정보가 있음) 이건 J2SE 환경에서 사용하는 방법
커넥션풀에 대해 좀 더 알아보자
커넥션 풀
→ DB 연결(Conntection)을 미리 여러 개 만들어두고 재사용하는 창고
이게 왜 필요한가?
- DB 커넥션 하나 만드는 과정은 비싸고 느림.
EntityManagerFactory → 설정 및 커넥션 풀 관리
- JPA에서 커넥션풀은 JPA 구현체 + 풀 라이브러리 조합으로 관리
- Hibernate + HikariCP (요즘 표준)
- EclipseLink + 내부 풀
- 옛날: C3P0, DBCP (요즘 잘 안 씀)
커넥션을 하나 만들 때 실제로 일어나는일
- TCP 연결
- 인증(ID/PW)
- 세션 생성
- DB 리소스 할당
→ 이걸 요청마다 매번하면 성능 폭망, DB 과부하, 서버는 놀고 DB는 죽음..
커넥션 풀의 동작 방식
1. 애플리케이션 시작
EntityManagerFactory 생성 → 커넥션 풀 생성 → DB 커넥션 여러 개 미리 확보
2. 트랜잭션 시작
em.getTransaction().begin();
- 이 시점에 풀에서 커넥션 하나 빌림
3. SQL 실행
- 빌린 커넥션으로 쿼리 수행
4. 트랜잭션 종료
em.getTransaction().commit();
- 커넥션을 닫는 게 아니라 풀에 반납
엔티티 매니저 팩토리 && 엔티티 매니저 && 커넥션 풀을 Spring boot에서 보자

- postgreSQL로 진행해봄
package com.persistenceContext.persistenceContextexam;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import javax.sql.DataSource;
@Component
@RequiredArgsConstructor
public class JpaInspector implements CommandLineRunner {
private final EntityManagerFactory emf;
private final EntityManager em;
private final DataSource dataSource;
@Override
public void run(String... args) {
System.out.println("===== JPA Infra =====");
System.out.println("EMF = " + emf.getClass());
System.out.println("EM = " + em.getClass());
System.out.println("DS = " + dataSource.getClass());
}
}
서버 실행하면 콘솔에서 꼭 봐야 할 것
- EntityManagerFactory → 스프링이 만든 구현체
- EntityManager → 프록시 (SharedEntityManager)
- DataSource → HikariDataSource
→ “Spring에서 주입되는 EM은 진짜가 아니다”가 눈으로 보임
import jakarta.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
protected Member() {}
public Member(String name) { this.name = name; }
}
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class TestService {
private final EntityManager em;
@Transactional
public void tx() {
em.persist(new Member("재윤"));
System.out.println("persist 완료");
}
}
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class TestController {
private final TestService testService;
@GetMapping("/test")
public String test() {
testService.tx();
return "ok";
}
}
실행 로그 - /test 전
1. 커넥션 풀(Hikari) 생성 + 커넥션 미리 확보
com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
HikariPool-1 - Pool stats (total=1/10, idle=1/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (bbbddec1-de48-47ca-bb6f-e9867a0571cf)
HikariPool-1 - Established new connection (bbbddec1-de48-47ca-bb6f-e9867a0571cf)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@197d521a
HikariPool-1 - After adding stats (total=2/10, idle=2/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (a723ee15-3011-4678-ad23-1a630289d4e5)
HikariPool-1 - Established new connection (a723ee15-3011-4678-ad23-1a630289d4e5)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@72e9f3b3
HikariPool-1 - After adding stats (total=3/10, idle=3/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (eacaa76c-0527-4f11-9a51-7776116b2448)
HikariPool-1 - Established new connection (eacaa76c-0527-4f11-9a51-7776116b2448)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@f216598e
HikariPool-1 - After adding stats (total=4/10, idle=4/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (b7e2f4df-33b4-496c-bd19-b4caa4f25c1e)
HikariPool-1 - Established new connection (b7e2f4df-33b4-496c-bd19-b4caa4f25c1e)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@d7abe26a
HikariPool-1 - After adding stats (total=5/10, idle=5/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (479d0621-1cca-4179-948a-44300548ad82)
HikariPool-1 - Established new connection (479d0621-1cca-4179-948a-44300548ad82)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@a34fb132
HikariPool-1 - After adding stats (total=6/10, idle=6/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (aa2c5a7b-4349-47ba-82db-098b9de454e3)
HikariPool-1 - Established new connection (aa2c5a7b-4349-47ba-82db-098b9de454e3)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@412daca2
HikariPool-1 - After adding stats (total=7/10, idle=7/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (1497aea9-6884-4b83-aa50-fef3949f6847)
HikariPool-1 - Established new connection (1497aea9-6884-4b83-aa50-fef3949f6847)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@87bf4c81
HikariPool-1 - After adding stats (total=8/10, idle=8/10, active=0, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (21d8fed5-8b72-47d1-9ee4-5bd678d8cb41)
HikariPool-1 - Established new connection (21d8fed5-8b72-47d1-9ee4-5bd678d8cb41)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@1a6d9311
HHH000489: No JTA platform available
HikariPool-1 - After adding stats (total=9/10, idle=8/10, active=1, waiting=0)
HikariPool-1 - Attempting to create/setup new connection (831d227c-0297-4fcb-a538-4158ff0ab53d)
HikariPool-1 - Established new connection (831d227c-0297-4fcb-a538-4158ff0ab53d)
HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@4fd97cee
- properties에서 커넥션 풀을 만드는 개수를 지정하지 않으면 자동으로 maximumPoolSize=10, minimumIdle=10가 지정되어 만들어져서 위처럼 나옴
2. JpaInspector
EMF = class jdk.proxy2.$Proxy118
EM = class jdk.proxy2.$Proxy119
DS = class com.zaxxer.hikari.HikariDataSource
- DS가 HikariDataSource → 커넥션 풀
- EMF/EM이 jdk.proxy2.$Proxy..로 나온 이유는
- 스프링이 트랜잭션/영속성 컨텍스트를 붙이기 위해 프록시로 감싸서 주입했기 때문
- 즉 내가 주입 받은 EM은 진짜 “EntityManager 인스턴스”가 아니라 요청/트랜잭션 상황에 맞춰 진짜 EM을 찾아주는 대리인(프록시)
→ 스프링에서 주입되는 EntityManager는 프록시다.
실행 로그 - /test 후
1. OpenEntityManagerInViewIntercetpr(OSIV) 때문에 EM을 먼저 열어둠
Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
의미
- 웹요청 시작~응답 끝까지 EntityManager를 열어둠
- 뷰 렌더링/컨트롤러 이후에도 지연로딩 가능
- 대신 “트랜잭션 바깥에도 쿼리가 나갈 수 있음”
2. 트랜잭션 매니저가 “스레드에 바잉딘 된 EM”을 발견
Found thread-bound EntityManager [SessionImpl(...<open>)] for JPA transaction
Creating new transaction with name [TestService.tx]
- OSIV가 이미 EM을 열어두고 스레드에 묶어둔 상태라서 트랜잭션 매니저가 “어?, 이 스레드에 열린 EM이 있네” 하고 재사용
그 뒤 로그가
Not closing pre-bound JPA EntityManager after transaction
- 트랜잭션 끝났다고 EM 닫지 않는다는 뜻 → 요청이 끝날 때 (OSIV가) 닫아야 하니
3. 실제로 트랜잭션이 시작됨(@Transactional)
Getting transaction for [TestService.tx]
Exposing JPA transaction as JDBC [HibernateConnectionHandle...]
- 여기서 드디어 JPA 트랜잭션이 JDBC 커넥션과 연결됨
- “커넥션을 얻는 시점”은 정확히 이 타이밍 근처
→ 여기서 HNikari가 10개 만들어놓았으니 풀에서 1개 대여
4. persist 시점에 시퀀스 값 먼저 뽑음
select nextval('member_seq')
persist 완료
insert into member ...
- @GeneratedValue + sequence 전략 때문에
- insert 전에 PK를 만들려고 nextval 먼저 호출
- 그 다음 insert 실행
5. 커밋 + 트랜잭션 종료
Initiating transaction commit
Committing JPA transaction ...
Completing transaction ...
- commit 시점에 flush가 일어나면서 insert SQL 확정
- 커넥션은 “닫는 게 아니라 풀에 반납”
6. 요청이 끝나면 OSIV가 EM 닫음
Closing JPA EntityManager in OpenEntityManagerInViewInterceptor
- 트랜잭션은 이미 끝났지만 EM 자체는 요청 끝나면서 닫힘(= 영속성 컨텍스트 종료)
결과
- 실제 해보았을 때 커넥션 풀을 애플리케이션 시작 때 만들어지고, 커넥션을 미리 확보한다라는 것을 알 수 있었음
- 스프링이 주입하는 EntityManager/EntityManagerFactory는 프록시임.
- OSIV가 켜져 있어서 요청 전체 동안 EM이 열려있고 트랜잭션은 그 안에서 잠깐 시작/종료됨.
codyssey/springAndJPA/persistenceContextexam/src/main/java/com/persistenceContext/persistenceContextexam at main · whitecy01/co
나만의 백엔드 세계를 탐험하는 여정 기초부터 실전까지 한 걸음씩 쌓아가는 개발자의 기록. Contribute to whitecy01/codyssey development by creating an account on GitHub.
github.com
영속성 컨텍스트(persistence context)
- 엔티티를 영구 저장하는 환경
- 엔티티 매니저 → 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리함.
em.persist(member);
이 메소드는 엔티티 매니저를 사용해서 회원 엔티티를 영속성 컨텍스트에 저장함.
→ 영속성 컨텍스트는 논리적인 개념에 가깝고 눈에 보이지도 않음. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어짐 And 엔티티 매니저를 통해 여기에 접근 및 관리할 수 있음
엔티티의 생명주기
엔티티는 4가지 상태가 존재
- 비영속(new/transient) → 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속(managed) → 영속성 컨텍스트에 저장된 상태
- 준영속(detached) → 영속성 컨텍스트에 저장되었다가 분리된 상태
- 삭제(removed) → 삭제된 상태

비영속
- 엔티티 객체를 생성함 but 순수한 객체 상태임 아직 저장하지 않음
- 영속성 컨텍스트와 DB와는 전혀 관련이 없음
Member member = new Member();
member.setId(id);
member.setUsername("재윤");

영속
- 엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장 == 영속 상태 == 영속성 컨텍스트에 의해 관리된다는 것.
- em.find나 JPQL을 사용해서 조회한 엔티티도 영속성 컨텍스트가 관리하는 영속 상태
em.persist(member);

삭제
- 엔티티를 영속성 컨텍스트와 DB에서 삭제
em.remove(member);
영속성 컨텍스트의 특징
- 영속성 컨텍스트와 식별자 값 → 영속성 컨텍스트는 엔티티를 식별자 값으로 구분함. 식별자 값 없으면 예외 발생
- 영속성 컨텍스트와 DB 저장 → JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 DB에 반영함 == flush(플러시)
영속성 컨텍스트가 엔티티를 관리하는 것에서 장점을 이제 한 본 보려고 함
엔티티 조회 - 1차 캐시
- 영속성 컨텍스트는 내부에 캐시를 가지고 있음 == 1차 캐시 → 영속 상태의 엔티티 이곳에 저장됨
- 이 내부에 Map이 있음 키는 @Id 값이고 value는 엔티티 인스턴스

- em.find(엔티티 클래스의 타입, 식별자 값)를 호출하였다고 했을 때 1차 캐시에서 엔티티를 찾고 1차 캐시에 없으면 DB에서 조회

- em.persist를 하면 1차 캐시에 저장됨 == 메모리에 있는 1차 캐시
→ 성능상 이점
추가적으로
- 영속성 컨텍스트는 엔티티의 동일성을 보장 → 객체 조회에서 같은 객체를 반환
엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연
//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pure-jpa");
//엔티티 매니저 - 생성
EntityManager em = emf.createEntityManager();
//트랜잭션 - 획득
EntityTransaction tx = em.getTransaction();
tx.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
tx.commit();// 트랜잭션 - 커밋 -> 커밋하는 순간 SQL을 DB에 보냄
- 엔티티 매니저 → 트랜잭션을 커밋하기 직전까지 DB에 엔티티를 저장하지 않고 내부 쿼리 저장소에 SQL을 모아놓음
- 트랜잭션을 커밋할 때 모아둔 쿼리를 DB에 보냄 → 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)


- 트랜잭션 커밋 → 엔티티 매니저는 영속성 컨텍스트를 flush(영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업 이때 등록, 수정, 삭제한 엔티티를 DB에 반영) → 동기화한 후 실제 DB 트랜잭션을 커밋
- 동기화 == 영속성 컨텍스트에 쌓아둔 변경 사항을 SQL로 만들어 DB에 반영
- DB 트랜잭션을 커밋 == JDBC Connection 단위, BEGIN/COMMIT/ROLLBACK, SQL 결과를 실제로 저장할지 말지 결정 즉, commit 전까지는 DB가 확정하지 않음
트랜잭션을 지원하는 쓰기 지연이 가능한 이유
- 데이터를 저장하는 즉시 등록 쿼리 DB에 보낸 후 마지막에 트랜잭션을 커밋
- 데이터를 저장하면 등록 쿼리를 메모리아 모아놓고 트랜잭션 커밋할 때 쿼리를 DB에 보낸 후 커밋
이 2개가 어차피 결과가 같음 → 트랜잭션을 커밋하면 함꼐 저장되고 롤백하면 함께 저장되지 않음 즉, 커밋 직전에만 DB에 SQL을 전달하면 됨
엔티티 수정 - 변경 감지(dirty checking)
- 요구사항 변경으로 인해 막 수정한다고 하자 그러면 JPA 사용 안 하면 비즈니스 로직이 SQL에 의존함. → 자동으로 변경되는 걸 좀 해주면 좋겠음 ㅠㅠ라는 거임
//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("pure-jpa");
//엔티티 매니저 - 생성
EntityManager em = emf.createEntityManager();
//트랜잭션 - 획득
EntityTransaction tx = em.getTransaction();
tx.begin();
Member findMember = em.find(Member.class, "memberA");
//영속 엔티티 데이터 수정
member.setUsername("재윤");
member.setAge(20);
tx.commit();
- JPA로 엔티티 수정하게 된다면 → 엔티티 조회해서 데이터만 변경하면 됨
- 엔티티의 변경사항을 DB에 자동으로 반영하는 기능 변경 감지(dirty checking)라고 함
밑 그림을 보자

- JPA는 엔티티를 영속성 컨텍스트에 보관할 때 최초의 상태를 복사해서 저장해둠 == 스냅샷
- 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티 찾음 → 찾으면 수정 쿼리를 쓰기 지연 SQL 저장소에 보내놓음
→ 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용됨, 비영속, 준영속은 값을 변경해도 DB에 반영되지 않음
JPA 기본 전략
- 재미있는 거는 수정된 것만 SQL을 반영하도록 하는 게 아니라 엔티티의 모든 필드 업데이트 → data 전송량이 증가하는 단점 but 장점을 본다면
- 수정을 했을 때 Hibernate가 어떤 SQL을 쓸지 매번 판단을 안 해도 됨 → 애플리케이션 시작 시점에 미리 수정 쿼리를 만들어 놓아서 캐싱 해놓고 이거 바인딩해서 재사용하면 됨
- DB는 SQL을 바로 실행하지 않음 순서가 SQL 문자열 파싱 → 문법/권한 검사 → 실행 계획 생성 → 실행인데 1~3번이 좀 비쌈 SQL이 매번 다르면 DB는 재사용 안함 SQL이 항상 같으면 DB가 봤던 쿼리라 파싱/계획 재사용을 함
- 뭐 필드가 많거나 저장되는 내용이 크면 수정된 데이터만 사용해서 동적으로 UPDAT SQL 생성 전략 가능 → 하이버네이트 확장 기능 → DynamicUpdate 어노테이션 사용
- 이건 뭐 컬럼이 30개 이상이면 이게 더 빠르다고함
엔티티 삭제
em.remove(memberA)
- 엔티티 등록과 비슷하게 삭제 쿼리를 쓰기 지연 SQL에 등록 후에 → 트랜잭션을 커밋해서 플러시를 호출하면 실제 DB에 삭제 쿼리 전달
- em.remove를 호출하는 순간 영속성 컨텍스트에서 제거됨
지연로딩
- 영속성 컨텍스트에서 위에서 말한 것처럼 식별자를 가지고 있고, 상태 관리(변경 감지), 트랜잭션 범위 내에서 객체 동일성 보장가 있었다.
우선 지연로딩이 뭔지부터 보자
@ManyToOne(fetch = FetchType.LAZY)
private Team team;
- 이 상태에서 Member를 조회하면 Team 엔티티를 즉시 조회하지 않음 즉, DB에는 TeamSQL이 안나감
영속성 컨텍스트가 여기서 핵심인 걸 본다면
- 프록시가 아직 DB를 안 갔다라는 걸 기억함
- 이 프록시는 실제 Team 데이터는 없고 식별자만 알고 있음, 영속성 컨텍스트 참조를 들고 있음
밑 코드가 실행된다고 하자
member.getTeam().getName();
- 이 순간 내부에서는
프록시 -> 영속성 컨텍스트에 질문 "Team(id=1) 이미 관리 중임?:"
영속성 컨텍스트에서 Team이 있으면 객체 반환하고 없으면 DB에 SQL 날려서 Team 조회함 그리고 영속성 컨텍스트에 등록하고 → 프록시 → 실제 엔티티로 변환
즉, 이점은 DB 접근을 끝까지 미룰 수 있다는 거, 이미 있으면 DB를 안 가도 됨
준영속
위에서는 비영속 → 영속 → 삭제를 봄 이제는 영속 → 준영속을 한 번 봐보자
- 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능 사용 불가능.
- 변경 감지 → 값 변경해도 DB에 반영안됨
만드는 방법 → em.detach, em.clear(), em.close()
- em.deatch == 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거됨
- em.clear == 영속성 컨텍스트의 모든 엔티티를 준영속 상태로 만듦
- em.close() == 영속성 컨텍스트를 종료 엔티티 전부 준영속 상태가 됨.
준영속 상태의 특징
- 거의 비영속에 가까움
- 식별자 값을 가지고 있음
- 비영속 상태는 식별자 값이 없을 수 있지만 이건 이미 한 번 영속 상태여서 반드시 식별자 값을 가지고 있음
- 지연로딩 불가능
- 실제 객체 대신 프록시 개체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 불러오는데 이 상태는 영속성 컨텍스트가 관리하지 않아서 지연 로딩 시 문제 발생
병합 : merge()
- 준영속 상태에서 이름을 바꿔보는 걸 해보자.
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
public class App
{
static EntityManagerFactory emf = Persistence.createEntityManagerFactory("pure-jpa");
public static void main( String[] args )
{
Member member = createMember("memberA", "회원1");
member.setUsername("회원명변경"); // 준영속 상태에서 변경
mergeMember(member);
}
static Member createMember(String id, String username){
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
Member member = new Member();
member.setId(id);
member.setUsername("재윤");
member.setAge(20);
em.persist(member);
tx.commit();
em.close();
return member;
}
static void mergeMember(Member member){
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
Member mergeMember = em.merge(member);
tx.commit();
//준영속 상태
System.out.println("member = " + member.getUsername());
//영속 상태
System.out.println("mergeMember = " + mergeMember.getUsername());
System.out.println("em contains member = " + em.contains(member));
System.out.println("em contains mergeMember = " + em.contains(mergeMember));
em.close();
}
}
결과
member = 회원명변경
mergeMember = 회원명변경
em contains member = false
em contains mergeMember = true
순서는 다음과 같다.

- membvver.setUsername(”회원명변경”)을 호출해서 회원 이름을 변경했지만 준영속 상태인 member 엔티티를 관리하는 영속성 컨텍스트가 존재하지 않아서 수정 사항을 DB에 반영 불가
- 준영속 상태의 엔티티 수정하려면 → 준영속 상태를 다시 영속 상태로 변경(merge) → 트랜잭션을 커밋할 때 수정했던 회원명이 DB에 반영됨(member 엔티티가 영속 상태가 된 것은 아님 mergeMember라는 새로운 영속 상태 엔티티 반환)
즉, 중요한 것은
- 준영속 엔티티(member)를 그대로 다시 붙이지 않음
- 영속성 컨텍스트 안에 새로운 영속 엔티티를 만든다
- 준영속 엔티티의 값을 복사한다.
- 그 새 영속 엔티티를 반환
member는 계속 준영속 상태임. 단지 member
member가 객체로는 있지만 DB에 반영이 안 된 것
codyssey/springAndJPA/persistenceContextexam/src/main/java/com/persistenceContext/persistenceContextexam at main · whitecy01/co
나만의 백엔드 세계를 탐험하는 여정 기초부터 실전까지 한 걸음씩 쌓아가는 개발자의 기록. Contribute to whitecy01/codyssey development by creating an account on GitHub.
github.com
'개발 지식 > Spring boot' 카테고리의 다른 글
| [Spring boot] JPA - JPA의 연관관계 (0) | 2026.01.30 |
|---|---|
| [Spring boot] JPA - 기본 키 매핑 전략 (0) | 2026.01.30 |
| [Spring boot] JPA - 순수 JPA 애플리케이션 개발(JPQL) (0) | 2026.01.30 |
| [Spring boot] JPA - 순수 JPA 프로젝트 설정(import 문제, 라이브러리 정리) (0) | 2026.01.30 |
| [Spring boot] JPA - Spring data JPA가 아닌 순수 JPA (0) | 2026.01.30 |
