[스프링부트] 영속성 컨텍스트

YTT.erica·2024년 6월 20일

영속성 컨텍스트

영속성 컨텍스트란, JPA의 생명주기를 관리하는 저장소

  • 애플리케이션이 데이터베이스와 상호작용할 때 사용되는 임시 데이터 저장 공간 -> 가상의 DB 역할
  • EntityManagerFactory, EntityManager, EntityTransaction으로 영속성 컨텍스트에 접근, 저장/조회, 관리

영속성 컨텍스트 장점

  • 1차 캐시
  • 동일성 보장
  • 쓰기 지연
  • 변경 감지
  • 지연 로딩
  • 더티 체킹

1차 캐시

엔티티 매니저가 관리하는 영속성 컨텍스트 내부의 첫 번째 캐시

영속성 컨텍스트 단위

특정 연속성 컨텍스트 내에서만 유효
-> 엔티티 매니저 단위로 관리 -> 동일한 영속성 컨택스트 내에서만 유효
-> 동알한 영속성 컨텍스트 내에 조화된 엔티티는 모두 1차 저장소에 복귀

엔티티 조회 최적화

엔티티를 조회할 때, 영속성 컨텍스트는 먼저 1차 캐시 확인

  • 캐시에 엔티티 존재 -> 데이터베이스 조회 없이 캐시된 엔티티 반환
  • 데이터베이스와의 불필요한 통신 하락

엔티티 식별자 기반 저장

엔티티의 식별자(Primary Key)를 기준으로 엔티티 인스턴스 저장

  • 식별자를 통해 엔티티 빠르게 조회 가능

변경 사항 추적

엔티티의 상태 변화를 추적

  • 엔티티가 수정되면, 그 변경 사항은 1차 캐시에 반영되고, 트랜잭션이 커밋될 때, 변경된 엔티티는 데이터베이스에 반영

엔티티 생명주기

엔티티 매니저를 통해 생성되거나 조회된 엔티티는는 컨텍스트가 살아있는 동안 1차 캐시에 유지
엔티티 매니저가 종료되면 해당 영속성 컨텍스트와 1차 캐시가 사라짐

1차 캐시의 동작 예시 1

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

// 데이터베이스에서 엔티티를 조회합니다.
Member member = em.find(Member.class, 1L); // 첫 번째 조회, 데이터베이스에서 조회하여 1차 캐시에 저장

// 같은 엔티티를 다시 조회합니다.
Member sameMember = em.find(Member.class, 1L); // 두 번째 조회, 1차 캐시에서 조회

// 두 조회 결과가 동일한 인스턴스인지 확인합니다.
System.out.println(member == sameMember); // true

em.getTransaction().commit();
em.close();
  • em.find(Member.class, 1L) 호출이 두 번 이루어지지만,
  • 두 번째 호출은 1차 캐시에서 엔티티를 조회하므로 데이터베이스에 접근 X
  • 이렇게 1차 캐시는 성능 최적화와 효율적인 엔티티 관리를 가능하게 합니다.

1차 캐시의 동작 예시 2

1차 캐시 저장

// 엔티티를 생성한 상태 (비영속)   
Member member = new Member();   
member.setId("member1");   
member.setName("회원1");  // 엔티티를 영속 -> 1차 캐시에 저장됨.   
entityManager.persist(member);

1차 캐시 조회

// 1차 캐시에서 조회   
Member member = entityManager.find(Member.class, "member1");

만약 1차 캐시에 존재하지 않는 엔티티를 찾고자 하는 경우에는?

  • 1차 캐시를 확인 -> 1차 캐시에 없으므로 DB 조회
  • DB에서 조회 후, 해당 결과를 1차 캐시에 저장

동일성 보장

반복 가능한 읽기 등급의 트래잭션 격리

  • 데이터베이스가 아닌 애플리케이션 차원에서 제공
Member member1 = entityManager.find(Member.class, "member1");   
Member member2 = entityManager.find(Member.class, "member1");  System.out.println(member1 == member2); // 동일성 비교 결과 : true

동일성 보장 작동 원리

엔티티 조회 시

영속성 컨텍스트는 엔티티를 조회할 때 먼저 1차 캐시를 확인

  • 만약 조회하려는 엔티티가 1차 캐시에 존재하면, 데이터베이스를 조회하지 않고 1차 캐시에서 해당 엔티티 인스턴스를 반환
  • 이 과정에서 동일한 식별자를 가진 엔티티는 항상 동일한 인스턴스가 됨

엔티티 저장 시

영속성 컨텍스트는 해당 엔티티를 1차 캐시에 저장

  • 만약 동일한 식별자를 가진 다른 엔티티가 이미 존재한다면, 새로운 엔티티를 저장하지 않고 기존 인스턴스를 사용합니다.

엔티티 병합 시

영속성 컨텍스트는 주어진 엔티티의 식별자 확인

  • 1차 캐시에 존재하는 동일한 식별자의 엔티티를 반환하거나 새롭게 병합된 엔티티를 1차 캐시에 저장

동일성 보장 예시

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

// 엔티티를 데이터베이스에서 조회하여 1차 캐시에 저장
Member member1 = em.find(Member.class, 1L);

// 동일한 식별자를 가진 엔티티를 다시 조회
Member member2 = em.find(Member.class, 1L);

// 두 엔티티의 동일성 확인
System.out.println(member1 == member2); // true, 동일한 인스턴스

// 엔티티의 상태를 변경
member1.setName("Updated Name");

// 동일한 엔티티를 다시 조회하여 상태 확인
Member member3 = em.find(Member.class, 1L);
System.out.println(member3.getName()); // "Updated Name", 변경된 상태 반영

em.getTransaction().commit();
em.close();

member1, member2 는 동일한 식별자를 가짐 -> 동일한 인스턴스 반환
member1 상태를 변경하면 member3 으로 다시 조회 -> 동일한 인스턴스 반환 -> 변경 상태 반영


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

JPA에서 엔티티의 변경 사항을 즉시 데이터베이스에 반영하지 않고, 트랜잭션이 커밋될 때 한꺼번에 반영
-> 데이터베이스 접그 횟수 줄이고, 성능 최적화 가능
-> 변경 사항은 1차 캐시에 저장 -> 트랜잭션 커밋 시점에 데이터베이스에 반영

쓰기 지연의 작동 원리

영속성 컨텍스트에 엔티티 추가

엔티티를 생성하고 영속성 컨텍스트에 추가 -> 데이터베이스에 실제로 저장 X

변경 사항 1차 캐시에 반영

엔티티 상태가 변경되면, 변경 사항을 1차 캐시에 반영 -> 데이터베이스에는 반영 X

플러시

트랜잭션이 커밋되거나 flush 메서드가 호출되면 영속성 컨텍스트는 변경 사항을 데이터베이스 반영
-> 모든 엔티티에 대한 SQL 문이 실행되어 생성

트랜잭션 커밋

플러시된 변경 사항이 확정되고 데이터베이스에 반영

쓰기 지연의 예시

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

// 새로운 엔티티를 생성하고 영속성 컨텍스트에 추가
Member member = new Member();
member.setName("John Doe");
em.persist(member);

// 엔티티의 상태를 변경
member.setName("Jane Doe");

// 아직 데이터베이스에 반영되지 않음
// 명시적으로 플러시를 호출하지 않으면, 트랜잭션 커밋 시점에 변경 사항이 반영됨

em.getTransaction().commit();
em.close();

em.persist(member) 호츨 -> 새로운 Member 엔티티가 영속성 컨텍스트에 추가 (데이터베이스에 저장 x)
엔티티의 이름을 변경해도 데이터베이스에 반영 X -> 1차 캐시에 저장
트랜잭션이 커밋되는 시점에서 Member엔티티의 생성과 변경 사항이 한꺼번에 데이터베이스에 반영


변경 감지 (Dirty Checking)

영속성 컨텍스트에 의해 관리되는 엔티티의 상태 변화를 자동으로 감지 -> 트랜잭션 커밋 시점에 해당 변경 사항을 데이터베이스에 반영

변경 감지의 작동 원리

엔티티의 원본 상태 저장

엔티티의 영속 컨텍스트에 처음 추가 -> 해당 엔티티의 원본 상태(snapshot) 저장

엔티티 변경 추적

엔티티의 필드 값이 변경 -> JPA 엔티티의 현재 상태를 원본 상태와 비교해 어떤 필드가 변경됐는지 추적

트랜잭션 커밋 또는 플러시 시점

트랜잭션이 커밋되거나 flush 메서드가 호출 -> JPA는 변경된 엔티티를 찾아 데이터베이스에 업데이트 쿼리를 생성하고 실행

변경 감지의 예시

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

// 엔티티를 데이터베이스에서 조회
Member member = em.find(Member.class, 1L);

// 엔티티의 필드 값을 변경
member.setName("Updated Name");

// 트랜잭션 커밋 시점에 변경 감지가 이루어짐
em.getTransaction().commit();
em.close();

member.setName() 메서드 호출 -> member 엔티티의 이름 필드 변경
트랜잭션이 커밋될 때 member 엔티티의 원본 상태와 현재 상태를 비교하여 변경 감지
JPA는 UPDATE 쿼리를 생성하여 변경된 이름을 데이터베이스에 반영


지연 로딩

JPA에서 엔티티와 관련된 데이터를 실제로 필요할 때까지 로딩하지 않음
-> 불필요한 데이터를 가져오는 것 방지, 성능 최적화
-> 엔티티 간의 연관 관계를 처리할 때 사용

지연 로딩의 작동 원리

엔티티 로딩 지연

엔티티의 연관된 데이터를 접근할 때까지(사용할 때까지) 로딩 지연

연관 관계 설정

@OnetoMany, @ManyToOne, @OneToOne, @ManyToMany 등의 연관 관계 설정

지연 로딩 설정

@ManyToOne (fetch = FetchType.LAZY) 처럼 설정을 하여 지연 로딩 지정

지연 로딩의 예시

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
    private List<Order> orders;
    
    // getters and setters
}

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String product;
    
    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
    
    // getters and setters
}
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();

// Member 엔티티를 조회
Member member = em.find(Member.class, 1L);

// 지연 로딩 설정으로 인해 orders 컬렉션은 아직 로딩되지 않음
System.out.println("Member name: " + member.getName());

// orders 컬렉션에 접근하는 시점에서 데이터베이스 조회가 발생
List<Order> orders = member.getOrders();
for (Order order : orders) {
    System.out.println("Order product: " + order.getProduct());
}

em.getTransaction().commit();
em.close();

member.getOrders()를 호출할 때서야 orders 컬렉션이 데이터베이스에서 로딩


출처 및 참고자료

인프런 '자바 ORM 표준 JPA 프로그래밍 - 기본편' 강의

profile
'◡'✿ 꿈을 찾아가보자고~ '◡'✿

0개의 댓글