JPA(Java Persistence API) 02 - 영속성

엔티티 매니저 팩토리(Entity Manager Factory)

엔티티 매니저를 생성하는 객체. 엔티티 매니저 팩토리 생성 비용은 매우 크므로 데이터베이스 당 1개만 만들어서 애플리케이션 전체에서 공유하여 사용한다.

엔티티 매니저(Entity Manager)

엔티티를 관리하는 객체. 엔티티 매니저 팩토리에서 엔티티 매니저를 생성하는 비용은 매우 적다. 여러 스레드가 동시 접근해서 사용하면 문제가 발생하므로 스레드 간 공유는 안 된다.

데이터베이스 연결이 꼭 필요한 시점이 되어야 데이터베이스 커넥션을 획득하여 사용한다. 보통 트랜잭션 시작 시 데이터베이스 커넥션을 획득한다.

엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 저장하여 관리한다.

영속성(Persistence)

어떤 상태가 계속되는(지속되는) 성질이라고 이해하면 된다.

영속성 컨텍스트(Persistence Context)

엔티티가 지속되는 컨텍스트(맥락, 또는 환경이라고도 할 수 있다). 논리적 개념에 가깝다.

엔티티 매니저를 생성할 때 영속성 컨텍스트가 함께 생성된다. 즉 1대1 관계라고 볼 수 있으나, 여러 엔티티 매니저가 동일한 영속성 컨텍스트에 접근할 수도 있다.

영속성 컨텍스트는 1차 캐시, 동일성 보장, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 기능을 지원한다.

엔티티의 상태

  • 비영속(new/transient) : 영속성 컨텍스트와 관계 없는 상태.
  • 영속(managed) : 영속성 컨텍스트에 의해 관리되는 상태.
  • 준영속(detached) : 영속성 컨텍스트에 의해 관리되었다가 분리된 상태.
  • 삭제(removed) : 엔티티가 영속성 컨텍스트와 데이터베이스에서 삭제된 상태.

1차 캐시

영속성 컨텍스트 내부에 캐시가 존재하는데 이것을 1차 캐시라고 부른다. 영속 상태인 엔티티는 여기에 저장되며 영속성 컨텍스트를 통해 엔티티를 조회하면 먼저 1차 캐시에서 찾고 없으면 데이터베이스에서 조회한다. 이 때 데이터베이스에서 조회한 엔티티를 1차 캐시에 저장하고 영속 상태로 만든 엔티티를 반환한다. 이렇게 하여 다음에 해당 엔티티를 조회할 경우 1차 캐시에서 엔티티를 반환할 수 있게 한다.

쉽게 생각하면 영속성 컨텍스트 내부에 키와 값 쌍으로 이뤄진 맵이 하나 있는데 키는 @Id 어노테이션을 매핑한 식별자이고 값은 엔티티 인스턴스라고 생각하면 된다.

1차 캐시에서 엔티티를 조회하는 경우 동일 키의 인스턴스에 대해 동일성을 보장한다. 즉, 영속성 컨텍스트에서 영속 상태의 엔티티를 조회할 때 동일 키를 사용해서 여러 번 조회한 경우 동일한 인스턴스를 반환한다는 의미이다.

트랜잭션을 지원하는 쓰기 지연(Transactional write-behind)

엔티티 매니저는 트랜잭션 커밋 전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소(=쓰기 지연 SQL 저장소)에 SQL을 누적한다. 그리고 트랜잭션 커밋 시 내부 쿼리 저장소에 누적된 쿼리를 데이터베이스에 보내는데 이것은 트랜잭션을 지원하는 쓰기 지연이라고 한다.

트랜잭션을 지원하는 쓰기 지연이 가능한 이유는, 매번 SQL을 데이터베이스에 보내는 방식이나 내부 쿼리 저장소에 SQL을 누적했다가 트랜잭션 커밋 시 한꺼번에 SQL을 데이터베이스에 보내는 방식이나 결론적으로 같은 결과이기 때문이다. 트랜잭션을 커밋하지 않으면 매번 SQL을 데이터베이스에 보내도 소용이 없으며, 트랜잭션 커밋 직전에만 데이터베이스에 SQL을 전달하면 전자나 후자나 같은 결과를 보여줘야 한다.

변경 감지(Dirty checking)

JPA에서 엔티티 수정 후 트랜잭션 커밋 직전에 그 어떤 메서드도 호출할 필요가 없다. 엔티티 데이터만 변경했는데도 데이터베이스에 반영되는 이유는, 엔티티의 변경사항을 자동으로 감지하는 변경 감지 기능이 있기 때문이다.

JPA에서 엔티티를 영속성 컨텍스트에 보관할 때 최초 상태를 스냅샷(Snapshot)으로 복사해서 저장해둔다. 그리고 엔티티 매니저에서 플러시할 때 스냅샷과 현재 엔티티를 비교해서 엔티티의 변경사항을 감지한다.

변경 감지는 영속 상태의 엔티티에만 적용되며 비영속, 준영속 상태의 엔티티는 변경사항이 있어도 변경 감지가 되지 않고 따라서 데이터베이스에 반영되지도 않는다.

JPA의 UPDATE SQL 생성 기본 전략은 엔티티의 모든 필드를 업데이트하는 것이다. 이런 전략을 채택하면 데이터베이스에 보내는 데이터 전송량이 증가하나 아래와 같은 장점이 있다.

  • 모든 필드를 사용하는 수정 쿼리는 항상 같기 때문에 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재사용할 수 있다. 즉, 수정 쿼리의 재사용성 측면에서 이점이 있다.
  • 데이터베이스에 동일 쿼리를 전송하면 데이터베이스는 이전에 파싱된 쿼리를 재사용할 수 있다.

필요에 따라 수정된 필드에 대해서만 UPDATE SQL을 생성하는 전략을 선택할 수 있다. 하이버네이트의 @DynamicUpdate 어노테이션을 엔티티 클래스에 적용하면 된다. 데이터를 저장할 때 데이터가 존재하는 필드만으로 INSERT SQL을 생성하는 @DynamicInsert 어노테이션도 있다.

엔티티 삭제 역시 즉시 삭제하는 것이 아니라 삭제 쿼리가 쓰기 지연 SQL 저장소에 저장되고 트랜잭션 커밋 시 플러시가 호출되면 데이터베이스에 삭제 쿼리를 전달한다. 물론 영속성 컨텍스트에서는 엔티티 삭제 메서드인 remove 메서드를 호출한 순간부터 제거된 상태이므로 해당 엔티티를 재사용하지 말고 가비지 컬렉팅이 되도록 내버려 둘 것.

플러시(Flush)

트랜잭션 커밋 시 영속성 컨텍스트에 반영된 변경사항을 데이터베이스에 동기화한다. 이것을 플러시라고 한다. 플러시는 영속성 컨텍스트에 보관된 엔티티를 지우는 작업이 아님을 명심할 것.

영속성 컨텍스트를 플러시하는 방법은 다음과 같다.

  1. 직접 호출
  2. 트랜잭션 커밋
  3. JPQL 쿼리 실행

JPQL 쿼리 실행 시 플러시가 동작하는 이유는, JPQL 쿼리를 실행하는 시점에 영속성 컨텍스트의 변경사항이 적용되지 않으면 JPQL 쿼리의 결과가 영속성 컨텍스트와 동기화되지 않은 결과가 되기 때문이다.

엔티티 매니저를 통해 플러시 모드를 지정할 수 있다. 엔티티 매니저의 setFlushMode 메서드를 사용하면 되며 파라미터로 FlushModeType Enum을 지정하면 된다.

  • FlushModeType.AUTO : 트랜잭션 커밋 또는 쿼리 실행 시 플러시(기본값).
  • FlushModeType.COMMIT : 트랜잭션 커밋 시에만 플러시.

준영속

영속 상태에서 준영속 상태로 바꾸는 방법은 3가지가 있다.

  • detach : 특정 엔티티를 준영속 상태로 전환한다.
  • clear : 영속성 컨텍스트 초기화. 해당 영속성 컨텍스트의 모든 엔티티가 준영속 상태로 전환된다.
  • close : 영속성 컨텍스트 종료. 마찬가지로 해당 영속성 컨텍스트의 모든 엔티티가 준영속 상태로 전환된다.

준영속 상태의 엔티티는 이전에 영속 상태였다가 준영속 상태가 된 엔티티이므로 식별자 값을 가지고 있다.

준영속 상태의 엔티티는 지연 로딩이 불가능하다.

병합(merge)

준영속 상태의 엔티티를 다시 영속 상태로 전환할 때 병합을 사용한다. merge 메서드는 준영속 상태의 엔티티를 파라미터로 받아서 새로운 영속 상태의 엔티티로 반환한다. 여기서 주의할 것은 파라미터로 넘긴 준영속 상태의 엔티티 자체가 영속 상태로 전환되는 것이 아니라, 해당 엔티티와 동일한 정보를 가진 영속 상태의 엔티티를 새로 만들어 반환해주기 때문에 반환받은 엔티티를 사용해야 한다는 것이다.

비영속 상태의 엔티티도 병합을 통해 영속 상태로 만들 수 있다. 병합은 준영속, 비영속을 신경 쓰지 않기에 save or update 기능을 수행할 수 있다.

위 내용은 김영한님의 자바 ORM 표준 JPA 프로그래밍를 읽으며 개인적으로 요약 및 정리하는 내용이다.
자세한 내용이 알고 싶으면 김영한님의 자바 ORM 표준 JPA 프로그래밍을 직접 읽어보길 추천한다.