[Spring/Springboot] 영속성(Persistence) - 1차 캐시/변경 감지(Dirty Checking)/지연 로딩(Lazy Loading)

2024. 12. 15. 23:07CS/Spring

 

 

public static void main(String[] args){
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf. createEntityManager();
EntityTransactiono tx = em.getTransaction();
tx.begin();

try{
	//비영속
	Member member = new Member();
	member.setId(100L);
	member.setName("HelloJPA");
	
	//영속
	em.persist(member);
		//영속 상태가 되었다고 DB에 바로 쿼리가 날라가는 것이 아님
		//transaction을 커밋하는 시점에 영속성 컨텍스트에 있는게 DB에 쿼리가 날라가게 됨
	
	//준영속
	em.detach(member);//회원 엔티티를 영속성 컨텍스트에서 분리
	
	//삭제
	em.remove(member); //객체를 삭제한 상태
	
	tx.commit();
}catch(Exception e){
	tx.rollback();
}

application 이랑 DB 사이에 중간계층이 하나 있는 것 ⇒ 영속성 컨텍스트의 이점

1. 1차 캐시

  • 1차 캐시 : DB 한 Transaction 안에서만 영향있음
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

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

//1차 캐시에서 조회 가능
Member findMember = em.find(Member.class, "member1");
	//**DB까지 가지 않고, 1차 캐시 안에 있는 값을 그냥 조회 가능**

Member findMember = em.find(Memeber.class, "member2");
	**//DB에 없는 member2경우 DB에서 조회하고, 영속성 컨텍스트안 1차 캐시에 저장하고, 반환함
	//이후에 조회하면 1차 캐시에서 조회**

2. 동일성(identity)보장

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

System.out.println(a==b);//동일성 비교 true

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

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

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

em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 DB에 보내지 않음
	//1차 캐시에 저장됨과 동시에 JPA가 entity를 분석해서 insert SQL을 생성하고
	//쓰기지연 SQL 저장소에 쌓아둠


transaction.commit(); //트랙잭션 커밋
//COMMIT 하는 순간 DB예 INSERT SQL을 보냄
	//쓰기 지연 SQL 저장소에 있던 애들이 DB로 날라가면서 커밋됨
  • “hibernate.jdbc.batch_size" value=에 설정한 size만큼 한방에 모아서 쿼리를 날림

4. 변경 감지(Dirty Checking)

EntityManager em = emf.createEntityManager();
EntityTransaction transaction  = em.getTransaction();
transaction.begin(); // 트랜잭션 시작

//영속 엔티티 조회
Member memberA = em.find(Member.class, "memberA");

//영속 엔티티 데이터 수정
memberA.setUsername("hi");
memberA.setAge(10);

//em.update(member) 이런 코드가 있어야 하지 않을까?
	// 자바 컬렉션 처럼 객체를 다루기 때문에 필요 없음
	
transaction.commit(); // 트랜잭션 커밋
  • JPA는 commit을 하면 flush()를 함
    • flush() → 엔티티와 스냅샷 비교 “어! memberA 바뀌었네!” → UPDATE SQL 쿼리를 (쓰기 지연 저장소에) 생성 → flush(DB에 반영) → DB에 commit
    • 스냅샷 : 내가 값을 딱 읽어온(영속성 컨텍스트에 들어온) 최초 시점의 상태를 딱 떠두는 것
  • 엔티티 삭제
Member memberA = em.find(Member.class, "memberA);
em.remove(memberA);//엔티티 삭제

 

  • flush
    • 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영
      • JPA는 값을 바꾸면 transaction이 commit 되는 시점에 데이터베이스에 반영한다!
    • 플러시 발생하면 무슨일이 발생하는가(DB에 transaction이 commit 되면, flush가 자동으로 발생)
      1. 변경 감지
      2. 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
      3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)
    • 영속성 컨텍스트를 플러시 하는 방법
      1. em.flush() - 직접 호출
        • “쿼리를 DB에 미리 반영하고 싶어~ 쿼리를 미리 좀 보고 싶어~”
        • 1차 캐시 역시 원래대로 생성됨 다만, 쓰기 지연 SQL 저장소의 쿼리가 DB에 반영되는 것임
      2. transaction commit - 플러시 자동 호출
      3. JPQL query 실행 - 플러시 자동 호출
        • 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();
//JPQL은 무조건 쿼리가 날라가기 때문에 위처럼 조회해도 조회가 됨

 

Flush
영속성 컨텍스트를 비우지 않음
영속성 컨텍스트의 변경내용을 DB에 동기화
트랜젝션이라는 작업 단위가 중요 ⇒ 커밋 직전에만 동기화 하면 됨

5. 지연 로딩(Lazy Loading)

  • 프록시(proxy)
    • em.find() : DB를 통해서 실제 엔티티 객체 조회
    • em.getReference(): DB 조회를 미루는 가짜(프록시) 엔티티 객체 조회
      • getReference() 호출하는 시점에는 DB 쿼리를 안 하는데도 객체가 조회됨
      • DB의 쿼리를 하는 시점은 getReference로 찾은 값이 실제 사용되는 시점
    • 프록시 특징
      • 실제 클래스를 상속 받아서 만들어짐
      • 실제 클래스와 겉 모양이 같음
      • 사용하는 입장에서는 진짜 객체인지 프록시 객체인지 구분하지 않고 사용하면 됨
      • 프록시 객체는 실제 객체의 참조(target)을 보관
      • 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드 호출
    • 프록시 객체의 초기화

 

👉 프록시 객체 특징
1. 프록시 객체는 처음 사용할 때 한번만 초기화
2. 프록시 객체를 초기화 할 때, 프록시 객체가 실제 엔티티로 바뀌는 것은 아님
     - 초기화되면 프록시 객체를 통해서 실제 엔티티에 접근이 가능한 것일 뿐
3. 프록시 객체는 원본 엔티티를 상속 받음 ⇒ 따라서, 타입 체크시 주의 ( == 비교 실패, 대신 instance of 사용)
4. 영속성 컨텍스트에 찾는 엔티티가 이미 있으면 em.getReference()를 호출해도 실제 엔티티 반환
5. 영속성 컨텍스트의 도움을 받을 수 없는 준영속 상태일 때, 프록시를 초기화하면 문제 발생 
  • 지연로딩
    • 지연로딩 LAZY를 사용해서 프록시로 조회
      • Member를 조회할 땐 딱 Member만 가져오고, Team은 proxy로 가져옴
@Entity
public class Member extends BaseEntity{
	@Id
	@GeneratedValue
	@Column(name = "member_id")
	private Long id;
	
	@Column(name = "username")
	private String name;
	
	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "team_id")
	private Team team;
	.
	.
	.
}

 

  • 실제 Team의 어떤 속성을 사용하는 시점에 프록시 객체가 초기화되면서 DB에서 해당 값을 가져옴

  • 즉시로딩
    • Member와 Team을 자주 함께 사용한다면? 즉시 로딩 EAGER을 사용해서 함께 조회
@Entity
public class Member extends BaseEntity{
	@Id
	@GeneratedValue
	@Column(name = "member_id")
	private Long id;
	
	@Column(name="username")
	private String name;
	
	@ManyToOOne(fetch = FetchType.EAGER)
	@JoinColumn(name = "team_id")
	private Team team;
	.
	.
	.
}

 

  • 쿼리가 나갈 대 애초부터 Member와 Team을 조인해서 한방에 다 가져옴
    • proxy가 아닌 진짜가 다 나오기 때문에 이미 초기화가 끝난 상태이며 초기화가 필요 없음
  •  
👉 프록시와 즉시로딩 주의
1. 가급적 지연 로딩만 사용(특히 실무에서)
2. 즉시 로딩을 적용하면 예상치 못한 SQL이 발생
   - 즉시 로딩은 JPQL에서 N+1 문제를 일으킴
3. @ManyToOne, @OneToOne은 기본이 즉시 로딩이기 때문에 따로, LAZY로 설정해 줄 것
4. @OneToMany, @ManyToMany는 기본이 지연로딩