Persistence Context
개념
- JPA가 데이터를 DB로부터 불러오거나 저장할 때 사용하는 1차 캐시. 이 안에 들어간 엔티티들은 영속 상태(Persistent State) 라고 불리고, JPA가 자동으로 변경 사항을 추적합니다.
- 트랜잭션이 시작될때 영속성 컨텍스트를 생성하였다가 트랜잭션이 끝나면 추적한 변경사항을 일괄 DB에 반영합니다.
- JPA 동작 구조 (Repository → EntityManager → Persistence Context)
- JPA는 DB가 아니라 Persistence Context를 다루는 기술이다.
- EntityManager는 이 Persistence Context를 조작하는 표준 API 계층이며, Repository는 EntityManager를 추상화해서 개발자가 쉽게 사용할 수 있도록 감싼 상위 레벨 추상화 계층이다.
Persistence Context의 핵심 기능
기능 | 설명 |
1차 캐시 (First-Level Cache) | 같은 엔티티를 여러 번 조회해도 DB 쿼리를 다시 날리지 않음 |
엔티티 동일성 보장 | 같은 영속성 컨텍스트 안에서는 동일한 ID의 엔티티는 항상 같은 객체 (== 비교 true) |
변경 감지 (Dirty Checking) | 트랜잭션 커밋 시, 엔티티의 변경 사항을 감지해 자동으로 update 쿼리를 생성 |
쓰기 지연 (Write-Behind) | persist/save한 insert 쿼리를 모아뒀다가 트랜잭션 종료 시 한꺼번에 실행 |
지연 로딩 (Lazy Loading) | 프록시 객체로 관계 엔티티를 미리 가져오지 않고, 실제 사용할 때 조회 |
영속성 컨텍스트 등록하여 데이터 캐쉬
- 엔티티 신규 생성 후 저장 (persist())이나 엔티티 호출 (find())이 실행되면 엔티티는 영속성 컨텍스트에 캐시된다
- JPA는 DB에 엔티티를 찾아보기전에 영속성 컨텍스트에 엔티티가 존재하는지 확인한다. 존재한다면 DB 쿼리를 실행하지않는다
스냅샷(snapshot) 관리를 통한 DirtyChecking
- 엔티티가 영속성 컨텍스트에 들어와 영속 상태가 되는 순간 엔티티의 초기 상태의 값을 복사해 저장해둠
- flush() 시점에 스냅샷 값과 엔티티 값을 비교하여 변경사항을 일괄 update 실행함
class PersistenceContext {
Map<EntityKey, Object> entitiesByKey; // 1차 캐시: ID로 엔티티 객체 추적
Map<Object, EntityEntry> entityEntries; // 스냅샷 메타 정보
}
액션플래그 생성을 통한 쓰기지연
- entityManager.persist(), entityManager.remove() , entityManager.merge()같은 메서드를 호출했을 때, JPA는 즉시 SQL을 실행하지 않고, 내부에 "INSERT/UPDATE/DELETE 수행 예정"이라는 플래그(액션)를 따로 저장함
- 쓰기 지연 SQL 목록
List<InsertAction> insertQueue
List<UpdateAction> updateQueue
List<DeleteAction> deleteQueue
- flush는 변경 감지된 내용과 쓰기 지연된 SQL을 모아서 DB에 실제 일괄 반영하는 것
- 1. 스냅샷 비교하여 변경감지 (Dirty Checking) → 변경사항이 발견되면 SQL 업데이트 액션플래그를 생성
- 2. INSERT 쓰기 큐 내 액션플래그 처리 → INSERT INTO user (name) VALUES ('변상화')
- 3. Update 큐 내 액션플래그 처리 → UPDATE user SET name = '변상화'
- 4. DELETE 큐 내 액션플래그 처리 → Delete
어플리케이션 레벨에서 예외발생시 rollback 처리하여 자원 절약
- DB에서 예외처리할 시에 발생하는 Undo log 쓰기, 락 획득, MVCC 처리 등 부하 발생을 없앰
지연쓰기로 인해 여러번 분할하여 실행할 쿼리를 한번에 전송함
- JDBC 호출, DB parsing, 네트워크 왕복 비용이 줄어들음
Persistence Context의 자료구조
- 자료구조
- Persistence Context는 Map<EntityKey(EntityType, PrimaryKeyValue), EntityInstance> 형태의 구조
Map<EntityKey, EntityEntry>
- 여기서 EntityKey는 Entity의 Class Type과 @Id의 조합 값으로 구성됩니다
class EntityKey {
private final Class<?> entityClass; // 엔티티 타입 (ex: User.class)
private final Object primaryKey; // @Id 값 (ex: 1L)
}
- EntityEntry는 현재 Managed Entity 객체와, 최초의 스냅샷을 둘 다 가지고 있는 객체.
class EntityEntry {
Object managedEntity; // 현재 메모리에서 관리 중인 객체
Map<String, Object> originalValues; // 최초 영속화 시 복사된 필드값 스냅샷
}
Persistence Context 안의 동작
repository.save(entity) 호출
- save()는 id가 null이면 persist, id가 있으면 merge를 결정한다.
- id 값을 개발자가 수동으로 세팅했든 말든, save()는 "id 유무"만 보고 persist/merge를 선택한다.
- 때문에 혼동을 방지하기 위해서 일반적으로 id는 JPA가 @GeneratedValue로 자동 생성하게 두고, 개발자가 직접 id를 builder로 세팅하는 일은 최소화하는 걸 권장. id를 수동으로 건드리면, persist/merge 판단이나 flush, cascade 동작에서 복잡한 예외 상황이 생길 수 있기 때문이야.
save(entity)
↓
if (entity.getId() == null)
→ persist(entity)
else
→ merge(entity)
- entityManager.persist(entity) : ID가 부여되지 않은 새로운 엔티티를 영속성 컨텍스트에 등록
- id == null (ID가 아직 없는 경우), @GeneratedValue(strategy = IDENTITY) 같은 경우 Insert 예정 Entity'로 따로 저장
- id != null, @GeneratedValue(strategy = SEQUENCE) 같은 경우 nextval 해서 ID를 뽑은 경우 바로 EntityKey 만들어서 Persistence Context에 put
// 1. persist() 호출
public <T> void persist(T entity) {
// 1-1. ID 값을 가져온다
Object id = getId(entity);
if (id == null) {
// 1-2. ID가 아직 없는 경우 (IDENTITY 전략처럼)
// - 아직 EntityKey를 만들 수 없다
// - 임시로 Persistence Context에 "insert 예정 상태"로 등록
persistenceContext.addInsertPendingEntity(entity);
// (※ 별도 관리 컬렉션에 "ID 미확정 Entity"로 추가)
} else {
// 1-3. ID가 이미 있는 경우 (SEQUENCE처럼 nextval로 미리 뽑은 경우)
EntityKey key = new EntityKey(entity.getClass(), id);
persistenceContext.put(key, entity);
}
}
- entityManager.merge(entity) :
- Detached 상태(Persistence Context에 없는 엔티티)를 Managed 상태(Persistence Context가 관리하는 상태)로 변환하는 과정이야
public <T> T merge(T detachedEntity) {
// 1. Detached entity의 id를 가져온다
Object id = getId(detachedEntity);
// 2. 1차 캐시(Persistence Context)에서 id로 찾는다
T managedEntity = persistenceContext.get(detachedEntity.getClass(), id);
if (managedEntity == null) {
// 3. 1차 캐시에 없으면 DB select 쿼리로 찾는다
managedEntity = dbSelect(detachedEntity.getClass(), id);
if (managedEntity != null) {
// 4. DB에 row가 있으면 Persistence Context에 등록
persistenceContext.put(new EntityKey(detachedEntity.getClass(), id), managedEntity);
}
}
if (managedEntity == null) {
// 5. DB에도 없으면 detachedEntity를 복제(clone)해서 새 Managed Entity를 만든다
managedEntity = cloneEntity(detachedEntity); // 복제본 생성
// 복제본에는 id를 그대로 복사하거나 Hibernate가 임시 ID 부여 가능
persistenceContext.put(new EntityKey(detachedEntity.getClass(), id), managedEntity);
}
// 6. Detached Entity의 변경된 값들을 복제본(managedEntity)에 복사
copyProperties(detachedEntity, managedEntity);
// 7. flush 타이밍에 update 또는 insert SQL 발생
return managedEntity;
}
- repository.findById(id) 호출
- entityManager.find(entityClass, id) : 1차 캐시(Persistence Context)에서 먼저 조회 하고없으면 DB select 쿼리 날린 후 조회 후 1차 캐시에 저장함
Object entity = persistenceContext.get(entityClass, id);
if (entity == null) {
entity = executeSelectSQL(entityClass, id); // DB select 쿼리
persistenceContext.put(entityClass, id, entity); // 1차 캐시에 등록
}
return entity;
EntityManager 공통 메서드 정리
메서드 | 설명 |
persist(entity) | 새로운 엔티티를 메모리에 등록하고 "Insert 대상"으로 관리하기 시작하는 것 (→ 나중에 insert SQL 발생) |
find(entityClass, id) | ID로 엔티티를 조회 (1차 캐시(Persistence Context) 먼저 조회) |
merge(entity) | detached 상태(영속성 컨텍스트에서 분리된) 엔티티를 다시 연결해서 관리 대상으로 만듦 |
remove(entity) | 엔티티를 삭제 대상으로 표시 (flush/commit 시 실제 삭제) |
detach(entity) | 특정 엔티티를 영속성 컨텍스트에서 분리 (더 이상 상태 추적 안 함) |
flush() | 영속성 컨텍스트의 변경사항을 DB에 강제로 반영 (batch 작업 전에 강제 동기화할 때 유용) |
clear() | 영속성 컨텍스트에 등록된 모든 엔티티를 제거 (1차 캐시 비우기) |
Persistence Context(영속성 컨텍스트)는 트랜잭션에 묶여있다. 그리고 아래 경우에 종료된다:
- 트랜잭션이 커밋될 때 @Transactional이 붙은 서비스 메서드가 끝나서 트랜잭션이 커밋되면, 영속성 컨텍스트도 flush(변경사항 동기화) 후 종료된다.
- entityManager.clear() 또는 entityManager.close()를 호출할 때
- Spring Web 애플리케이션에서 요청이 끝날 때
JPA 구조로 인한 Best Practice
- JPA에서 양방향 연관관계를 선언했으면, 객체 양쪽의 상태를 일치시켜줘야한다
public void addUserAuth(UserAuth userAuth) {
this.userAuthList.add(userAuth);
userAuth.setUser(this);
}
@Test
void test_권한부여_로직() {
User user = new User();
UserAuth userAuth = new UserAuth(user, auth);
// user.getUserAuthList().add(userAuth); 누락!
// setUser만 호출했다고 가정
// 대상 서비스 로직
myService.assignUserAuth(user);
assertEquals(1, user.getUserAuthList().size()); // 실패!
}
Common Mistakes & How to Avoid Them
MistakeSolution
Loading too many entities into memory | Use pagination (Pageable in Spring Data JPA). |
Updating detached entities without merging | Use merge() before updating. |
Using find() repeatedly for the same entity | Store the entity in a variable; JPA caches it. |
Fetching unnecessary data | Use @Query or JOIN FETCH to optimize queries. |
JPA를 잘못사용하는 케이스
- 쓰기 지연(Write-Behind) 상태일 때, flush 전에 객체 값을 바꾸면 바뀐 값이 저장이 됨
- JPA는 flush()가 실행될때까지 쓰기지연상태이며 dirtychecking 후 쓰기가 생성되므로 변경된 값이 저장됨
User user = userRepository.save(new User("홍길동", 30)); // INSERT SQL 안 나감 — 쓰기 지연 큐에 등록
user.setAge(40); // save 이후 값 변경 (변경 감지 대상)
em.flush(); // 이 시점에 INSERT 쿼리 날림 → age = 40이 반영됨
- record를 변경하고자 PK값을 바꾸면 영속성 컨텍스트 구조상 새로운 값으로 삽입됨
- 영속성 컨텍스트에 엔티티는 PK와 Object 맵으로 저장하고 있기때문에 PK를 변경하면 새로운 key-value가 저장되어서 insert로 처리될 수 있음
@Transactional
public void updateUserInfo(UserDto.UserInfoUpdateReqDto userReqDto) {
User user = userRepository.findWithUserAuthsById()
user.updateUserInfo();
// UserAuth 업데이트
UserAuth userAuth = user.getUserAuthList();
userAuth.changeKey(user.getId(), userReqDto.getAuthId());
// user, userauth 업데이트
User updatedUser = userRepository.save(user);
UserAuth updatedUserAuth = userAuthRepository.save(userAuth);
return;
}
User user = userRepository.findById(1L).get();
user.setId(999L); // ❌ id를 직접 수정!
userRepository.save(user); // save하면 merge 호출
- Hibernate는 id=999L로 "다른" 엔티티라고 착각
- DB에 새 insert 시도
- PK 충돌 또는 중복 데이터 생성
User user = new User();
user.setId(1L); // 수동으로 id 세팅 (❌)
user.setName("홍길동");
userRepository.save(user);
- Hibernate는 id=1L이 있으니까 "update 흐름"을 탄다.
- 근데 실제 DB에 id=1L row가 없으면 update 0건 (silent fail)
- 기대는 insert였는데, 아무것도 저장되지 않는다.
'개발기술 > ORM' 카테고리의 다른 글
JPA 데이터 조회쿼리 정의 ; JPA Method, JPQL, QueryDSL, 네이티브SQL (0) | 2025.03.05 |
---|---|
JPA 기타기능 (Pageable, auditing, data.sql 기능) (0) | 2025.02.28 |
JPA 활용의 필요성과 활용법 (0) | 2025.02.27 |
Spring JPA 트랜잭션 (0) | 2024.12.22 |
Spring JPA Entity 설정 (2) | 2024.10.28 |