JPA의 영속성 컨텍스트(Persistence context)

so.oy·2024년 6월 4일
1

JPA

목록 보기
2/3

본 내용은 인프런의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 수강하고 정리한 글입니다.

영속성 컨텍스트란?

영속성 컨텍스트란 "엔티티를 영구 저장하는 환경" 이라는 뜻이다. 쉽게 생각하자면, 영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 DB라고 생각하면 된다.

EntityManager.persist(object)
  • persist()를 통해서 영속성 컨텍스트에 객체를 저장한다.
  • 엔티티 매니저를 통해서 영속성 컨텍스트에 접근하고 관리할 수 있다.
  • 엔티티 매니저를 생성할때 하나 만들어진다.

엔티티의 생명주기

엔티티는 EntityManager를 통해 영속성 컨텍스트에 저장이 된다. 하지만, 이는 실제 DB가 아니기때문에 생명주기를 갖는다.

1. 비영속(new/transient)

영속성 컨텍스트와 전혀 관계가 없는 새로운 상태

Member member = new Member()
member.setId(1L);
member.setName("홍길동");

엔티티 객체를 생성했지만,영속성 컨텍스트에 저장하지 않은 상태이다.

2. 영속(managed)

영속성 컨텍스트에 관리되는 상태

// 객체를 생성한 상태
Member member = new Member()
member.setId(1L);
member.setName("홍길동");

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

// 객체를 영속화
em.persist(member);

em.persist(member)를 통해서 Member 객체를 영속화 하였다. 이를 통해 Member 객체는 영속성 컨텍스트에 의해서 관리된다.

3. 준영속(detached)

영속성 컨텍스트에 저장되었다가 분리된 상태

em.detach(member);

회원 엔티티를 영속성 컨텍스트에서 분리하여 더 이상 영속성 컨텍스트에 의해서 관리되지 않도록 한다.

4. 삭제(removed)

삭제된 상태

em.remove(member);

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

영속성 컨텍스트의 장점

  1. 1차 캐시
  2. 동일성(identity) 보장
  3. 트랜잭션을 지원하는 쓰기 지연 (Transactional write-behind)
  4. 변경 감지 (Dirty checking)
  5. 지연 로딩 (Lazy Loading)

1. 1차 캐시

영속성 컨텍스트의 내부에는 1차 캐시 라는 것이 있다. 영속성 컨텍스트에 의해 관리되는 영속 상태의 엔티티를 여기에 저장한다.
이 1차 캐시에는 키와 값이 존재하는데, 여기서 키는 데이터베이스의 PK(식별자 값)이며, 값은 Entity 객체 자체이다.

1-1. 1차 캐시에서 조회 방법

Member member = new Member()
member.setId(1L);
member.setName("홍길동");

// 1차 캐시에 저장
em.persist(member);

// 1차 캐시에서 조회
Member findMember = em.find(Member.class, 1L);

위의 코드를 살펴보면 조회의 흐름을 다음과 같다.
1. 데이터베이스가 아닌 1차 캐시에서 조회한다.
2. 조회하는 값이 있으면, 엔티티를 반환한다.
3. 조회하는 값이 없으면, 데이터베이스에서 조회한다. (이때는 select 쿼리 전송)
4. 데이터베이스에서 찾은 엔티티를 1차 캐시에 저장한다.
5. 엔티티를 반환한다.

위의 흐름을 통해 알 수 있듯이, 만약 1차 캐시에 1L번의 member 객체가 존재하지 않아 데이터베이스에서 엔티티를 찾아왔다면, 이후 1번 member 객체를 다시 찾을때는 DB가 아닌 1차 캐시에서 찾아오는 것을 알 수가 있다.
물론 데이터베이스에 SELECT 쿼리도 전송되지 않는다.

동일성(identity) 보장

Member a = em.find(Member.class, "101L");
Member b = em.find(Member.class, "101L");

System.out.print(a == b)     // true

먼저 위와 같이 조회를 했을 때, 아래와 같이 한번의 쿼리가 전송된다.

위에서 알아봤듯이, 1차캐시에서 조회했지만 존재하지 않은 101번 객체는, 데이터베이스에서 조회하게 되며 1차 캐시에 저장되었다. 이로 인해서 a객체를 조회할 때 1번의 조회 쿼리만 동작한 것이다.

이와 같이 1차 캐시가 있기 때문에 마치 자바 컬렉션에서 꺼낸 것과 같이 동일성 보장이 되며, a와 b의 비교 결과가 true로 나온 것이다.

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

쓰기 지연이란 생성한 쿼리를 SQL 저장소에 모아두었다가 트랜잭션 commit() 시점에 플러시가 되면서 데이터베이스에 sql을 전송하는 것을 말한다.

쓰기 지연 흐름

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();

transaction.begin(); // [트랜잭션] 시작

em.persist(memberA);
em.persist(memberB);

transaction.commit();

영속성 컨텍스트 안에는 1차 캐시쓰기 지연 SQL 저장소가 존재한다.
객체를 persist( )하면, 1차 캐시에 들어가는 동시에, JPA는 이 엔티티를 분석하면서 쿼리를 생성한다.
이렇게 생성한 쿼리는 쓰기 지연 SQL 저장소에 쌓아둔다.

코드를 보자면, em.persist(memberA);로 memberA 객체가 1차 캐시에 들어갔다. 이와 동시에 JPA는 memberA 객체를 분석해 INSERT 쿼리를 작성한다. 작성한 쿼리는 쓰기 지연 SQL 저장소에 둔다.
em.persist(memberB); 다음 memberB 객체가 persist( )되면서 1차 캐시와 쓰기 지연 SQL 저장소에 쿼리가 쌓이게 된다.

이후 transaction.commit();이 되면서 쓰기 지연 SQL 저장소에 있던 쿼리가 모두 데이터베이스에 반영된다.

추가적으로 hibernate.batch_size라는 것을 이용하면 사이즈만큼 모아서 한번에 네트워크로 전송할 수도 있다.

4. 변경 감지 (Dirty checking)

변경 감지라는 것은 JPA에서 엔티티를 수정할 때의 변경 방식을 말한다.

Member member = em.find(Member.class, 150L);
member.setName("zzz");

System.out.println("==================");

// 트랜잭션 commit
tx.commit();

150번 Member 객체를 찾아서 zzz로 name을 수정해주었다. 단순히 set()으로 값을 수정하고 persist()나 다른 무언가를 해주지 않아도 데이터베이스에 update 쿼리가 전송되는 것을 볼 수 있다.

1차 캐시에는 이전에 설명한 pk, 엔티티와 스냅샷이 있다.

스냅샷 - 최초 영속성 컨텍스트에 들어온 1차 캐시 상태

이 스냅샷은 값을 읽어온 최초시점의 상태를 떠두는 것이다.
JPA는 데이터베이스 트랜잭션을 커밋하는 시점에 내부적으로 플러시가 호출 되는데, 이 과정에서 엔티티와 스냅샷을 비교한다.
변경된 값이 존재하면 업데이트 쿼리를 쓰기 지연 SQL 저장소에 만들어 두고 플러시 한 뒤, 데이터베이스 트랜잭션을 커밋한다.

이러한 과정을 통해서 persist( ) 할 필요 없이 커밋 시점에 엔티티가 관리되는 것이다.

플러시 (Flush)

플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것을 말한다. 보통 데이터베이스 트랜잭션이 커밋될 때 이 플러시라는 것이 일어난다.
앞에서 보았듯이 쌓아둔 SQL 쿼리들을 플러시를 통해서 영속성 컨텍스트의 현재 변경사항과 데이터베이스를 맞춰주는 것이다.

플러시 흐름

  1. 데이터베이스 트랜잭션 커밋이 발생하면 플러시는 자동으로 일어난다.
  2. 변경 감지 (Dirty checking)
  3. 수정된 엔티티를 쓰기지연 SQL 저장소에 등록
  4. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)

데이터베이스에 쿼리 전송 -> 데이터베이스 트랜잭션 커밋 순서로 동작한다.

영속성 컨텍스트를 플러시하는 방법

  1. em.flush - 직접 호출
  2. 트랜잭션 커밋 - 플러시 자동 호출
  3. JPQL 쿼리 실행 - 츨러시 자동 호출

코드로 알아보기

Member member = new Member(200L, "member200");
em.persist(member);

em.flush();

System.out.println("==================");

tx.commit();

위의 코드의 결과로는 아래와 같은 결과가 나온다.

원래대로라면 commit하는 시점에 쿼리가 전송되기 때문에 "=========" 선 아래에 INSERT 쿼리가 작성되어야 하지만, 그 전에 flush를 강제 호출 했기때문에 INSERT 쿼리가 즉시 전송된 것이다.

플러시 정리

  1. 플러시를 한다고 해서 1차 캐시가 지워지지는 않는다. 즉, 영속성 컨텍스트를 비우는 것이 아니다.
  2. 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화시키는 것이다.
  3. 트랜잭션이라는 작업 단위가 중요 -> 커밋 직전에 동기화

이렇게 JPA의 영속성 컨텍스트에 대해서 알아보았다. 영속성 컨텍스트는 JPA를 이해하는데 가장 중요한 용어이다. 눈에 보이지 않고 논리적인 개념으로 어렵게 느껴졌던 영속성 컨텍스트를 이렇게 정리함으로 잘 이해할 수 있었다.

0개의 댓글