Search

[JPA의 사실과 오해] 영속성 컨텍스트 (동작 관점의 JPA)

Tags
Study
JPA
Database
Last edited time
2025/04/21 11:56
2 more properties
해당 포스팅은 조영호님의 JPA의 사실과 오해라는 강의 내용을 바탕으로 작성되었습니다.

1. 영속성 컨텍스트의 구성

영속성 컨텍스트는 작업단위 + 식별자 맵으로 구성되어 있음

1.1. 작업 단위(Unit Of Work)

정의
비즈니스 트랜잭션의 영향을 받는 엔티티 목록을 유지 관리
변경 사항 기록 및 동시성 문제 해결 조정
해설
트랜잭션 내 어떤 어떤 일을 해야돼 라는 것을 정의

1.2. 식별자 맵 (Identity Map)

정의
로드된 모든 객체를 맵(Map)에 유지하여 각 객체가 한번만 로드되도록 처리
객체를 참조할 때 맵을 사용하여 객체 조회
해설
DB에서 객체를 읽거나, DB에 객체를 저장하는 과정의 모든 객체를 다 저장
캐시 역할도 함
로드된 모든 객체를 맵에 유지하여 각 객체가 한 번만 로드되도록 처리

2. 엔티티 상태 전이 (엔티티 생명주기)

JPA의 도작 관점을 이해하기 위해서는 엔티티 상태 전이에 대한 이해가 필요함
엔티티 매니저가 JPA 메서드를 호출하면 상태 전이가 발생함

2.1. 영속 상태 (Persistent) & 비영속 상태 (Transient)

영속 상태로 전이하는 경로
1.
객체를 조회하는 경우
2.
비영속 상태의 persist 하는 경우

2.1.1. 객체를 조회하는 경우

엔티티 매니저에서 아래 메서드를 통해 객체를 갖고오면 객체가 영속 상태가 됨
find()
getReference()
Query::getResultList()
Query::getSingleList()
예시
1.
1번 Screening 객체 조회 find()
2.
식별자 맵에 1번 Screening이 존재하는지 확인
3.
식별자 맵에 없으면 데이터베이스 조회
4.
데이터베이스에서 조회한 원본 객체를 식별자 맵에 추가
이때, 원본을 기반으로 스냅샷을 별도로 생성
5.
원본 객체를 반환
해설
영속 상태는 영속성 컨텍스트에서 관리 되고 있는 상태
영속 상태는 JPA 관점에서 식별자 맵에 들어가있다는 것을 의미
여기서 영속 상태란 JPA의 관점이며, DB와는 관계가 없음
영속상태는 JPA가 “내가 관리해야하는 대상이구나” 라는 것을 의미

2.1.2. 비영속 상태의 객체를 persist 하는 경우

1.
어플리케이션 레벨에서 객체를 새로 생성함
Screening newScreeing = new Screening()
JPA의 관점에서 JPA는 해당 객체를 모름. 그래서 일단 Transient (비영속) 상태라고 정의
2.
persist()
entityManager.persist(newScreening)
위 메서드를 통해 영속성 컨텍스트에 persist 하여 식별자 맵에 등록
영속(Persistent) 상태로 변경
식별자 맵에 들어가면 영속(Persistent) 상태라고 볼 수 있음
3.
쓰기 지연 SQL 저장소에 INSERT 쿼리 등록
4.
영속성 컨텍스트 플러시(Flush)
persist() 실행 시점에 쿼리를 실행하지 않음
플러시됨 시점에 영속성 컨텍스트의 수정 사항을 DB에 동기화하여 쿼리 실행
일반적으로 영속성 컨텍스트는 트랜잭션이 커밋될때 자동으로 플러시됨
이때, 영속성 컨텍스트의 변경사항을 보내는 것이며 플러시되어도 영속 상태의 객체들은 여전히 남아 있음
트랜잭션이 종료될 때 영속성 컨텍스트가 초기화 됨

2.2. 제거 상태 (Removed)

영속 상태의 객체를 제거 상태로 변경
1.
영속성 컨텍스트에 영속 상태의 객체 존재
2.
entityManager.remove()
a.
영속성 컨텍스트에서 객체 제거 (식별자맵에서 객체 제거)
b.
쓰기 지연 SQL 저장소에 DELETE 쿼리 등록
3.
entityManager.flush()
a.
데이터베이스 레코드 삭제 쿼리 플러시
b.
DELETE 쿼리 실행
영속 상태를 제거 상태로 변경 예시 코드
@DataJpaTest(showSql = false) public class JpaPersistenceContextTest { @Autowired private EntityManager em; @Test public void remove() { DiscountPolicy policy1 = new DiscountPolicy(1L, DiscountPolicy.PolicyType.AMOUNT_POLICY, Money.wons(1000), null); // 객체 영속화됨 (Transient -> Persitent) em.persist(policy1); // 쿼리 실행: insert into discount_policy (...policy1...) values... em.flush(); // 영속화된 객체를 제거 상태로 변경 em.remove(policy1); // 쿼리 실행: Delete from discount_policy where id = 1 em.flush(); } }
Java
복사

2.3. 준영속 상태 (Detached)

영속성 컨텍스트 내에 등록된 영속 객체를 비우는 것을 의미
영속성 컨텍스트에 넣었다가 빼는 것
더이상 영속성 컨텍스트에 관리되지 않으므로 자동 변경 감지가 동작하지 않음
실제로는 거의 쓰지 않으며, 사이드 이펙트가 발생할 수 있으므로 쓰지 않는 것을 권장함
entityManger.clear
영속성 컨텍스트 내 모든 엔티티를 제거하여 준영속 상태로 변경
entityManger.detach
영속성 컨텍스트 내 개별 엔티티를 제거하여 준영속 상태로 변경
entityManger.merge
영속성 컨텍스트에 재등록 하는 것
이때 내부적으로 SELECT 쿼리를 실행시켜 객체를 최신 상태로 유지시킴
merge 과정에서 객체의 상태에 null 등이 들어간 경우 null로 머지될 수 있으므로 안쓰는 것이 좋음

2.4. 비영속 상태와 준영속 상태의 차이

영속성 컨텍스트에서 엔티리를 제거한다면 영속성 컨텍스트에서 관리되지 않는다는 점만 보면 비영속 상태와 다를 바없는데 별도의 준영속 상태로 두는 이유는 무엇일까?

2.4.1. 기본 개념 비교

비영속 상태(Transient)
엔티티 객체가 생성되었지만 아직 영속성 컨텍스트와 전혀 관계가 없는 상태
즉, 순수한 자바 객체 상태로 가 해당 객체를 인식하지 못함
데이터베이스와 매핑된 적이 없는 새로운 객체
준영속 상태(Detach)
이전에 영속 상태였다가 영속성 컨텍스트에서 분리된 상태
한번은 영속성 컨텍스트에 의해 관리되었던 이력이 있는 객체
이미 식별자(ID)를 갖고 있으며 데이터베이스에 저장된 적이 있는 객체

2.4.2. 핵심 차이점

이력 측면
비영속: 영속성 컨텍스트와 한 번도 관계를 맺은 적이 없음
준영속: 한 번이라도 영속 상태였던 적이 있음(관계 이력 있음)
식별자(ID) 보유
비영속: 식별자 값이 없을 수 있음 (설정했더라도 DB와 매핑되지 않음)
준영속: 반드시 식별자 값을 가지고 있음(이미 DB에 저장된 적이 있음)
영속화 방법
비영속: persist() 메서드로 영속화
준영속: merge() 메서드로 재영속화

2.4.3. 준영속 상태가 필요한 이유

식별자(ID)의 존재와 그 의미 때문
DB와의 연결고리
준영속 객체는 ID를 갖고 있음. 이 ID는 DB의 특정 레코드와 연결되어있음을 의미
만약 이 객체를 영속화해야 할때, JPA는 ID를 보고 DB와 연결되었음을 인지하고 UPDATE 를 준비 (merge 동작의 핵심)
비영속 객체와의 구분
만약 준영속 객체를 비영속 객체로 취급한 경우, JPA는 이 객체가 DB에 이미 존재하는 레코드에 해당하는지 알 방법이 없음
persist() 를 호출하면 JPA는 이 객체를 완전히 새로운 데이터를 인식하고 INSERT 시도
이는 대부분의 경우 PK 중복 오류를 발생시키거나 원치 않는 데이터 삽입으로 이어짐
merge() 의 역할
준영속 상태는 merge() 메서드와 밀접한 관련이 있음
merge() 는 준영속 객체의 ID를 사용하여 영속성 컨텍스트에서 해당 ID를 가진 영속 객체를 찾거나 DB에서 조회한 후 준영속 객체의 변경사항을 영속 객체에 병합(반영)하는 역할을 함
이 과정은 객체가 준영속 객체 (ID를 가지며, DB와 연결고리를 가진 상태)이기에 가능
결론적으로, 준영속 상태는 "DB에 대응하는 레코드는 있지만, 지금 당장 JPA가 관리하고 있지는 않은" 상태를 명확히 표현하기 위해 존재하며, 특히 merge()를 통한 데이터 수정 및 병합 시나리오에서 필수적인 개념임

3. 영속성 컨텍스트의 추가 기능

3.1. 트랜잭션을 지원하는 쓰기 지연(Transactional Write-Behind)

persist 실행 시점에 쿼리를 전송하지 않고 트랜잭션을 커밋할 때 모아둔 쿼리를 한번에 DB에 보내는 방식
persist() 는 DB에 저장하라는 것이 아님
DB에 저장할 수 있게 영속성 컨텍스트에 등록해주세요 라는 의미
예시 코드 1) ID가 SEQUENCE 타입인 객체 저장
public class DiscountPolicy { @Id @GeneratedValue(generator = "discount_seq") private Long id; // 중략 } @DataJpaTest(showSql = false) public class JpaPersistenceContextTest { @Autowired private EntityManager em; @Test public void sequence_transactional_write_behind() { DiscountPolicy policy1 = new DiscountPolicy(1L, DiscountPolicy.PolicyType.AMOUNT_POLICY, Money.wons(1000), null); DiscountPolicy policy2 = new DiscountPolicy(1L, DiscountPolicy.PolicyType.PERCENT_POLICY, Money.wons(1000), null); // 객체 영속화됨 (Transient -> Persitent) // select ext value for discount_seq em.persist(policy1); em.persist(policy2); // 영속성 컨텍스트 플러시. 이때 전체 쿼리가 일괄 실행됨 // insert into discount_policy (...policy1...) values... // insert into discount_policy (...policy2...) values... em.flush(); } }
Java
복사
예시 코드 2) ID가 IDENTITY 타입인 객체 저장
public class Movie { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; // 중략 } @DataJpaTest(showSql = false) public class JpaPersistenceContextTest { @Autowired private EntityManager em; @Test public void identity_transactional_write_behind() { Movie movie1 = new Movie(1L, "영화1", 120, Money.wons(10000)); Movie movie2 = new Movie(1L, "영화2", 120, Money.wons(10000)); // Auto Increment ID 채번을 위해 `persist` 시점에 INSERT 쿼리 실행 // insert into movie (... 영화1...) values ... // insert into movie (... 영화2...) values ... em.persist(movie1); em.persist(movie2); em.flush(); } }
Java
복사

3.2. 보장된 객체 식별자 범위 제공(Guaranteed Scope Object Identity)

1차 캐시 (First Level Cache) 역할
예시
1.
영속 상태의 1번 Screening 객체를 다시 조회 find()
2.
식별자 맵에 1번 Screening이 존재하는지 확인
3.
존재한다면 해당 객체를 반환
예시 코드 - 캐싱된 데이터 조회
@DataJpaTest(showSql = false) public class JpaPersistenceContextTest { @Autowired private EntityManager em; @Test public void first_level_cache() { DiscountPolicy policy1 = new DiscountPolicy(1L, DiscountPolicy.PolicyType.AMOUNT_POLICY, Money.wons(1000), null); // 객체 영속화됨 (Transient -> Persitent) em.persist(policy1); // 영속성 컨텍스트 플러시. 이때 쿼리가 일괄 실행됨 // insert into discount_policy (...policy1...) values... em.flush(); // 트랜잭션이 종료 되지 않았으므로 여전히 영속성 컨텍스트 내 영속 객체가 존재함 // 따라서 find 시점에 SELECT 쿼리가 실행되지 않음 DiscountPolicy loadedPolicy = em.find(DiscountPolicy.class, policy1.getId()); assertThat(policy1).isEqualTo(loadedPolicy); } }
Java
복사

3.3. 자동 변경 감지 (Automatic Dirty Checking)

JPA는 기본적으로 업데이트를 자동으로 해줌 (em.update 등이 필요 없음)
1.
DB에서 조회하여 영속성 컨텍스트 내 원본과 스냅샷을 생성하여 (영속화) 원본 반환함
2.
클라이언트가 영속 객체의 상태를 수정하는 경우, 영속성 컨텍스트 내 원본 객체가 수정됨
3.
플러시 시점에 원본 객체와 1번의 스냅샷 객체를 비교하여 UPDATE 쿼리를 쓰기 지연 SQL 저장소에 등록
4.
INSERT, UPDATE DELETE 쿼리 정렬 후 DB 반영
예시 코드
@DataJpaTest(showSql = false) public class JpaPersistenceContextTest { @Autowired private EntityManager em; @Test public void automatic_dirty_checking() { DiscountPolicy policy1 = new DiscountPolicy(1L, DiscountPolicy.PolicyType.AMOUNT_POLICY, Money.wons(1000), null); // 객체 영속화됨 (Transient -> Persitent) em.persist(policy1); // 쿼리 실행: insert into discount_policy (...policy1...) values... em.flush(); // 영속성 컨텍스트 내 원본 객체 변경 policy1.setAmount(Money.wons(2000)); // 원본과 스냅샷 비교 후 UPDATE 쿼리 등록 후 쿼리 실행 // persist 메서드를 호출하지 않아도 자동으로 변경 감지 후 UPDATE 쿼리 실행 // Update discount_policy Set amount=20000 where id = 2 em.flush(); } }
Java
복사

4. 두가지 중요한 영속성 컨텍스트 메커니즘

4.1. 도달 가능성에 의한 영속성 (Persistence by Reachability)

해설
영속 상태의 객체 하위에 비영속 상태의 객체를 연결하는 경우 → 자동으로 영속 상태 전파
하나만 저장되면 아래 객체들이 같이 저장됨 → 캡슐화의 관점
예시
1.
id가 2인 Movie 객체 생성 후 영속 상태인 Screening 객체에 매핑
a.
이때 Movie 객체는 비영속 상태
2.
entityManger.flush()
a.
flush 시점에 Screening의 영속 상태가 Movie 객체에 전파
b.
Movie 객체 영속화
3.
Screening 객체를 저장하면 Movie 객체도 함께 저장됨

4.2. 지연 로딩 (Lazy Loading)

즉시 로딩(Eager Loading)
특정 객체를 조회할 때 연관 하위 객체를 함께 조회하게 함
즉시 로딩 방법은 꼭 조인이 아닐 수도 있음
지연 로딩(Lazy Loading)
즉시 로딩을 사용할 경우 불필요하게 연관 하위 객체가 항상 조회됨
따라서 실제 조회하고자 하는 대상이 필요할때까지 조회를 미루는 것을 의미
이를 미루기 위해 실제 엔티티를 생성하지 않고 프록시 객체를 생성
예시
1.
Screening 1 조회
a.
이때 하위 객체인 Movie는 Proxy 타입으로 존재
2.
Screening 에서 Movie로 접근
a.
DB에서 Movie 로드
b.
Proxy 객체를 Movie 객체 변경하고 영속상태로 변경
프록시 객체
프록시 객체는 실제 엔티티를 상속받은 가상의 객체임
하이버네이트가 런타임에 동적으로 생성하는 특수한 클래스의 인스턴스
프록시 객체는 실제로 바이트코드 조작 라이브러리를 통해 런타임에 동적으로 생성
(ManyToOne 연관관계의 경우)최초 생성 시점에는 ID 값만 가지고 있음
주의 사항
영속성 컨텍스트의 생명주기는 트랜잭션 단위임
따라서, 트랜잭션이 끝난 이후 하위 객체를 조회하려고 할 경우 LazyInitializationException 발생