영속성 컨텍스트는 JPA에서 엔티티를 보관하고 관리하는 공간, 환경에 해당한다고 할 수 있다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 생성되며, 엔티티 매니저를 통해 접근할 수 있고, 관리할 수 있다.
엔티티는 4가지의 상태로 존재한다.
첫번째는 비영속 상태로 엔티티 객체를 생성한 직후의 순수한 객체 상태이며, 영속성 컨텍스트나 데이터베이스와 관련이 없다.
두번째는 영속 상태로 엔티티 매니저를 통해 영속성 컨텍스트에 저장된 상태 혹은 조회 쿼리 등을 통해 조회된 엔티티를 의미한다. 영속 상태의 엔티티는 영속성 컨텍스트에 의해 관리된다.
세번째는 준영속 상태로 영속 상태의 엔티티가 영속성 컨텍스트에서 더이상 관리하지 않은 상태가 되는 것이다. 이는 엔티티 매니저에서 detach로 분리시키거나, close로 영속성 컨텍스트를 종료하는 경우, cleare를 호출하여 영속성 컨텍스트를 초기화하는 경우에 해당한다.
네번째는 삭제 상태로 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제하는 경우이다.
영속성 컨텍스트는 몇 가지 특징을 갖고 있는데,
첫번째는 영속성 컨텍스트는 기본적으로 엔티티를 식별자 값으로 구분한다는 것이다. 즉 영속 상태의 엔티티는 항상 식별자 값을 갖고 있다는 것이다.
두번째는 영속성 컨텍스트의 엔티티가 데이터베이스에 실제로 반영되는 시점으로, JPA는 트랜잭션을 커밋하는 순간 엔티티를 데이터베이스에 반영하는 데 이를 플러시라 한다.
세번째는 영속성 컨텍스트를 사용함으로써 얻을 수 있는 이점으로, 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 등이다.
엔티티의 CRUD에 기반하여 영속성 컨텍스트의 특징을 살펴보면 다음과 같다.
엔티티의 조회
1차 캐시
영속성 컨텍스트는 내부에 캐시를 갖고 있는데 이를 1차 캐시라고 하며, 영속 상태의 엔티티는 모두 이곳에 저장된다. 1차 캐시는 엔티티의 ID 값을 키로 하고, 엔티티 인스턴스를 값으로 갖는 Map으로 생각해볼 수 있다. 그렇기 때문에 우리가 엔티티 매니저를 통하여 데이터를 조회하게 되면, ID를 이용하여 1차 캐시를 먼저 조회하며, 1차 캐시에 데이터가 존재하지 않는 경우 데이터베이스에 접근하여 데이터를 조회하게 된다. 데이터베이스에서 조회된 데이터는 1차 캐시에 저장된 후 영속 상태에서 반환되게 된다.
동일성 보장
같은 ID로 조회된 엔티티는 1차 캐시에 저장된 값들에 대한 동일 인스턴스의 반환이기 때문에, 값은 동등성(equal)이 아닌 동일하다(identity)는 특징을 갖는다.
엔티티의 등록
쓰기 지연
엔티티를 등록하는 경우 엔티티 매니저는 트랜잭션 커밋 직전까지 INSERT SQL을 내부 쿼리 저장소에 저장해두었다가 트랜잭션 커밋 시에 한번에 등록하게 된다. 이를 쓰기 지연이라하며, 앞서 예시로 든 엔티티 등록의 경우를 좀 더 상세하게 살펴보면, 엔티티 인스턴스 생성 후 persist 명령어를 통하여 영속 상태로 전환하게 되면, 엔티티는 1차 캐시에 저장되고, 내부 쿼리 저장소에 INSERT SQL이 저장된다. 커밋 시에 엔티티 매니저는 우선 영속성 컨텍스트를 플러시하게 된다. 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업으로, 등록, 수정, 삭제된 엔티티 정보를 내부 쿼리 저장소에 저장된 SQL을 데이터베이스에 전송함으로써 동기화하게 된다.
엔티티의 수정
변경감지
기존의 SQL 쿼리를 직정 작성하여 데이터를 수정하는 경우, 변경되는 데이터의 범위에 따라 수정 쿼리를 준비해야한다는 단점이 있다. 이는 수정 쿼리가 많아지는 것은 단점은 물론이고, 비즈니스 로직을 이해하기 위해서 SQL을 확인해야되기 때문에 비즈니스 로직이 SQL에 의존하게 된다는 단점을 갖게 된다.
JPA에서는 엔티티의 변경을 감지하여, 플러시를 하면서 수정 쿼리를 데이터베이스에 전송하여 데이터를 수정하게 된다. 변경을 감지하기 위하여 엔티티가 영속성 컨텍스트에 저장된 순간의 스냅샷을 1차 캐시에 저장하며, 플러시 시점에 이 스냅샷과 엔티티를 비교하여 변경된 내용이 있으면, 수정쿼리를 생성하여 내부 쿼리 저장소로 보내었다가 이 쿼리를 데이터베이스로 전송하게 된다. 이러한 흐름에서 알 수 있듯이 변경 감지는 일차적으로 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다.
JPA는 기본 전략으로 엔티티의 모든 필드를 매번 업데이트하는 방식을 취하는데, 이는 데이터 전송량을 증가시키는 단점이 있지만, 수정쿼리가 매번 같게 되고, 이는 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다는 장점과 데이터베이스에 동일한 쿼리를 보내었을 때 이전에 파싱된 쿼리를 재사용할 수 있다는 장점을 갖고 있다.
엔티티 삭제
엔티티는 삭제 시 영속성 컨텍스트에서 제거되지만 데이터베이스에서는 즉시 삭제되지 않는다. 등록시와 마찬가지로 삭제쿼리를 내부의 쓰기 지연 SQL 저장소에 등록하였다가 플러시 시점에 데이터베이스에 전송되며, 플러시를 마치면서 트랜잭션 커밋이 이뤄지게 된다.
플러시
앞서 언급한 대로 영속성 컨텍스트의 엔티티의 내용이 데이터베이스에 반영되는 것은 플러시가 실행되는 시점이다. 플러시는 엔티티 매니저에서 직접 메소드를 호출하는 직접호출과 트랜잭션 커밋 시 자동 호출되는 경우가 있다. 추가로 JPQL 등을 통하여 쿼리 실행 시 플러시가 자동 호출되는 경우가 있는데, 이는 쿼리를 실행하기 전에 영속성 컨텍스트에 저장된 엔티티 정보를 데이터베이스와 싱크하기 위함이다.
준영속
영속성 컨텍스트가 관리하는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라 한다. 이는 다시 말해서 준영속 상태의 엔티티는 영속성 컨텍스트의 기능을 사용하지 못한다는 것이다.
영속 상태에서 준영속 상태로 엔티티 상태를 변경하기 위해서는 엔티티 매니저에서 detach 메소드를 실행시켜 분리하거나, clear 메소드를 이용하여 영속성 컨텍스트를 초기화하는 방법이 있다. 혹은 close를 하여 영속성 컨텍스트를 종료할 수 있다.
준영속 상태의 엔티티는 앞서 말한대로 영속성 컨텍스트의 관리 대상이 되지 않기 때문에, 영속성 컨텍스트의 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등의 대상이 되지 않는다. 하지만 준영속 상태는 비영속 상태와 다르게 ID를 갖고 있는데, 이는 영속 상태에서 준영속 상태로 전환된 것이기 때문이다.
병합
준영속 상태 혹은 비영속 상태의 엔티티를 영속 상태로 변경할 수도 있는데 이 역할을 하는 것이 merge(병합)다. merge 메소드는 준영속, 비영속 상태의 엔티티를 받아 그 정보로 새로운 영속 상태의 엔티티를 반환한다. 병합은 중영속, 비영속을 신경쓰지 않고, 식별자 값으로 엔티티를 조회할 수 있으면 불러서 병합하고, 조회할 수 없으면 새로 생성하여 병합한다. 즉 병합은 save or update 기능을 수행한다.
참고
자바 ORM 표준 HPA 프로그래밍 - 김영한