영속성 관리
Entity Manager Factory
Entity Manager Factory는 Entity Manager 인스턴스를 관리하는 주체입니다.
Entity Manager Factory를 생성하는 비용은 아주 크므로 애플리케이션 실행 시 데이터베이스 당 딱 1개씩만 생성하고 공유해서 사용해야 하며, 여러 스레드가 동시에 접근해도 안전하므로 스레드 간 공유할 수 있습니다.
Entity Manager
Entity Manager Factory에서 Entity Manager를 생성하며, 이때 생성하는 비용은 거의 들지 않습니다.
대부분의 JPA 기능을 Entity Manager가 제공하며, Entity를 데이터베이스에 등록/수정/삭제/조회 등 엔티티와 관련된 모든 일을 처리합니다.
Entity Manager는 말 그대로 Entity를 관리하는 관리자로, 개발자 입장에서 Entity를 저장하는 가상의 데이터베이스로 생각하면 됩니다.
Entity Manager는 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않으며, 보통 트랜잭션을 시작할 때 커넥션을 획득합니다.
주의해야 할 점은 Entity Manager는 데이터베이스 커넥션과 밀접한 관계가 있어 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 절대로 스레드 간에 공유를 하거나 재사용하면 안 됩니다.
영속성 컨텍스트(persistence context)란?
영속성 컨텍스트(persistence context)는 Entity를 영구 저장하는 환경이라는 뜻으로,
Entity Manager로 Entity를 저장하거나 조회하면 Entity Manager는 영속성 컨텍스트에 Entity를 보관하고 관리합니다.
// Entity Manager를 사용해 회원 entity를 영속성 컨텍스트에 저장
em.persist(member)
영속성 컨텍스트는 Entity Manager 생성 시 만들어지며, Entity Manager를 통해 접근하고 관리할 수 있으며, 아래와 같은 특징이 있습니다.
- 영속성 컨텍스트는 엔티티를 식별자 값(
@Id
)으로 구분하므로, 영속 상태는 식별자 값이 반드시 있어야 합니다. (식별자 값이 없는 경우 예외 발생) - 영속성 컨텍스트에 저장된 내용을
flush()
로 데이터베이스에 반영합니다. - 영속성 컨텍스트로 Entity 관리 시 장점
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
Entity 생명주기
- 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
- 영속(managed) : 영속성 컨텍스트에 저장된 상태
- 준영속(detached) : 영속성 컨텍스트에 저장되어 있다가 분리된 상태
detach()
,clear()
,close()
호출 시 준영속 상태가 됩니다.
- 삭제(removed) : 삭제된 상태
1차 캐시
영속성 컨텍스트는 내부에 캐시를 가지고 있으며, 이를 1차 캐시라고 합니다. 영속 상태의 엔티티는 Map<K, V>
형태로 저장됩니다.
- Key :
@Id
로 매핑한 식별자 값 - Value : Entity 인스턴스
entityManager.find()
메소드 호출시 영속성 컨텍스트의 1차 캐시를 먼저 조회하고, Entity가 1차 캐시에 존재하지 않으면, 데이터베이스를 조회하여 Entity 생성 후 1차 캐시에 저장한 후에 영속 상태의 Entity를 반환합니다.
동일성 보장
val users = User("userA", "userB", "userC")
// persistenceContext(영속성 컨텍스트)에 저장
entityManager.persist(userA)
val user1 = entityManager.find(User::class.java, 1L)
val user2 = entityManager.find(User::class.java, 1L)
user1 shouldBe user2
영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환하므로, user1과 user2는 동일한 인스턴스인 것을 확인할 수 있습니다.
즉, 영속성 컨텍스트는 성능상 이점과 엔티티의 동일성을 보장합니다.
- 동일성(identity) : 실제 인스턴스가 같음 (
==
비교 값이 같음) - 동등성(equality) : 실제 인스턴스는 다를 수 있지만, 인스턴스가 가지고 있는 값이 같음.
트랜잭션을 지원하는 쓰기 지연 (Transactional write-behind)
Entity Manager는 트랜잭션을 커밋하기 직전까지 데이터베이스에 Entity를 저장하지 않고, 내부 쿼리 저장소에 쿼리를 모아두다가 커밋시 모아둔 쿼리를 데이터베이스에 보냅니다.
트랜잭션 커밋시 Entity Manager는 우선 영속성 컨텍스트를 flush
하여 변경 내용을 데이터베이스에 동기화하며, 이때 등록, 수정 삭제한 Entity를 데이터베이스에 반영합니다.
영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화한 후에 실제 데이터베이스 트랜잭션을 커밋합니다.
변경 감지(dirty checking)
JPA는 Entity를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해둡니다. (스냅샷)
그리고 flush 시점에 스냅샷과 비교하여 변경된 내용이 있다면, UPDATE SQL 문을 쓰기 지연 SQL 저장소에 저장해 둔 후 데이터베이스 커밋 시점에 변경 내용을 자동으로 반영합니다.
변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 Entity에만 적용됩니다.
val userA = entityManager.find(User::class.java, 1L)
// 영속성 데이터 수정
// 변경 감지(dirty checking)로 데이터베이스에 자동 반영
userA.firstname = "userAA"
위 코드에서 수정 쿼리에서 실제로 수정된 필드만 반영될 거라 예상할 수 있지만,
UPDATE USER
SET
firstname = ?
WHERE
id = ?
JPA의 기본 전략은 Entity의 모든 필드를 업데이트하는 것입니다.
UPDATE USER
SET
firstname = ?
, lastname = ?
, description = ?
WHERE
id = ?
그 이유는 다음과 같은 장점이 있기 때문입니다.
- 모든 필드를 사용하면 수정 쿼리가 항상 같다. (바인딩 되는 데이터는 다름) 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해 두고 재사용할 수 있다.
- 데이터베이스에 동일한 쿼리를 보내면, 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다.
이렇게 모든 필드를 사용해 데이터베이스에 전송하는 것이 필드가 많거나 저장되는 내용이 너무 크면 수정된 데이터만 사용해서 동적으로 UPDATE SQL을 생성하는 전략을 선택할 수 있습니다.@DynamicUpdate
/ @DynamicInsert
어노테이션으로 동적으로 SQL을 생성하게 지정할 수 있습니다.
@Entity
@DynamicUpdate
data class Member(...)
하나의 테이블에 많은 수의 컬럼이 있는데, 몇몇개의 컬럼만 자주 업데이트 하는 경우에 사용하는 것을 권장하며, 기본 전략 사용 후, 최적화가 필요할 정도로 느린 경우 전략을 수정하면 됩니다.
(상황에 따라 다르지만, 컬럼이 30개 이상이 되면, 정적 수정 쿼리보다 @DynamicUpdate
를 사용한 동적 수정 쿼리가 더 빠르다고 합니다.)
Flush
flush()
는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것입니다.
영속성 컨텍스트를 아래 3가지 방법으로 flush 할 수 있습니다.
flush()
직접 호출- 테스트나 다른 프레임워크와 JPA를 함께 사용하는 경우를 제외하고 거의 사용하지 않습니다.
- 트랜잭션 커밋 시
flush()
자동 호출- 변경 내용을
flush()
로 전달하지 않고, 커밋만 하는 경우 변경 데이터가 반영되지 않습니다. 그러므로 트랜잭션 커밋 전flush()
를 호출하여 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해야 합니다.
- 변경 내용을
- JPQL 쿼리 실행시
flush()
자동 호출- 식별자 기준으로 조회하는
find()
메소드 호출시에는 flush가 실행되지 않습니다.
- 식별자 기준으로 조회하는
FlushModeType
FlushModeType.AUTO
: 커밋이나 쿼리 실행 시 flush (default)FlushModeType.COMMIT
: 커밋할 때만 flush(성능 최적화를 위해 사용)
entityManager.flushMode = FlushModeType.COMMIT
준영속 상태
준영속 상태란, 영속성 컨텍스트가 관리하는 영속 상태의 Entity가 영속성 컨텍스트에서 분리된 것을 의미합니다. 즉, 준영속 상태의 Entity는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없습니다.
em.detach(entity)
: 특정 엔티티만 준영속 상태로 전환em.clear()
: 영속성 컨텍스트 완전히 초기화em.close()
: 영속성 컨텍스트 종료
주로 영속성 컨텍스트가 종료되면서 준영속 상태가 되며, 개발자가 직접 준영속 상태로 만드는 일은 거의 없습니다.
준영속 상태는 아래와 같은 특징이 있습니다.
- 비영속 상태에 가깝다.
- 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않음
- 식별자 값을 가지고 있다.
- 준영속 상태는 이미 한 번 영속 상태였으므로, 반드시 식별자 값을 가지고 있다.
- 지연 로딩을 할 수 없음
- 지연 로딩은 실제 객체 대신 프록시 객체를 로딩해 두고, 해당 객체를 실제로 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법
- 준영속 상태는 영속성 컨텍스트가 더 이상 관리하지 않으므로 지연 로딩 시 문제 발생
merge()
비영속/준영속 상태의 Entity를 다시 영속 상태로 변경하려면 merge()
를 사용하면 됩니다. merge()
는 새로운 영속 상태의 Entity를 반환합니다.
merge는 비영속, 준영속 상태를 신경 쓰지 않으며, 식별자 값으로 Entity를 조회할 수 있으면 조회 후 병합하고, 조회할 수 없는 경우 새로 생성(save or update)하여 병합합니다.