JPA

[JPA] 배워서 엔터프라이즈 개발하자 3일차 - 영속성 관리

suhwanc 2022. 3. 12. 03:33

본 글은 김영한님의 [인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의와 자바 ORM 표준 JPA 프로그래밍 책을 기반으로 정리해 작성하였습니다.

자바 ORM 표준 JPA 프로그래밍 책

 

JPA의 역할은 크게 엔티티와 테이블을 매핑하는 부분, 매핑한 엔티티를 실제 사용하는 부분으로 나눌 수 있다.

오늘은 엔티티 매니저(Entity Manager)가 어떤 역할을 하고, 어떻게 사용하는지 알아보자.

 

 

1. 엔티티 매니저 팩토리와 엔티티 매니저

 

데이터베이스를 하나만 사용하는 애플리케이션은 일반적으로 EntityManager Factory를 하나만 생성한다.

다음은 생성 코드이다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("suhwan");

 

여기 Persistence라는 것은 META-INF/persistence.xml 에 있는 정보를 바탕으로 EntityManagerFactory를 생성한다.

(* META-INF/persistence.xml 는 JPA가 필요한 설정 정보를 관리하는 공간입니다. 참조 링크)

 

이렇게 팩토리를 하나 만들게 되면, 다음과 같이 필요할 때마다 엔티티 매니저를 생성하면 된다.

EntityManager em = emf.createEntityManager();

 

엔티티 매니저 팩토리는 말 그대로 공장을 짓는다고 생각하면 되는데, 그만큼 비용이 많이 든다는 의미이다. 따라서 애플리케이션 전체에서 하나만 만들어 공유하도록 설계되어 있다. 대신 여러 스레드가 동시에 접근해도 안전해 서로 다른 스레드 간 공유가 가능하다.

 

하지만 엔티티 매니저는 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안 된다는 것을 명심하자.

 

 

2. 영속성 컨텍스트란?

 

영속성 컨텍스트(persistence context)는 JPA를 이해할 때 아주 어려운 용어라고 한다.

말 그대로 '엔티티를 영구 저장하는 환경' 이라는 뜻인데, 앤티티 매니저로 엔티티를 저장하거나 조회한다는 것은 영속성 컨텍스트에 엔티티를 보관하고 관리한다는 의미와 같다.

즉, persist() 메소드는 엔티티 매니저를 사용해서 엔티티를 영속성 컨텍스트에 저장한다는 의미이다.

 

하지만 영속성 컨텍스트란 눈에 보이는 개념은 아니다. 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어지며, 엔티티 매니저를 통해서 영속성 컨텍스트에 접근하고 관리할 수 있다.

 

 

 

3. 엔티티의 생명주기

 

엔티티에는 4가지 상태가 존재한다.

 

  • 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed) : 영속성 컨텍스트에 저장된 상태
  • 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed) : 삭제된 상태

이제부터 하나씩 알아가 보자.

 

1) 비영속

 

엔티티 객체를 생성했지만, 아직 저장하지 않은 상태이다. 따라서 영속성 컨텍스트 또는 데이터베이스와 전혀 관련이 없는 상태이다.

 

2) 영속

 

엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장했다(persist)

이렇게 영속성 컨넥스트가 관리하는 엔티티를 영속 상태라 한다. 

 

3) 준영속

 

영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않으면 준영속 상태가 된다. 

이 경우 관리하던 엔티티에 대해 em.detach() 함수를 호출하거나, 엔티티 매니저 자체를 em.close(), em.clear() 해도 엔티티는 준영속 상태가 된다.

 

4) 삭제

 

엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한다(remove)

 

 

 

4. 영속성 컨텍스트의 장점

 

영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 있다.

 

1) 1차 캐시

 

영속성 컨텍스트는 내부에 캐시를 두고 있는데, 이를 1차 캐시라고 한다.

영속 상태의 엔티티들은 모두 이곳에 저장되는데, 나중에 em.find() 등을 통해 호출하면 우선적으로 1차 캐시에서 찾고 만약 없다면 데이터베이스에서 조회한다. 따라서 성능상 이점을 누릴 수 있다.

 

2) 동일성 보장

 

위 1차 캐시와 연관된 장점인데, em.find('같은 엔티티')를 반복해서 호출해도 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환하기 때문에 여러번 호출해도 동일성이 보장된다. 즉 '==' 비교의 값이 같다는 것이다.

 

3) 트랜잭션을 지원하는 쓰기 지연

 

엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 차곡차곡 모아둔다. 그리고 이후 커밋 시 모아둔 쿼리를 한꺼번에 보내는데, 이를 트랜잭션을 지원하는 쓰기 지연이라고 한다.

당연히 매 쿼리마다 데이터베이스에 옮기는 작업이 사라지므로 이 또한 성능상 이점을 누릴 수 있다.

 

4) 변경 감지

 

JPA로 엔티티를 수정할 때는 단순히 엔티티를 조회해서 데이터만 변경하면 된다. 단순히 이런 코드로 말이다.

suhwanCoin.setStatus = "-10%";

그냥 보면, 단순히 내 로컬에서 데이터를 변경한 것일뿐인데, 어떻게 데이터베이스에 반영이 되는 걸까?

이렇게 엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 기능을 변경 감지(dirty checking)이라고 한다.

 

과정은 다음과 같다.

(* 사전 지식 : 스냅샷이란 엔티티를 영속성 컨텍스트에 보관할 때, 최초 상태를 복사해서 저장해두는 것을 의미한다.)

 

  • 1. 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 플러시(flush())가 호출된다.
  • 2. 엔티티와 스냅샷을 비교해서 변경된 엔티티를 찾는다.
  • 3. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
  • 4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
  • 5. 데이터베이스 트랜잭션을 커밋한다.

 

단, 변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다. 당연히 영속 컨텍스트 범위 내에 있는 쓰기 지연 SQL 저장소를 사용하기 때문에 밖에 나간 엔티티들은 이 기능을 받지 못할 것이다.

 

만약 여러분들이 직접 JPA를 통해 객체(엔티티)를 수정하고 돌려보셨다면 실행된 UPDATE SQL에서 신기한 부분을 발견할 수 있는데, 

바로 JPA의 기본 수정 전략은 필드를 몇 군데 바꿨는지에 상관없이 모든 필드에 대한 업데이트 쿼리를 날린다는 것이다.

 

이런 방식을 사용하면 데이터베이스에 보내는 데이터 전송량이 증가한다는 단점이 있지만, 수정 쿼리의 틀이 항상 같다는 점, 똑같은 쿼리를 보낼 시 이전에 한 번 파싱된 쿼리를 재사용할 수 있다는 장점이 있어서 이런 식으로 진행한다고 한다.

 

만약 수정된 필드만 바꾸는 쿼리를 날리고 싶다면 @DynamicInsert 어노테이션을 참고하도록 하자.

 

 

5. 플러시

 

플러시(flush())는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다 (동기화의 의미)

플러시 실행 시 이런 일들이 순서대로 일어난다.

  • 1. 변경 감지가 동작해서 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교해 수정된 엔티티를 찾고, 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록한다.
  • 2. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.

 

지금까지 정리한 영속성 관리 부분은 매핑한 엔티티가 어떻게 굴러가는지에 대한 동적인 부분이다. 내일부터는 엔티티와 테이블의 매핑 방법에 대해 좀 더 자세히 알아보는 시간을 갖도록 하자.