[JPA] 영속성 컨텍스트

2024. 3. 22. 16:37카테고리 없음

영속성 컨텍스트란?

ORM은 객체와 데이터베이스 테이블의 매핑을 통해 엔티티 클래스 객체 안에 포함된 정보를 테이블에 저장하는 기술이다.

JPA에서는 테이블과 매핑되는 엔티티 객체 정보를 영속성 컨텍스트를 통해 애플리케이션 내에서 오래 지속되도록 보관한다.

 

영속성 컨텍스트는 JPA를 이해하는데 가장 중요한 용어이다.

  • 영속성 컨텍스트는 논리적인 개념
  • 눈에 보이지 않음
  • 엔티티 매니저를 통해 영속성 컨텍스트에 접근

 

엔티티의 생명주기

 

 

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

 

 

비영속

객체를 생성한 상태

Member member = new Member();
member.setId("member1");
member.setUsername("홍길동");

 

영속

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

Member member = new Member();
member.setId("member1");
member.setUsername("홍길동");

// 객체를 영속성 컨텍스트에 저장(영속)
em.persist(member);

 

준영속

// member 엔티티를 영속성 컨텍스트에서 분리(준영속)
em.detach(member);

 

삭제

// 객체를 삭제한 상태(삭제)
em.remove(member);

 

영속성 상태의 장점

  • 1차 캐시
  • 동일성(identity) 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지
  • 지연 로딩

 

영속성 컨텍스트(Persistence Context)를 그림으로 표현하면 다음과 같이 나타낼 수 있다.

 

 

영속성 컨텍스트에는 1차 캐시 영역과 쓰기 지연 SQL 저장소 영역이 있다.

JPA API 중에서 엔티티 정보를 영속성 컨텍스트에 저장하는 API를 사용하면, 영속성 컨텍스트의 1차 캐시에 엔티티 정보가 저장된다.

// 엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("홍길동");

// 객체를 영속성 컨텍스트에 저장(영속)
em.persist(member);

 

영속성 컨텍스트의 조회 - find()

영속성 컨텍스트에 저장된 데이터나 데이터베이스에 저장된 데이터를 조회하기 위해서는 EntityManager의 find() 메서드를 통해 조회할 수 있다.

 

1차 캐시에서 조회하는 경우

Member member = new Member();
member.setId("member1");
member.setUsername("홍길동");

em.persist(member);

Member findMember = em.find(Member.class, "member1");
  • 객체를 생성한 후, em.persist(member)를 통해 1차 캐시에 저장한다.
  • 1차 캐시에 저장된 상태에서 find() 메서드를 통해 조회할 경우 1차 캐시에 저장된 데이터를 그대로 조회한다.

 

데이터베이스에서 조회하는 경우

Member findMember2 = em.find(Member.class, "member2");
  • find() 메서드는 먼저 영속성 컨텍스트의 1차 캐시에 조회할 엔티티가 존재하는지 먼저 탐색한다.
  • 만약, 1차 캐시에 존재하지 않을 경우 데이터베이스에서 조회한 뒤, 1차 캐시에 저장한다.
  • 이후 조회 데이터를 반환한다.

 

같은 데이터를 2번 조회할 경우

Member findMember1 = em.find(Member.class, "member1");
Member findMember2 = em.find(Member.class, "member1");

System.out.println(findMember1 == findMember2);
  • 먼저, member1을 찾기 위해 1차 캐시를 찾는다.
  • 1차 캐시에 존재하지 않아 데이터베이스에서 조회한다.
  • 이후 1차 캐시에 저장한 뒤 member1을 반환한다.
  • findMember2가 실행될 때 1차 캐시에서 데이터를 조회한다.
  • findMember1에 의해 1차 캐시에 저장된 member1을 반환한다.
  • 즉, 같은 데이터를 2번 조회할 경우 조회 쿼리는 1회만 실행된다.

 

또한, findMember1과 findMember2를 비교하면, true로 같은 데이터임을 나타낸다.

이는 영속 엔티티의 동일성을 보장한다는 뜻이다.

// 출력
true

 

엔티티 등록 - 쓰기 지연

엔티티 매니저는 데이터 변경 시 반드시 트랜잭션을 시작해야 한다.

EntityManager em = EntityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 트랜잭션

// 트랜잭션 시작
tx.begin();

// 비영속
Member member = new Member();
member.setId("member1");
member.setUsername("홍길동");

// 영속
em.persist(member);

// 엔티티 등록
tx.commit();
  • em.persist(member); : member 엔티티를 영속 컨텍스트에 저장하지만, 데이터베이스에는 반영되지 않는다.
  • tx.commit(); : 트랜잭션을 커밋하는 순간 데이터베이스에 INSERT SQL을 보내 저장하게 된다.
  • persist()를 실행할 때, 영속 컨텍스트의 1차 캐시에는 member 엔티티가 저장되고, 쓰기 지연 SQL 저장소에는 member 엔티티의 INSERT SQL 쿼리문이 저장된다.
  • txcommit()을 실행하는 순간 쓰기 지연 SQL 저장소에 저장된 INSERT SQL 쿼리를 보내 데이터베이스에 저장하는 것이다.

 

따라서, 여러 개의 엔티티를 생성하고 persist를 하더라도, commit()을 하기 전에는 데이터베이스에 저장되지 않는다. 이를 쓰기 지연이라 하며, 영속 컨텍스트의 장점이다.

 

엔티티 수정 - 변경 감지

EntityManager em = EntityManagerFactory.createEntityManager();
EntityTransaction tx = em.getTransaction(); // 트랜잭션

// 트랜잭션 시작
tx.begin();

// member 조회
Member member = em.find(Member.class, "member");
member.setUsername("hello");
member.setAge("20");

// 엔티티 등록
tx.commit();
  • 엔티티의 수정은 set메서드를 통해서 변경한 뒤, 별다른 로직 없이 트랜잭션 커밋을 하는 순간에 업데이트된다.
  • 이것이 가능한 이유는 바로 변경 감지(Dirty Checking) 기능을 제공하기 때문이다.
  • 영속 컨텍스트의 1차 캐시에는 member의 초기 데이터가 저장되어 있을 것이다.
  • 이후 set 메서드를 통해 데이터를 변경한다.
  • 트랜잭션 커밋 시 flush()가 발생하면서 1차 캐시에서 엔티티와 스냅샷을 비교하여 변경에 대한 감지를 한다.
  • 이후 SQL UPDATE 쿼리를 생성하여 쓰기 지연 SQL 저장소에서 쿼리를 보낸다.
  • 이로써 DB에 저장된 데이터를 수정하게 된다.

 

엔티티 삭제

Member member = em.find(Member.class, "member");

em.remove(member); // 엔티티 삭제
  • 엔티티 삭제는 remove() 메서드를 통해 데이터를 삭제할 수 있다.
  • 영속성 컨텍스트와 데이터베이스에서 모두 제거된다.

 

플러시 - flush()

트랜잭션 커밋을 실행하면 변경 내용을 데이터베이스에 반영하게 된다.

트랜잭션 커밋이 일어날 때 플러시도 함께 발생하여 데이터베이스에 반영할 수 있는 것이다.

즉, 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것이다.

 

플러시 발생 시

  • 변경 감지(dirty checking)
  • 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
  • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)

 

 

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

  • em.flush() - 직접 호출(테스트에 사용)
  • tx.commit() - 트랜잭션 커밋을 통한 자동 호출
  • JPQL 쿼리 실행 - 플러시 자동 호출

 

 

직접 호출 예시

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

em.flush(); // 강제 호출
System.out.println("------------");

tx.commit();
  • flsuh()는 변경을 감지하여 데이터베이스에 반영하는 역할을 한다.
  • 따라서 이후에 commit()이 발생해도 쿼리를 다시 실행하지는 않는다.
  • 또한, flush()를 한다고 해서 1차 캐시의 내용이 사라지지 않는다.

 

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();

위 코드와 같이 persist()를 실행한 뒤, JPQL로 쿼리를 보내면 members에는 데이터베이스로부터 결과를 얻을 수 없을 것이다.

쿼리 발생 이전에 데이터베이스에 반영하는 flush()가 호출되어야 하기 때문이다.

직접 플러시를 호출하거나, 쿼리 이전에 commit()을 해야 한다.

이러한 문제점을 방지하기 위해 중간에 JPQL이 실행하게 되면 자동으로 플러시를 호출하여 JPQL 쿼리를 반영할 수 있도록 하는 것이다.

 

 

플러시 모드 옵션

  • FlushModeType.AUTO : (기본 값) 커밋이나 쿼리를 실행할 때 플러시
  • FlushModeType.COMMIT : 커밋할 때만 플러시
em.setFlushMode(FlushModeType.COMMIT)

커밋이나 JPQL 쿼리 등을 실행할 때 자동으로 플러시 되는 것을 방지하기 위해 플러시 모드를 설정할 수 있다.

변경된 것과는 아무런 관련이 없는 데이터베이스 테이블에 쿼리를 보내고자 할 때 종종 사용된다.

 

플러시에 대한 오해

  • 플러시는 영속성 컨텍스트를 비우지 않는다.
  • 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 역할이다.
  • 플러시의 개념은 트랜잭션이라는 작업 단위에 중요 → 커밋 직전에만 동기화하면 된다.

 

준영속 상태

em.persist() 또는 em.find()를 실행하면 해당 데이터는 영속 컨텍스트에 저장되어 영속 상태가 된다.

준영속 상태는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)된 상태를 말한다.

준영속 상태에서는 영속성 컨텍스트가 제공하는 기능을 사용하지 못하게 된다.

준영속 상태로 만드는 방법

  • em.detach(entity) : 특정 엔티티만 준영속 상태로 변환
  • em.clear() : 영속성 컨텍스트를 완전히 초기화
  • em.close() : 영속성 컨텍스트를 종료