1. 영속성 컨텍스트(Persistence Context)란?
JPA를 공부하면서 가장 먼저 마주치는 개념이 바로 영속성 컨텍스트(Persistence Context)입니다.
엔티티를 find()로 조회하거나, save()할때 JPA가 내부에서 어떤 식으로 엔티티를 관리하는지 이해하려면, 영속성 컨텍스트에 대해 알아야 합니다.
먼저 영속성은 데이터나 객체가 프로그램 종료 후에도 사라지지 않고 지속되는 특성을 말합니다.
즉, 데이터를 영구적으로 저장하는 능력을 말합니다.
영속성(Persistence): 데이터나 객체가 프로그램 종료 후에도 사라지지 않고 지속되는 특성
JPA의 영속성 컨텍스트는 Entity를 영구 저장하는 환경이라는 뜻으로, Entity를 관리하기 위해 사용하는 메모리상의 1차 캐시이자, Entity 생명 주기를 추적하는 공간입니다.
쉽게 말해서 JPA가 Entity를 관리하는 메모리 상의 저장소라고 볼 수 있습니다.
영속성 컨텍스트(Persistence Context): Entity를 관리하기 위해 사용하는 메모리 상의 저장소
생명주기: 비영속, 영속, 준영속, 삭제
이 저장소 안에 들어온 엔티티는 JPA가 아래와 같은 기능을 자동으로 제공할 수 있는 '관리되는 엔티티(영속 상태)'가 됩니다.
- 1차 캐시: 같은 엔티티를 계속 조회해도 DB를 다시 가지 않게 함
- 변경 감지(Dirty checking): 엔티티 값만 바꾸면 트랜잭션 커밋 시 자동으로 UPDATE
- 쓰기 지연(Write-Behind): 여러 SQL을 모아서 한번에 보냄
- 지연 로딩(Lazy Loading): 연관된 엔티티를 즉시 다 가져오는 대신, 필요한 시점에 가져옴
즉, 우리가 편하게 사용하는 JPA의 대부분 기능은 영속성 컨텍스트 안에 엔티티가 존재할 때 동작합니다.
이러한 영속성 컨텍스트는 EntityManager를 통해서 사용하게 됩니다.
EntityManager는 JPA에서 영속성 컨텍스트를 생성, 관리하는 핵심 인터페이스 입니다.
개발자가 영속성 컨텍스트에 직접 접근하지 않고, EntityManager를 통해 간접적으로 조작할 수 있도록 합니다.
EntityManager: JPA에서 영속성 컨텍스트를 생성, 관리하는 핵심 인터페이스
Spring 환경에서는 보통 @Transactional이 시작될 때, 트랜잭션 범위의 EntityManager를 생성하고, 그 내부에서 영속성 컨텍스트가 함께 만들어집니다. 트랜잭션이 끝나면 이 컨텍스트도 사라지고, 그 안에 있던 엔티티들도 더이상 관리되지 않는 준영속(detached) 상태가 됩니다.
2. 영속성 컨텍스트의 핵심 기능
1) 1차 캐시(First-Level Cache)와 동일성 보장(Identity Guarantee)
Member member1 = em.find(Member.class, 1L); // 첫 조회 -> DB조회 + 1차 캐시 저장
Member member2 = em.find(Member.class, 1L); // 두번째 조회 -> DB 조회X, 캐시에서 바로 반환
member1 == member2 // true
영속성 컨텍스트에는 ID를 key로 엔티티를 저장하는 캐시가 존재합니다.
그래서 같은 엔티티를 동일 트랜잭션에서 여러번 조회하더라도 DB를 다시 조회하지 않습니다.
이를 통해, 불필요한 DB 접근을 줄여서 성능이 향상됩니다.
또한 캐시에서 반환하는 객체는 같은 객체이기 때문에 == 비교가 가능하고, 이는 변경 감지 기능의 기반이 됩니다.
2) 변경 감지(Dirty Checking)
Member member = em.find(Member.class, 1L);
member.setName("새 이름"); // setter만 호출해도 OK (save 호출 불필요)
// 커밋 시점: UPDATE member SET name='새 이름' ...
JPA는 엔티티를 처음 영속 상태로 만들 때, 해당 엔티티의 초기 값(스냅샷)을 따로 저장해둡니다.
그리고 트랜잭션 커밋 시점에 해당 엔티티 값과 스냅샷을 비교해 변경된 부분이 있다면 자동으로 UPDATE SQL을 생성합니다.
스냅샷: 영속성 컨텍스트가 엔티티를 처음 관리할 때, 엔티티의 초기 상태를 복사해둔 것
3) 쓰기 지연(Wirte-Behind)과 flush를 통한 동기화
persist(), remove()등을 호출 했다고 해서 그 즉시 SQL이 실행되지 않습니다.
영속성 컨텍스트 내부에는 쓰기 지연 SQL 저장소가 있어서, INSERT/UPDATE/DELETE 쿼리를 미리 모아두었다가 flush시 한꺼번에 실행합니다.
flush()는 영속성 컨텍스트에서 쌓인 변경 사항을 DB에 반영하는 과정입니다.
flush가 호출되면 다음과 같은 과정이 일어납니다.
- 쓰기 지연 SQL 저장소의 INSERT/UPDATE/DELETE 실행
- 스냅샷과 비교 후 변경 감지(Dirty Checking) 결과 반영
- DB와 영속성 컨텍스트의 상태를 맞춤
여기서 중요한 점은 flush는 영속성 컨텍스트를 비우지 않는다는 점 입니다. 캐시는 그대로 유지되고 엔티티는 여전히 영속상태입니다.
자동 flush 시점은 다음과 같습니다.
- 트랜잭션 커밋 직전
- JPQL 실행 직전
- 명시적 em.flush() 호출
이러한 기능을 통해 개발자는 엔티티 중심으로 작업하고 SQL 실행 시점은 신경 쓸 필요가 없어지게 됩니다.
4) 지연 로딩(Lazy Loading)
Member member = em.find(Member.class, 1L);
Team team = member.getTeam(); // 여기서는 아직 조회 안됨
team.getName(); // 이 시점에 쿼리 실행
연관된 엔티티를 처음부터 전부 가져오지 않고, 실제로 필요할 때 쿼리를 날려 가져오는 기능입니다.
지연 로딩은 프록시 객체를 통해 구현되며, 프록시를 실제 객체로 초기화하기 위해서는 영속성 컨텍스트가 필요합니다.
3. 엔티티의 생명주기(Entity Lifecycle)
엔티티는 영속성 컨텍스트 안에서 특정한 상태를 가지며 움직입니다.
이 상태를 엔티티 생명주기(Entity Lifecycle)라고 부르며, 다음 4가지 상태를 오가게 됩니다.
- 비영속(Transient)
- 영속(Persistent)
- 준영속(Detached)
- 삭제(Removed)
각 상태에 대해서 알아보겠습니다.
1) 비영속 (Transient)
Member member = new Member(); // 비영속 상태
member.setName("건영");
JPA가 아직 모르는, 관리되지 않는 상태입니다.
비영속 상태 엔티티의 특징은 다음과 같습니다.
- 영속성 컨텍스트에 들어가지 않은 순수 Java 객체
- DB와 어떤 연관도 없음
- Dirty Checking, 1차 캐시, 쓰기 지연 모두 작동하지 않음
즉, JPA의 관리대상이 아닙니다.
2) 영속 (Persistent)
// persist()로 신규 엔티티 등록
Member member = new Member("건영"); // 비영속
em.persist(member); // 영속
// find()또는 jpql로 조회
@Transactional
public void test() {
Member member = em.find(Member.class, 1L); // persist 안 했어도 영속 상태
}
EntityManager에 의해 관리되는 상태입니다.
영속성 컨텍스트의 1차 캐시에 올라온 순간부터 엔티티는 영속 상태가 됩니다.
영속 상태에서는 위에서 설명한 영속성 컨텍스트의 핵심기능이 모두 활성화 됩니다.
영속 상태로 진입하는 방법은 두가지로 persist()로 신규 엔티티를 등록하거나, 트랜잭션 상태에서 find()로 조회하는 경우 입니다.
트랜잭션이 아닌 상태에서 find()로 조회하면, 이론적으로는 영속 상태이지만, Dirty Checking, flush 자동 호출, Lazy Loading 등을 이용할 수 없습니다. 즉, 트랜잭션 바깥에서 조회한 엔티티는 거의 준영속처럼 행동합니다.
3) 준영속 (Detached)
// 트랜잭션 종료
@Transactional
public void test() {
Member member = em.find(Member.class, 1L); // 영속
}
// 트랜잭션 종료 → 준영속 상태
// 명시적으로 컨텍스트에서 제거
Member member = em.find(Member.class, 1L);
em.detach(member); // 준영속 상태
// clear()로 전체 제거
em.clear(); // 전체 엔티티가 준영속화
한때 영속 상태였지만, 더 이상 영속성 컨텍스트가 관리하지 않는 상태입니다.
준영속 상태가 되는 경우는 3가지 입니다.
- 트랜잭션 종료 후
- 명시적으로 컨텍스트에서 제거
- clear()로 전체 제거
준영속 상태의 특징은 다음과 같습니다.
- 1차 캐시에 존재하지 않음
- Dirty Checking 작동안함
- Lazy Loading 작동안함
- flush 반영 대상 X
즉, JPA의 도움을 받지 못하는 상태입니다.
4) 삭제 (Removed)
Member member = em.find(Member.class, 1L);
em.remove(member); // 삭제 상태
EntityManager가 엔티티를 삭제 대상으로 표시한 상태 입니다.
삭제 상태의 엔티티도 영속성 컨텍스트에 남아 있으나 flush() 시 DELETE SQL이 발행됩니다.
만약 다시 persist()를 호출하면 delete 쿼리는 취소됩니다.
4. 엔티티 상태 관리 (persist / merge / remove / flush / save)
마지막으로 엔티티의 상태를 변경하거나 DB에 반영할 때 사용되는 핵심 동작들을 정리해보겠습니다.
1) persist()
Member member = new Member("건영"); // 비영속
em.persist(member); // 영속
persist()는 비영속 엔티티를 영속성 컨텍스트(1차 캐시)에 등록할 때 사용됩니다.
내부 동작은 다음과 같습니다.
- 엔티티를 1차 캐시에 저장
- 엔티티에 대한 INSERT SQL을 쓰기 지연
- flush 시점에 일괄 반영
2) merge()
Member detachedMember = new Member();
detachedMember.setId(1L); // 준영속 상태 엔티티
Member persistentMember = em.merge(detachedMember);
merge()는 준영속 상태의 엔티티를 다시 영속 상태로 복구하는 기능입니다.
내부 동작은 다음과 같습니다.
- 동일 ID의 엔티티를 DB 또는 영속성 컨텍스트에서 조회
- 있으면 그 객체가 merge 대상
- 없으면 새로운 인스턴스를 만들어 merge 대상으로 함
- 조회된 엔티티(새로운 영속 엔티티)에 준영속 엔티티 값 복사
- 준영속 엔티티는 계속 준영속 상태, 새로운 엔티티만 영속 상태
중요한 점은 merge는 원본 엔티티를 영속화 하지 않고, 새로운 영속 엔티티를 만들어 그 객체만 관리한다는 것 입니다.
3) remove()
Member member = em.find(Member.class, 1L);
em.remove(member); // 삭제 상태
엔티티를 영속성 컨텍스트에서 삭제 예정 상태로 표시하는 작업입니다.
즉시 실행되지 않고 flush 시점에 SQL이 실행됩니다.
앞서 설명햇듯 remove() -> persist()를 다시 호출하면 삭제가 취소됩니다.
4) flush()
영속성 컨텍스트의 변경 사항을 DB와 동기화 하는 과정입니다.
내부동작은 다음과 같습니다.
- 쓰기 지연 저장소에 쌓인 INSERT/UPDATE/DELETE 실행
- Dirty Checking 결과 UPDATE SQL 생성 및 실행
- DB 상태와 영속성 컨텍스트 상태를 맞춤
flush는 영속성 컨텍스트를 절대 비우지 않습니다. 이에 1차 캐시는 유지되고, 엔티티는 여전히 영속 상태입니다.
5) save()
// 비영속 상태
Member member = new Member("건영"); // 비영속 상태
memberRepository.save(member); // 내부적으로 persist()
// 준영속 상태
Member detached = new Member("건영");
detached.setId(1L); // 이미 존재하는 PK → 준영속 객체
memberRepository.save(detached); // 내부적으로 merge() 실행
// 영속 상태
@Transactional
public void updateMember() {
Member member = memberRepository.findById(1L).orElseThrow(); // 영속 상태
member.setName("새로운 이름");
memberRepository.save(member); // 내부적으로 아무 동작도 없음
}
Spring Data JPA의 save()는 단순한 등록 메서드가 아니라,
엔티티의 상태에 따라 persist 혹은 merge를 선택해서 실행하는 메서드 입니다.
비영속 상태일때는 persist(), 준영속 상태일때는 merge()를 실행하며 영속 상태일때는 아무 동작도 없습니다.
save()는 EntityManager의 동작을 추상화한 Repository 패턴에서 사용됩니다.
'Backend > JPA' 카테고리의 다른 글
| [JPA] @OrderBy로 정렬하기, OneToMany 특정 칼럼 기준 정렬 (0) | 2024.12.11 |
|---|