[JPA] 영속성 컨텍스트의 전반적인 이해(개념, 장점, 동작 방식)

go_go_·2023년 2월 25일
2

JPA

목록 보기
2/4
post-thumbnail

🔍 목차

  1. 영속성 컨텍스트란?
  2. 엔티티의 생명주기
  3. 엔티티를 영속성 컨텍스트에 보관하여 얻는 장점


📌 영속성 컨텍스트란?

JPA에서 가장 중요한 개념
1. 영속성 컨텍스트 - 실제 JPA 동작과 관련
2. 객체와 관계형 데이터 베이스 매핑(ORM) - 설계과 관련

👀 영속성 컨텍스트(PersistenceContext)

Entity를 영구 저장하는 환경이라는 뜻

  • 눈에 보이지 않는 논리적 개념
  • 애플리케이션과 DB 사이 객체를 보관하는 가상의 DB로 생각하기
  • EntityManager를 통해 Entity를 영속성 컨텍스트에 보관, 관리

👀 EntityManager

EntityManager를 통해 영속성 컨텍스트에 접근하고 관리

  • EntityManager를 생성하면 그 안에 영속성 컨텍스트 있음

    • EntityManagerFactory통해 요청이 올 때 마다 EntityManager 생성
    • EntityManager는 내부적으로 Connection 사용하여 DB 접근
  • entity 저장: EntityManager.persist(entity)

    • 데이터를 영속성 컨텍스트에 저장
    • 엔티티를 영속화한다는 의미
    • DB에 저장하는 것 아님!
  • spring에서 EntityManager 여러 개, 영속성 컨텍스트 1개 존재

    • 엔티티 매니저:영속성 컨텍스트 = N : 1


📌 엔티티의 생명주기

entity는 EntityManager를 통해 영속성 컨텍스트에 보관, 관리, 제거된다. 그에 따른 엔티티에 생명주기를 알아보자.

👀 EntityManager 생성

  • EntityManagerFactory
    • 설정 파일에서(ex. Persistence.xml) 작성한 psersistence-unit(DB 설정 정보)으로 생성
  • EntityManager
    • 위에서 생성한 EntityManagerFactory에서 생성 가능
// hello는 psersistence-unit 이름
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

// 이를 통해 엔티티를 영속성 컨텍스트에 관리 가능
EntityManager em = emf.createEntityManager();

👀 엔티티의 생명주기

비영속(new)

Member member = new Member(); // 엔티티(객체) 생성
  • 객체 생성
  • 엔티티가 영속성 컨텍스트에 없는 상태 (= 영속성 컨텍스트와 관계 x)

영속(managed)

em.persist(member);
  • 엔티티가 영속성 컨텍스트에 저장
  • 즉, 영속성 컨텍스트가 엔티티를 관리

준영속(detached)

em.detach(member);
  • 엔티티가 영속성 컨텍스트에 저장되었다가 분리된 상태
    • 엔티티 영속 상태에서 준영속 상태로 간 상태!
  • 영속성 컨텍스트가 엔티티 관리하지 않음
    • 영속성 컨텍스트가 제공하는 기능 사용하지 못함
  • 준영속 상태로 만드는 법
    • em.detach(entity);
      • 특정 엔티티만 준영속 상태로 변환
    • em.clear()
      • 영속성 컨텍스트 초기화(다 지움)
    • em.close()
      • 영속성 컨텍스트 종료

삭제(removed)

em.remove(member);
  • 영속성 컨텍스트에 있는 엔티티를 삭제
  • 이때 DB에서도 삭제
    • 주의: 트랜잭션이 끝나거나 EntityManger를 플러시 하여 삭제 쿼리가 나가야 DB에서 삭제됨


📌 엔티티를 영속성 컨텍스트에 보관하여 얻는 장점

영속성 컨텍스트의 장점은 다음과 같다. 하나씩 알아보자.

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

✏️ 1차 캐시

  • 영속성 컨텍스트에 1차 캐시 존재
  • 캐시는 map 형태로 key-value 저장
    • key: DB의 PK(기본 키)
    • value: 객체

1차 캐시 동작 예시

  • member1이 PK가 member1인 엔티티 생성 후 영속화
  • 이 다음 PK가 member1인 엔티티 조회하면?
    • em.find(Member.class, "member1");
    • JPA는 영속성 컨텍스트에 1차 캐시 확인
    • key = member1 있으면 캐시에서 값을 가져옴

만약 1차 캐시에 찾는 엔티티가 없으면?

  • em.find(Member.class, "member2");
  • JPA는 영속성 컨텍스트에 1차 캐시 확인
  • 1차 캐시에 없다면 DB 조회
  • DB에 데이터 있으면 가져와 1차 캐시에 저장 후 데이터 반환

사실 1차 캐시는 큰 도움이 되지 않는다.
이유1. 1차 캐시가 있는 EntityManager는 트랜잭션 단위로 만들고 사라진다. 즉, 1차 캐시가 살아있는 시간은 매우 짧아 성능에 큰 효과는 없다.
이유2. 트랜잭션마다 각자 EntityManger를 사용한다. 즉, 각자 다른 영속성 컨텍스트와 1차 캐시를 가진다.


✏️ 동일성 보장

  • 같은 트랜잭션 안에서 같은 객체는 == 비교 보장

예시

// 같은 PK 값 데이터 찾음
// 같은 데이터이지만 저장된 객체는 member1과 member2로 다름
Member member1 = em.find(Member.class, 1L);
Member member2 = em.find(Member.class, 1L);

// 이 때 == 비교를 하면?
System.out.println("result: "+(member1 == member2));

// 결과
result: true

✏️ 트랜잭션을 지원하는 쓰기 지연

  • 한 트랜잭션 안에서 DB에 보낼 쿼리문을 모았다가 한번에 보냄
    • 쿼리문 모아두는 곳: 쓰기 지연 SQL 저장소 (in 영속성 컨텍스트)
    • 쿼리문 보내는 시점
      1. 트랜잭션 끝나기 전 - transcation.commit()
      2. EntityManger 플러시 - em.flush()
  • 네트워크 부하를 줄이기 위한 기능
  • em.persist(entity)를 하면?
    • 영속성 컨텍스트 1차 캐시에 저장
    • JPA가 엔티티 분석하여 INSERT 쿼리 생성하여 쓰기 지연 SQL 저장소에 쌓아둠

예시
쓰기 지연 SQL 저장소에 쿼리문 쌓는 과정

  • em.persist(memberA);
    • memberA 객체 생성 후 영속화
    • 1차 캐시에 저장 및 INSERT 쿼리 쌓아둠
  • em.persist(memberB)
    • memberB 객체 생성 후 영속화
    • 1차 캐시에 저장 및 INSERT 쿼리 쌓아둠

트랜잭션 커밋이 일어나면?

  • transcation.commit() - 트랜잭션 커밋
  • 커밋 시점에 EntityManager에서 flush() 호출
    • flush: 영속성 컨텍스트의 내용을 DB에 반영, 모아두었던 SQL이 DB에 날라감
  • 실제 데이터 저장

버퍼링 기능

  • 모았다가 쿼리 한 번에 보낼 수 있음
  • Hibernate의 경우 옵션을 부여 가능
    • hibernate.jdbc.batch.size
    • 지정한 사이즈만큼 모아서 DB에 한 번에 쿼리 보내고 commit 하는 기능
    • ex. 10개 쿼리 모았다가 DB에 반영
      <!-- pom.xml에 추가-->
      <property name="hibernate.jdbc.batch.size" value="10" />

✏️ 변경 감지(Dirty checking)

  • 영속성 컨텍스트에서 보관하는 데이터에 변경이 일어났는지 확인
  • 데이터 변경이 일어났다면?
    • JPA가 UPDATE 쿼리문 날려줌
    • 즉, 값 변경 후 update 쿼리문 날릴 코드가 필요 없음

작동 순서
트랜잭션 커밋이 발생하면

  1. flush() 호출

  2. 1차 캐시에서 entity와 스냅샷 비교

    • 스냅샷: 최초로 영속성 컨텍스트에 들어온 객체 상태
    • entity: 실제 값
  3. entity와 스냅샷이 다르다면 쓰기 지연 SQL 저장소에 update 쿼리문 추가

  4. DB에 update 쿼리문 반영 후 commit


예시

Member member = em.find(Member.class, 1L); // name: 홍길동, age = 20
member.setAge(21); // 데이터 변경

트랜잭션 커밋 시점 로그

Hibernate: 
    /* update
        jpabasic.hellojpa.Member */ 
        update
            Member 
        set
            name=? 
        where
            id=?

추가
엔티티 삭제는 `em.remove(entity)로 이뤄진다. 위 예시로 든 update와 동일한 방법으로 commit 시점에 delete문이 DB로 날라가 적용된다.


✏️ 지연 로딩

  • 엔티티를 DB에서 가져올 때 만약 연관 관계를 가진 다른 객체를 가지고 있다면?
    • 즉시 로딩: 엔티티 조회할 때 연관된 객체도 함께 DB에서 조회
    • 지연 로딩: 연관된 객체는 조회x, 나중에 필요하면 그 때 DB에서 가져옴
  • 각 연관 관계마다 즉시 로딩, 지연 로딩 설정 가능

출처 및 참고자료
인프런 '자바 ORM 표준 JPA 프로그래밍 - 기본편' 강의

profile
개발도 하고 싶은 클라우드 엔지니어

0개의 댓글