Peony의 기록 창고 🌼
article thumbnail
반응형

JPA 에서 가장 중요한 2가지

  1. 객체와 관계형 데이터베이스 매핑하기 (Object Relational Mapping)
  2. 영속성 컨텍스트

JPA를 보통 쓰게되면, 엔티티 매니저 팩토리와 엔티티 매니저에 대해 이해를 해야한다.

예를 들면, 고객의 요청이 올 때마다 엔티티 팩토리를 통해 엔티티 매니저를 생성한다. 엔티티 매니저는 내부적으로 데이터 커넥션을 사용해서 데이터베이스를 사용하게 된다.

 

❓그럼 영속성 컨텍스트란 뭘까?

 

영속성 컨텍스트란?

  • 엔티티를 영구 저장하는 환경
  • EntityManager.persist(entity); : DB에 저장한다기보다 영속성 컨텍스트를 통해 엔티티를 영속화 한다는 의미이다. → 영속성 컨텍스트에 엔티티를 저장한다.
  • 영속성 컨텍스트는 논리적인 개념이고, 눈에 보이지 않는다.
  • 엔티티 매니저를 통해서 영속성 컨텍스트에 접근
    • J2SE 환경

 

엔티티의 생명주기

엔티티의 생명주기

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

 

비영속

JPA에 관계없이 그냥 객체 생성만 된 상태

 //객체를 생성한 상태(비영속) 
 Member member = new Member(); 
 member.setId("member1"); 
 member.setUsername("회원1");

 

영속

 //객체를 생성한 상태(비영속)
 Member member = new Member();
 member.setId("member1");
 member.setUsername("회원1");
 ​
 EntityManager em = emf.createEntityManager();
 em.getTransaction().begin();
 ​
 //객체를 저장한 상태(영속)
 em.persist(member);

주의 : 영속상태가 된다고 쿼리가 날라가는게 아니다. 저장하려면 commit 필수!

 

준영속, 삭제

 //회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태 
 em.detach(member); 
 ​
 //객체를 삭제한 상태(삭제) 
 em.remove(member);
  • 준영속 : 영속성 컨텍스트에서 삭제하는 것
  • 삭제 : 실제로 DB에서 해당 ROW를 삭제하는 것.

 

영속성 컨텍스트의 이점

1. 엔티티 조회, 1차 캐시

영속성 컨텍스트는 내부에 1차캐시라는 것을 들고 있다.

 //객체를 생성한 상태(비영속)
 Member member = new Member();
 member.setId("member1");
 member.setUsername("회원1");
 ​
 //객체를 저장한 상태(영속)
 em.persist(member);

예를 들어 멤버객체를 생성해서, 값을 세팅을 한다. (비영속) em.persist에서 멤버 객체를 집어넣으면, 내부에 1차 캐시라는 것이 있다. key가 디비 pk로 매핑한 값. 값은 엔티티 자체, 즉 멤버 자체가 값이 된다.

 

❓이렇게 되는게 무슨 이점이 있을까?

⇒ 조회할 때를 생각해보자.

 Member member = new Member();
 member.setId("member1");
 member.setUsername("회원1");
 ​
 //1차 캐시에 저장됨
 em.persist(member);
 ​
 //1차 캐시에서 조회
 Member findMember = em.find(Member.class, "member1");

이렇게 멤버 객체를 저장을 해놓고 em.find 를 통해 조회를 한다고 생각해보자.

JPA는 우선, 영속성 컨텍스트에서 1차 캐시를 찾아본다. 1차 캐시에 있으면 캐시에 있는 값을 그냥 조회해온다. 
 그런데, 만약 member2가 1차 캐시에 없고, DB에만 존재할 때, member2을 조회한다고 생각해보자. 그럼 1차 캐시에 없는 것을 확인하고, DB에서 조회를 한다. DB에서 조회한 member2를 다시 1차 캐시에 저장을 한다. 그 후 1차 캐시에서 member2를 반환해준다.

 사실 이게 큰 도움은 안된다. 엔티티 매니저라는 것은 대답을 트랜잭션 단위로 만들고, 트랜잭션이 끝날 때 같이 종료시킨다. 다시말해, 고객의 요청이 하나 들어와서 비즈니스가 끝나면, 영속성 컨텍스트를 지운다는 의미이므로 찰나의 순간에만 이점이 있다. 여러 트랜잭션을 관리하는 것은 2차 캐시이다. 따라서 성능적인 이점보다는 컨셉이 주는 이점이 있다.

 

2. 영속 엔티티의 동일성(identity) 보장

자바 컬렉션에서 똑같은 것을 가져와서 == 비교하면 똑같다. 이처럼 JPA에서도 영속 엔티티의 동일성을 보장해준다.

 Member a = em.find(Member.class, "member1");
 Member b = em.find(Member.class, "member1");
 System.out.println(a == b); //동일성 비교 true

위 코드를 돌려보면 true 값이 나온다. 이를 통해 JPA는 영속 엔티티의 동일성을 보장해준다는 것을 알 수 있다. 이런게 가능한 이유 : 1차 캐시가 있기 때문에 가능.

정리 : 1차 캐시로 REAPEATABEL READ 등급의 트랜잭션 격리 수준을 DB가 아닌 애플리케이션 차원에서 제공한다. 마치 자바의 컬렉션에서 값을 꺼낼 때 동일함을 보장해야한다는 것을 JPA에서 제공한다.

3. 엔티티를 등록할 때, 트랜잭션을 지원하는 쓰기 지연

 Member member1 = new Member(150L, "A");
 Member member2 = new Member(160L, "B");
 ​
 em.persist(member1);
 em.persist(member2);
 //여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
 ​
 transaction.commit(); // [트랜잭션] 커밋
 //커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
  • 트랜잭션 실행하고, em.persist로 멤버A, B를 넣는다.
  • 쓰기 지연 SQL에 저장된 쿼리들은 commit() 시점에 다 실행시켜서 DB에 적용한다.

 

memberA, B는 둘 다 쓰기지연 SQL저장소에 저장되어있고, 실제 DB에 적용은 안된 상태

❓왜 쓰기지연을 사용할까?

→ 버퍼링 기능이 제공된다. <property name="hibernate.jdbc.batch_size" value="10"/> 를 통해 10개씩 쌓일때마다 적용하게 한다. → 쿼리를 여러번 날리지 않고 최적화가 가능하다.

 

 

4. 엔티티 수정 : 변경 감지 (Dirty Checking)

 //엔티티 조회
 Member memberA = em.find(Member.class, "memberA");
 ​
 //영속 엔티티 데이터 수정
 memberA.setUsername("hi");
 memberA.setAge(10);
 ​
 transaction.commit();
  • update 관련 쿼리 없이 set으로 수정이 가능하다.

  • 1차 캐시안에는 @Id, Entity , 스냅샷 이 있다. 여기서 스냅샷 은 최초로 영속성 컨텍스트(1차캐시)에 들어오는순간 스냅샷을 찍어서 저장해둔다.
  • JPA는 트랜잭션이 커밋(commit)되는 순간 엔티티와 스냅샷을 모두 비교한다.
  • 변경된 것이 있을 경우 쓰기지연 SQL 저장소 에 업데이트 쿼리를 저장하고 수행하게 된다.

 

엔티티 삭제

 //삭제 대상 엔티티 조회
 Member member = em.find(Member.class, "memberA");
 em.remove(member);//엔티티 삭제

 

플러시란?

영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것.

보통 데이터베이스의 트랜잭션이 커밋될 때 발생

아까 우리가 쌓아놨던 쿼리들을 데이터베이스에 날려주는 것

플러시 발생

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

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

  1. em.flush() : 직접 호출
    • 플러시를 하면, 1차캐시도 지워지지 ❌ , 영속 컨텍스트에 있는 쓰기지연 SQL 저장소에 있는 쿼리들이 반여잉 되는 것.
  2. 트랜잭션 커밋: 플러시 자동 호출
  3. JPQL 쿼리 실행: 플러시 자동 호출
    • 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쿼리를 실행하는 시점에서 영속화 컨텍스트에 등록한 member들이 조회가 안되는 경우를 막기 위해 JPA에서는 JPQL 쿼리를 수행하기전에 flush를 실행해서 DB와 영속성 컨텍스트간에 동기화를 해준다.

 

정리

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



준영속 상태

  • 영속 → 준영속
  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
  • 영속성 컨텍스트가 제공하는 기능을 사용 못한다.
    • 변경 감지, 1차 캐시 등

 

준영속 상태로 만드는 방법

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

 

이 글은 김영한 님의 "자바 ORM 표준 JPA 프로그래밍 - 기본편" 강의를 듣고 정리한 내용입니다.

 

반응형
profile

Peony의 기록 창고 🌼

@myeongju