설모의 기록

[Spring] 영속성이란 (persistence) 본문

언어/Spring

[Spring] 영속성이란 (persistence)

HA_Kwon 2018. 8. 12. 01:17


JPA Persistence



Spring JPA 를 공부하다보면 persistence 라는 말을 많이 들어보게 됩니다. 

(출처 : 네이버 영어사전)


무슨 소리지...하고 매번 넘어갔었는데 이번 기회에 정리를 하게 되었습니다. JPA 에서의 영속성은 위의 단어 해석처럼 Entity를 영구적으로 저장해주는 환경을 의미합니다. 책의 내용을 바탕으로 자세하게 정리해보겠습니다.




EntityManagerFactory 와 EntityManager

데이터베이스를 하나만 사용하는 애플리케이션은 보통 EntityManagerFactory 를 하나만 생성합니다. EntityManagerFactory 는 여러 EntityManager 를 생성하는 객체입니다. 두 객체의 차이점은 아래와 같습니다.

  • EntityManagerFactory - 생성하는데 비용이 크기 때문에 애플리케이션 전체에서 한 번만 생성해 공유하도록 설계되어 있다. - 여러 스레드가 동시에 접근해도 안전하다. 따라서 서로 다른 스레드 간에 공유가 가능하다.

  • EntityManager - 생성하는데 비용이 거의 들지 않는다. - 여러 스레드가 동시에 접근하면 동시성 문제가 발생하기 때문에 스레드 간에 절대 공유하지 않는다.


참고로 EntityManager 는 데이터베이스 연결이 꼭 필요한 시점까지 (보통 트랜잭션을 시작할 때) 커넥션을 얻지 않습니다. EntityManagerFactory 와 EntityManager 를 생성하는 코드는 아래와 같습니다.

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("person"); // 파라미터 : 영속성 단위 이름
EntityManager entityManager = entityManagerFactory.createEntityManager();




영속성 컨텍스트란?

위에서도 말했다시피 영속성 컨텍스트(persistence context) 는 '엔티티를 영구 저장하는 환경' 이라는 뜻입니다. EntityManager 를 이용해 Entity 를 저장하거나 조회할 때 EntityManager 는 영속성 컨텍스트에 Entity 를 보관하고 관리합니다. EntityManger객체.persist(Entity객체) 를 실행하면 영속성 컨텍스트가 Entity 를 관리하게 됩니다.

영속성 컨텍스트는 눈에 보이지 않는 논리적인 개념입니다. 또한 EntityManager 를 하나 생성할 때 하나가 만들어지며, EntityManager 를 통해 접근할 수 있고 관리할 수 있습니다.

영속성 컨텍스트의 특징은 아래와 같습니다.

  • 영속성 컨텍스트는 Entity 를 식별자 값으로 구분합니다. Entity에서 @Id 어노테이션을 통해 지정한 멤버변수가 영속성 컨텍스트에 식별자 값으로 저장됩니다.

  • JPA 는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 Entity를 데이터베이스에 반영합니다.(이런 과정을 flush라 합니다.)

  • 1차 캐시를 이용합니다. 영속성 컨텍스트 내부에 존재하는 캐시(Map)를 1차 캐시라 합니다. 영속 상태의 Entity는 모두 이곳에 저장되며 키는 @Id 로 매핑한 식별자이며 값은 Entity 인스턴스입니다. entityManager.find() 메소드를 호출하면 먼저 1차 캐시에서 Entity를 찾고, 만약 찾는 Entity 가 1차 캐시에 없으면 데이터베이스에서 조회한 후 1차 캐시에 저장하고 영속 상태인 해당 객체를 반환합니다.

  • 객체의 동일성을 보장합니다. Person hyeona = new Person(1, "hyeona"); // id, name entityManager.persist(hyeona); Person hyeona1 = entityManager.find(Person.class, 1); Person hyeona2 = entityManager.find(Person.class, 1); hyeona1 == hyeona2 // true 이와 같이 1차 캐시에 있는 같은 Entity 인스턴스를 반환하기 때문에 Entity의 동일성을 보장합니다.

  • 트랜잭션을 지원하는 쓰기 지연을 수행합니다. EntityManager 는 트랜잭션을 commit 하기 직전까지 데이터베이스에 Entity 를 저장하지 않고 영속성 컨텍스트 내부의 SQL 저장소에 생성 쿼리를 저장해둡니다. 이 후 commit 을 하게 되면 저장해두었던 쿼리를 데이터베이스에 보냅니다. 이것을 트랜잭션을 지원하는 쓰기 지연이라고 합니다. 트랜잭션을 commit 하면 EntityManager 는 영속성 컨텍스트를 flush() 합니다. flush() 란 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업으로 등록, 수정, 삭제한 Entity 를 데이터베이스에 반영합니다. 즉, 쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 보냅니다. 이러한 동기화 작업을 거친 후 실제 데이터베이스 트랜잭션을 commit 합니다.

  • 변경을 감지합니다. 영속성 컨텍스트에는 이전 flush() 때의 Entity 상태를 복사해서 저장해둔 스냅샷이 존재합니다. JPA는 flush() 시점에 스냅샷과 Entity를 비교해 변경된 Entity를 찾습니다. 만약 있다면 각각의 객체에 대한 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 저장한 후 한꺼번에 데이터베이스로 보내고 데이터베이스 트랜잭션을 commit 합니다. 여기서 수정 쿼리는 Entity의 모든 필드를 업데이트합니다. 이렇게 하면 수정 쿼리를 애플리케이션 로딩 시점에 미리 생성해두고 재사용할 수 있으며, 데이터베이스에 동일한 쿼리를 보낼 때 데이터베이스가 이전에 한 번 파싱된 쿼리를 재사용하기 때문에 성능상 장점이 됩니다. 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용됩니다. 바꿔 말하면, 준영속 상태의 객체는 아무리 수정해도 영속성 컨텍스트가 변경을 감지하지 못합니다.

  • 지연 로딩을 수행합니다. 지연 로딩(Lazy Loading) 이란 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 실제 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법입니다.


참고로 EntityManager 는 데이터베이스 연결이 꼭 필요한 시점까지 (보통 트랜잭션을 시작할 때) 커넥션을 얻지 않습니다. 





Entity 생명주기

Entity 는 위의 그림과 같이 4가지의 상태가 존재합니다.

  • 비영속(new) - 영속성 컨텍스트와 전혀 관계가 없는 상태 - 상태 : 객체 생성 Person hyeona = new Person("hyeona");

  • 영속(managed) - 영속성 컨텍스트에 저장된 상태 - 상태 : 객체 생성 후 EntityManager 를 통해 영속성 컨텍스트에 저장   entityManager.persist(hyeona);

  • 준영속(detached) - 영속성 컨텍스트에 저장되었다가 분리된 상태 - 상태 : 영속성 컨텍스트가 영속 상태였던 Entity 를 관리하지 않음 (3가지 방법) (1) entityManager.detach(hyeona); (2) entityManager.closed(); // 영속성 컨텍스트 종료 (3) entityManager.clear(); // 영속성 컨텍스트 초기화 - 준영속 상태가 되면 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거됨 - 특징 (1) 영속성 컨텍스트가 제공하는 어떠한 기능(1차 캐시 등) 도 동작하지 않습니다.   (2) 이미 한 번 영속 상태였기 때문에 반드시 식별자 값을 가지고 있습니다. (3) 지연 로딩 시 문제가 발생합니다.

  • 삭제(removed) - 삭제된 상태 - 상태 : 영속성 컨텍스트와 데이터베이스에서 Entity 삭제 entityManager.remove(hyeona);




준영속 상태를 영속 상태로(merge)

준영속 상태인 Entity를 다시 영속 상태로 변경하려면 병합(merge) 를 이용합니다. 예제를 보겠습니다.

Person hyeona = new Person(1, "hyeona");
entityManager.persist(hyeona);
entityManager.detach(hyeona);
hyeona.setUserName("hhyyaa");
Person mergedHyeona = entityManager.merge(hyeona);

hyeona.getUserName(); // hyeona
mergedHyeona.getUserName(); // hhyyaa
entityManager.contains(hyeona); // false
entityManager.contains(mergedHyeona); // true

위와 같이 merge() 메소드를 수행하면 이전 Entity인 hyeona 가 영속 상태로 변경되는 것이 아니라 새로운 영속 상태의 Entity 를 반환합니다. 따라서 hyeona 와 mergedHyeona 는 같은 객체가 아닙니다.

merge() 의 과정을 더 자세하게 보겠습니다. 

파라미터로 넘어온 hyoena 를 식별자 값인 1로 1차 캐시에서 Entity 를 조회합니다. 만약 1차 캐시에 Entity 가 없으면 데이터베이스에서 Entity 를 조회하고 1차 캐시에 저장합니다. 만약 데이터베이스에도 없다면 새로운 Entity 를 생성해서 병합합니다.

조회한 영속 상태인 Entity(위의 코드에서 mergedHyeona) 에 hyeona 엔티티의 값을 채워 넣습니다. 그리고 난 후 mergedHyeona 를 반환합니다.

이렇게 되면 hyoena 객체는 merge() 후에도 준영속 상태로 남아 있습니다.




flush() 

flush() 는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영합니다. 자세한 과정은 아래와 같습니다.

  • 변경 감지가 동작해 영속성 컨텍스트에 있는 모든 Entity를 스냅샷과 비교한 후 수정된 Entity를 찾습니다. 수정된 Entity는 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 저장합니다. (등록, 수정, 삭제 쿼리)

  • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송합니다.


flush() 를 호출하는 방법은 3가지입니다.

  • 직접 호출
    : EntityManager 의 flush() 를 직접 호출해 강제로 플러시하는 방법입니다. 거의 사용하지 않는 방법입니다.

  • 트랜잭션 commit 시 플러시 자동 호출
    : JPA는 트랜잭션을 commit 할 때 자동으로 flush() 를 호출합니다.

  • JPQL 쿼리 실행 시 플러시 자동 호출
    : JPQL 같은 객체지향 쿼리를 호출할 때도 쿼리 실행 직전에 플러시가 자동으로 실행됩니다.



출처 : 자바 ORM 표준 JPA 프로그래밍

Comments