[JPA] 1.영속성 컨텍스트

Updated:

영속성 컨텍스트

이번에는 JPA 사용시 가장 중요한 2가지 포인트 중 하나인 JPA 내부 동작과정에 대해 알아본다. 이를 위해 영속성 컨텍스트 작동방식을 살펴본다

배경지식

-영속성 컨텍스트를 이해하기 위해 우선 엔티티 매니저엔티티 매니저 팩토리에 대해 알아보자

Entity Manager Factory

  • 엔티티 매니저를 만드는 역할
  • 생성비용이 만만치 않고 여러 스레드가 동시에 접근해도 안전하기 때문에 하나만 만들어 애플리케이션 전체에서 공유한다

Entity Manager

  • 엔티티의 CRUD 등 엔티티와 관련된 일을 처리
  • 여러 스레드가 동시에 접근 시 문제가 발생하므로 여러 개를 생성하여 사용

1.영속성 컨텍스트

1-1. 정의


영속성 컨텍스트란 엔티티를 영구 저장하는 공간을 의미한다

  • 영속성 컨텍스트에서 특정 엔티티를 관리하기 위해서 pk값이 필수적이다. 이는 뒤에 엔티티 매핑 글에서 이어지는 내용이다. 일단, 알아만 두자
  • 엔티티 매니저 생성시에 영속성 컨텍스트는 하나만 만들어지며, 엔티티 매니저를 통해 영속성 컨텍스트에 접근하고 관리

  • em.persist(entity)SQL에 INSERT쿼리를 날리는 것이 아니라 영속성 컨텍스트의 공간에 해당 엔티티를 저장하는 기능을 수행
    • 정확히 설명하면 식별자를 key값으로 하여 영속성 컨텍스트 내부의 1차 캐시에 엔티티를 저장
    • 참고로 SQL에 INSERT쿼리를 날리는 것은 트랜잭션 COMMIT 시점에 수행
  • 엔티티 매니저(영속성 컨텍스트)는 트랜잭션 종료시 close() 메서드로 삭제
    • 이때 1차 캐시도 같이 날아감

1-2. 엔티티의 생명주기


  • 비영속: 영속성 컨텍스트와 전혀 상관관계가 없는 새로운 상태
  • 영속: 영속성 컨텍스트에 의해 관리되는 상태
  • 준영속: 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제: 삭제된 상태

영속상태로 들어가는 방법

  1. em.persist()
    • 해당 엔티티를 영속성 컨텍스트의 1차 캐시에 저장
  2. em.find()
    • 식별자값을 key로 하여 해당 엔티티를 1차 캐시에서 찾아오는데, 없을 경우 DB에서 가져와 1차 캐시에 넣어둠

1-3. 영속성 컨텍스트의 특징


  • 영속성 컨텍스트는 엔티티를 식별자값으로 구분한다
    • 헷갈릴 수 있는데 여기서 엔티티라는 것은 테이블에서 각 튜플을 생각하면됨
  • JPA는 보통 트랜잭션 COMMIT 시점에 1차 캐시의 값을 DB에 반영하는데 이를 플러시(flush)라고 부름
  • 이외에도 영속성 컨텍스트가 엔티티를 관리하면 좋은 점들은 아래에서 설명해보겠음

1-4. 영속성 컨텍스트 이용시의 장점


1-4a. 1차 캐시를 이용한 엔티티 조회 상의 이점

  • 영속성 컨텍스트는 내부에 1차캐시라는 저장공간을 가지며 영속 상태의 엔티티들은 모두 이곳에 저장됨
  • key는 @Entity로 설정한 클래스에서 @Id로 매핑한 식별자이고 value는 엔티티 인스턴스이다

Member member = new Member();
member.setId("member1"); //1차 캐시에서 @Id가 "member1"이라고 생각하면 됨
member.setUsername("lim");

em.persist(member); //1차 캐시에서 Entity가 member라고 생각하면 됨

Member findMember = em.find(Member.class, "member1");
  • 위의 코드와 같이 em.persist()를 이용하여 Member 엔티티를 영속성 컨텍스트 내부의 1차캐시에 저장하고 em.find()를 통해 해당 식별자를 가지고 있는 엔티티를 조회함
Member member1 = em.find(Member.class, "member1");
Member member2 = em.find(Member.class, "member1");

System.out.println(member1 == member2); //true
  • 이번에는 다른 코드를 살펴보자. 위의 코드는 모두 member1 이라는 식별자를 가지는 Member 엔티티를 조회하는 코드이다. 결과는 true가 출력된다. 왜 그럴까?

  • 일단, 1차캐시에 member1을 식별자로 하는 Member 엔티티가 들어있다고 가정해보자. 그러면, 첫번째 줄에서 em.find()를 통해 DB에 직접 접근하지 않고 1차캐시를 훑어 엔티티를 반환할 것이다. 두번째 줄의 em.find()도 동일할 것이다. 그러므로, true를 반환한다
  • 1차캐시에 Member 엔티티가 들어있지 않은 경우에는 첫번째 줄에서 DB에 접근하여 해당 엔티티를 1차 캐시에 저장해놓은 후 반환한다. 두번째 줄은 1차 캐시에서 바로 가져온다
  • 이는 하나의 동일한 트랜잭션 내에서 동작하기 때문에 가능한 결과이다

영속성 컨텍스트 내부의 1차캐시를 이용하여 DB에 대한 접근을 최소화

1-4b. 쓰기지연을 이용한 엔티티 등록 상의 이점

  • 영속성 컨텍스트는 트랜잭션 COMMIT 이전까지 내부 쿼리 저장소에 INSERT SQL을 모아두었다가 COMMIT 시에 한번에 DB로 보내 쿼리를 수행하는데 이를 트랜잭션을 지원하는 쓰기 지연이라고 한다. 아래 코드를 살펴보자
EntityTransaction transaction = em.getTransaction();

//엔티티매니저는 데이터 변경 시 트랜잭션을 시작해야 함
transaction.begin(); //트랜잭션 시작

em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 내부쿼리 저장소에 저장할 뿐 DB로 보내지 않는다

transaction.commit(); //이때 트랜잭션 커밋을 통해 엔티티를 DB에 저장시킨다
  • em.persist() 메서드를 실행하면 엔티티를 1차캐시에 저장하는 동시에 INSERT SQL 쿼리를 만들어 쓰기 지연 SQL 저장소에 쌓아둔다
  • 이후 트랜잭션 COMMIT 시에 플러시하여 영속성 컨텍스트의 변경 사항을 DB와 동기화하는 작업을 수행한다
    • 쓰기 지연 SQL 저장소에 쌓인 CRUD 쿼리들을 수행하여 DB에 반영한다

쿼리들을 쓰기 지연 SQL 저장소에 모아두었다가 트랜잭션 COMMIT 시에 한번에 처리

1-4c. 변경감지(dirty checking)를 이용한 엔티티 수정 상의 이점

  • 우선 변경감지에 대해 알아보자. JPA는 엔티티를 1차 캐시에 저장할때 최초 상태를 복사하여 저장해두는데, 이를 스냅샷이라고 한다. 트랜잭션을 COMMIT 시점에 스냅샷과 현재 엔티티를 비교하여 변경점을 찾아낸다. 변경된 엔티티가 존재하면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소를 거쳐 DB에 보낸다. 이 과정을 변경감지라고 한다
  • 변경감지 기능을 이용하면 변경한 엔티티를 em.persist()할 필요 없이 setter만으로 DB의 엔티티를 수정가능하다. 단, 영속상태에 있는 엔티티만 가능하다는 점 잊지 말자!!!
    • 참고로 변경감지로 인해 실행된 UPDATE SQL의 쿼리는 변경된 부분만이 아닌 전체 필드를 업데이트한다. 전송량이 증가한다는 단점이 있지만 재활용이 가능하다는 점이 장점이다

변경감지(dirty checking)을 이용하여 따로 update() 메서드 없이도 DB를 수정 가능

1-5. 엔티티 삭제


  • em.remove()을 통해 엔티티를 삭제할 수 있다
  • 1차 캐시의 엔티티는 즉시 삭제되지만, DB의 엔티티는 트랜잭션 COMMIT 시점에 쓰기 지연 SQL 저장소의 쿼리들이 수행되며 삭제된다

2. 플러시

  • 영속성 컨텍스트의 변경 사항을 DB와 동기화하는 작업이다
  • 영속성 컨텍스트를 플러시하는 방법은 크게 2가지로 나뉜다
    1. em.flush()로 직접 호출하는 방법
    2. 트랜잭션 COMMIT 또는 JPQL 쿼리 실행을 통해 플러시를 자동 호출하는 방법
  • JPQL 쿼리 실행 시에도 플러시를 자동 호출하는 이유를 아래 코드를 통해 살펴보자. 지금은 단순히 알아두기만 하자
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

//중간에 JPQL 실행
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();
  • 우리는 em.persist()를 실행해도 JPA의 쓰기지연 기능 때문에 트랜잭션 COMMIT 전까지는 DB에 데이터가 저장되지 않는다는 점을 알고 있다. 이 상태에서 em.createQuery()를 실행해도 DB에 데이터가 없으니 당연히 조회가 되지 않는다. 이를 방지하기 위해 JPA는 JPQL 쿼리 실행 시에 무조건 플러시를 자동 호출한다
  • 플러시가 실행되도 변경 감지 및 쓰기 지연 SQL 저장소에 있는 쿼리가 DB에 반영될 뿐, 1차 캐시를 비우지는 않는다

트랜잭션이라는 작업 단위가 굉장히 중요하다. 즉, 커밋 직전에만 동기화하면 된다

3. 준영속 상태

  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 의미한다
  • 준영속 상태에 속해 있으면, 변경 감지, 쓰기 지연 SQL 등 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다
  • em.detach()를 통해 특정 엔티티를 준영속 상태로 전환할 수 있고 em.clear()를 통해 영속성 컨텍스트를 통째로 초기화시켜 버릴 수도 있다

Tags: ,

Categories:

Updated:

Leave a comment