본문 바로가기
JPA

영속성 관리 - 내부 동작 방식

by 성건희 2020. 4. 3.
반응형
영속성 관리 - 내부 동작 방식

JPA에서 가장 중요한 2가지

  • 객체와 관계형 데이터베이스 매핑하기
  • 영속성 컨텍스트

 

실제 JPA가 내부적으로 어떻게 동작하는지 이해하려면

영속성 컨텍스트에 대해 공부해야 한다.

 

엔티티 매니저 팩토리 / 엔티티 매니저

웹 어플리케이션에서 엔티티 매니저 팩토리

고객의 요청이 올 때마다 엔티티 매니저를 생성한다.

엔티티 매니저는 내부적으로 DB 커넥션을 사용해서 DB를 사용한다.

 

그럼 영속성 컨텍스트는 뭐야?

엔티티를 영구 저장하는 환경

EntityManager.persist(entity);

엔티티를 DB에 저장하는 것이 아니라, 영속성 컨텍스트에 저장한다는 뜻에 주의하자.

영속성 컨텍스트는 논리적인 개념으로, 눈에 보이지 않는다.

엔티티 매니저를 통해서 영속성 컨텍스트에 접근한다.

 

J2SE 환경에서는 엔티티 매니저와 영속성 컨텍스트가 1:1이다.

J2EE, 스프링 프레임워크 같은 컨테이너 환경에서는 엔티티 매니저와 영속성 컨텍스트가 N : 1이다.

 

참고 )

J2SE (Standard Edition)

  • 가장 보편적으로 쓰이는 자바 API 집합체이다.
  • 예전에는 J2SE로 불렸으나 6.0 이후 Java SE로 변경되었다.
  • 일반 자바 프로그램 개발을 위한 용도로 사용되며, 스윙이나 AWT 같은 GUI 방식의 기본 기능이 포함된다.

J2EE (Enterprise Edition)

  • 자바를 이용한 서버측 개발을 위한 플랫폼이다.
  • 전사적 차원(대규모 동시접속과 유지가 가능한 다양한 시스템의 연동 네트워크 기반 총칭)에서 필요로 하는 도구로 EJB, JSP, Servlet, JNDI 등의 기능을 지원하고 WAS를 이용하는 프로그램 개발 시 사용된다.

 

엔티티 생명주기

비영속 (new / transient)

영속성 컨텍스트와 전혀 관계가 없는 새로운상태

Member memeber = new Member();
member.setId(1L);
member.setUserName("건희");

 

영속 (managed)

영속성 컨텍스트에 관리되는 상태

Member memeber = new Member();
member.setId(1L);
member.setUserName("건희");

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

// 영속 상태
em.persist(member);

 

준영속 (detached)

영속성 컨텍스트에 저장되었다가 분리된 상태

영속성 컨텍스트가 제공하는 기능을 사용 못 한다.

(직접 사용할 일은 거의 없지만, 내부 매커니즘을 이해하기 위해서는 중요한 개념임)

Member memeber = new Member();
member.setId(1L);
member.setUserName("건희");

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

// 영속 상태
em.persist(member);
// 준영속 상태
em.detach(member);

 

준영속 상태로 만드는 방법

  • em.detach(entity)

    특정 엔티티만 준영속 상태로 전환

  • em.clear()

    영속성 컨텍스트를 완전히 초기화

  • em.close()

    영속성 컨텍스트를 종료

 

삭제 (removed)

삭제된 상태

Member memeber = new Member();
member.setId(1L);
member.setUserName("건희");

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

// 영속 상태
em.persist(member);
// 삭제 상태
em.remove(member);

 

영속성 컨텍스트의 이점

  • 1차 캐시
  • 동일성 (identity) 보장
  • 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
  • 변경 감지 (Dirty Checking)
  • 지연 로딩 (Lazy Loading)

 

1차 캐시

id가 1인 member를 em.persist(member); 하게 되면 member가 영속성 컨택스트의 1차 캐시에 저장된다.

1차 캐시는 key-value 구조로 되어있으며, key@Id("1"), valueEntity(member) 값이 들어간다.

 

이 후 em.find(Member.class, 1L);을 실행하면 바로 DB를 조회하는 것이 아니라

영속성 컨텍스트의 1차 캐시를 먼저 조회한다.

em.find(Member.class, 2L);를 조회한다면, 영속성 컨텍스트에 없으므로 DB에서 조회한다.

그 후 그 정보를 1차 캐시에 저장한 후 id가 2인 member를 반환한다.

 

사실 1차 캐시는 트랜잭션 단위로 만들어지므로,

고객의 요청이 하나 들어온 후 종료되면 영속성 컨택스트도 지운다.

따라서 비즈니스 로직이 매우 복잡한 경우가 아니라면 큰 성능 이점을 얻을 수는 없다.

 

문제 ) 쿼리가 몇번 나갈까?

Member findMember1 = em.find(Member.class, 100L);
Member findMember2 = em.find(Member.class, 100L);

 

findMember1를 조회할 때 영속성 컨텍스트에 없으므로 SELECT 쿼리가 1번 나간다.

그 후 findMember2를 조회할 때는 영속성 컨텍스트에 있으므로 1차 캐시에서 값을 꺼낸다.

따라서, 쿼리는 총 1번 발생하게 된다.

 

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

Member findMember1 = em.find(Member.class, 100L);
Member findMember2 = em.find(Member.class, 100L);

findMember1 == findMember2 // ??

답은 true가 나온다.

1차 캐시로 반복 가능한 읽기 (Repeatable READ) 등급의 트랜잭션 격리 수준을

데이터베이스가 아닌 애플리케이션 차원에서 제공한다.

(말이 어려워서 잘 이해가 안갈수도 있는데, 같은 트랜잭션 안에서는 동일성이 보장된다고 이해하자)

주의 ) 동일성 보장은 같은 트랜잭션 안에서 일어나야 함에 주의한다.

 

트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)

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

em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 보내지 않는다.

transaction.commit(); // 트랜잭션 커밋

1차 캐시에 member를 저장하고,

동시에 JPA가 엔티티를 분석해서 INSERT SQL을 생성한다.

그 후 쓰기 지연 SQL 저장소에 SQL을 저장한다.

트랜잭션이 커밋되는 시점쓰기 지연 SQL 저장소에 있던 SQL들이 flush 되면서 DB에 쿼리를 보낸다.

 

참고 ) JDBC batch를 이용하면 쿼리를 일정 크기만큼 모았다가 한방에 쿼리를 보낼 수 있다. (버퍼링)

하이버네이트에서의 설정은 다음과 같다.

<property name="hibernate.jdbc.batch_size" value="10"/> 

 

변경 감지 (Dirty Checking)

Member member = em.find(Member.class, 1L);
member.setName("ZZZ");

em.persist(member)로 변경된 값을 영속성 컨택스트에 넣지 않아도, 마치 자바 컬렉션을 이용하듯이 변경이 됨.

(em.persist(member)를 직접 쓰는 방법은 개발자의 실수를 유발할 수 있기 때문에 써서도 안된다.)

 

생각해보기 ) 어떻게 변경을 감지하는 걸까?

트랜잭션을 커밋하면 flush()가 호출된다.

그 후 1차 캐시 안에 있는 엔티티스냅샷을 비교한다.

다르면 UPDATE 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장한다.

쓰기 지연 SQL 저장소에 있던 SQL들이 flush 되면서 DB에 쿼리를 보낸 후 commit 된다.

 

참고 ) 스냅샷이란?

영속성 컨택스트의 1차 캐시에 들어온 최초 상태

 

플러시 란?

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

 

플러시가 발생하면 무슨일이 생길까?

트랜잭션이 커밋되면 플러시가 자동으로 발생되며, 다음과 같은 일이 발생한다.

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

 

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

em.flush() - 직접 호출

직접 쓸 일은 거의 없다. 보통 테스트 용으로 쓰인다.

Member member = new Member();
em.persist(member);

em.flush(); // 트랜잭션 커밋 전에 쿼리를 보고 싶은 경우

tx.commit();

 

트랜잭션 커밋 - 플러시 자동 호출

Member member = new Member();
em.persist(member);

tx.commit(); // flush 자동 호출

 

JPQL 쿼리 실행 - 플러시 자동 호출

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);

// JPQL 실행
query = em.createQuery("select m from Member m", Member.class);

트랜잭션이 커밋되는 시점에 DB에 SQL이 반영된다.

따라서 트랜잭션 커밋 전에 JPQL로 DB 직접 조회를 하면 DB에는 값이 없기 때문에 가져올것이 없다.

이런 문제가 발생할 수 있기 때문에 JPA는 JPQL을 실행할 때는 무조건 flush를 호출한다.

 

플러시 모드 옵션

em.setFlushMode(FlushModeType.COMMIT)

  • FlushModeType.AUTO

    커밋이나 쿼리를 실행할 때 플러시 (기본값)

  • FlushModeType.COMMIT

    커밋할 때만 플러시

    em.persist(memberA);
    em.persist(memberB);
    em.persist(memberC);
    
    // JPQL 실행
    query = em.createQuery("select c from Car c", Car.class);
    

    위 처럼 member가 아닌, 전혀 다른 쿼리를 날릴경우는 flush를 할 필요가 없기 때문에

    플러시 해봐야 얻을 이점이 없다.

    하지만 사실상 큰 도움은 안됨. 그냥 AUTO 쓰자

 

플러시 정리

  • 영속성 컨텍스트를 비우지 않는다.
  • 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화
  • 트랜잭션 커밋 직전에만 동기화 하면 된다.

 

JPA는 엔티티에 기본 생성자가 있어야 한다.

JPA는 내부적으로 리플랙션을 사용해서 동적으로 객체를 생성한다.

그렇기 때문에 기본 생성자가 있어야 한다.

 

참고

- https://medium.com/@dnjswbf/j2se-j2ee%EC%9D%98-%EC%B0%A8%EC%9D%B4-9e474cc3e46c

- 자바 ORM 표준 JPA 프로그래밍 - 기본편

 

반응형

'JPA' 카테고리의 다른 글

JPA 를 공부하면서 알게 된 내용 정리 1  (0) 2022.01.26
다양한 연관관계 매핑  (0) 2020.04.09
연관관계 매핑 기초  (0) 2020.04.06
엔티티 매핑  (0) 2020.04.06
JPA를 왜 써야 하는가?  (0) 2020.04.03

댓글