본문 바로가기
[IT]/JPA

[JPA] JPA의 영속성 관리 (JPA 영속성 컨텍스트)

by dop 2021. 3. 16.
이번에는 JPA 영속성 컨텍스트, 엔티티의 라이프 사이클,
영속성 컨텍스트가 엔티티 매니저에 의해 어떻게 관리되는지 알아봅시다! 

1. 엔티티 매니저 팩토리 & 엔티티 매니저

//JpaMain.java 중 일부 

//엔티티 매니저 팩토리 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성

EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득

 

엔티티 매니저 팩토리는 하나를 공용으로 쓰고 엔티티 매니저를 생성한다.

엔티티 매니저 DB와 상호작용 하는 역할을 한다.

엔티티 매니저는 트랜잭션을 시작할 때 커넥션을 획득한다. 

 

2. 영속성 컨텍스트? 

JPA의 가장 큰 특징 중 하나로 엔티티를 영구적으로 저장하는 환경이라는 뜻을 가지고 있다.

em.persist(member); 

위와 같은 형식으로 사용한다. (엔티티 매니저를 이용해서 회원 엔티티를 영속성 컨텍스트에 저장)

영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나 만들어지고, 엔티티 매니저를 이용해 관리, 활용할 수 있다.

(JPA의 개념에 대해 처음 접한다면 아직까지는 뭔지 감이 잘 오지 않을 것이다... 괜찮다! 이제 알아가면 된다.) 

 

3. 엔티티 생명주기

엔티티에는 크게 4가지 생명주기가 있다.

 

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

- 비영속 

Member member = new Member();
member.setId("member1");
member.setName("회원1");

엔티티 객체를 생성 후 저장하기 전의 상태, 아직 저장 전 상태이므로 영속성 컨텍스트, DB 와는 관련이 없다.(em.persist() 호출 전!)

- 영속

em.persist(mebmer);

엔티티 매니저를 이용해 영속성 컨텍스트에 저장한 상태.

추가로 em.find()나 JPQL을 활용해서 조회한 엔티티도 영속성 컨텍스트가 관리하는 영속 상태다.

- 준영속

em.detach(member);
em.close();// 영속성 컨텍스트 자체를 닫거나
em.clear();// 초기화해도 준영속 상태가 된다!

영속 상태의 엔티티를 영속성 컨텍스트에서 빼낸 상태.

- 삭제

em.remove(member);

 

엔티티를 영속성 컨텍스트와 DB에서 삭제한다. (다른 것들과는 다르게 DB에서도 삭제된다!)

4. 영속성 컨텍스트의 특징 

 - 영속성 컨텍스트와 식별자 값

    엔티티를 식별자 값(@Id로 매핑한 값)으로 구분한다.따라서, 영속 상태의 엔티티는 반드시 식별자 값이 있어야 한다.

 - 영속성 컨텍스트와 DB 저장

    영속성 컨텍스트에 저장된 엔티티는 트랜잭션을 커밋하는 순간 DB에 반영되는데 이것을 flush라고 한다.

 - 영속성 컨텍스트가 엔티티를 관리할 때의 장점

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

4.1) 엔티티 조회

영속성 컨텍스트는 내부에 1차 캐시를 갖고있는데, 영속상태의 엔티티는 모두 1차 캐시에 저장된다.
(내부에 Map이 있는데 Id로 식별, 값은 엔티티 인스턴스!)
//비영속
Member member = new Member();
member.setId("mebmer1");
member.setUsername("회원1");

//영속
em.persist(member);

위 코드를 실행하면 1차 캐시에 회원 엔티티가 저장된다.(아직 DB에는 저장안됨!)

1차 캐시의 키는 식별자 값이고 DB의 기본키와 매핑되어있다. 따라서 영속성 컨텍스트에 데이터를 저장, 조회하는 모든 기준은 DB의 기본키 값이다.

Member mebmer = em.find(Member.class,"member1");
//find(엔티티 클래스 타입, 조회할 식별자 값);

em.find()메소드는 호출시 먼저 1차 캐시에서 엔티티를 찾고 없다면 DB에서 조회한다.(1차 캐시에 있으면 바로 반환!)

1차 캐시에 존재하는 경우 성능상 이점을 누릴 수 있다. (준영속 상태로 만들지 않는 이상 1차 캐시에 존재하는 경우가 많다)

 

- 영속 엔티티의 동일성 보장 ( m1 == m2 )

   

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

System.out.println(a==b); // 동일성 비교
// 결과는 참이다! 

객체a를 조회할 때는 1차 캐시에 찾는 인스턴스가 없어 DB에서 가져온다. 이때 해당 엔티티가 1차 캐시에 올라간다. 그 상태에서 객체b를 조회하면 1차 캐시에 존재하기 때문에 DB까지 접근하지 않고 1차 캐시에서 엔티티를 받아온다.

즉, 객체 a와 b는 완전하게 동일한 것을 참조하게 된다!!

JPA는 1차 캐시를 통해 반복 가능한 읽기등급의 트랜잭션 격리수준을 DB가 아닌 어플리케이션 차원에서 제공한다는 장점이 있다! (반복되는 동일한 읽기 요청에 대해 DB까지 가지않고 해결!)

※ 동일성(==)과 동등성(equals메소드)

(Java를 어느정도 사용해 본 사람은 "==" 연산 때문에 애먹은 적이 한번쯤은 있을 것이다..)
"==" 비교연산자 : 실제 인스턴스가 같은지 비교 (참조 값 비교)
equals() 메소드   : 인스턴스가 가지고 있는 값 비교

4.2) 엔티티 등록

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

//엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 함!

transaction.begin();//[트랜잭션 시작]

em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 DB에 보내지 않음

//커밋하는 순간 보냄!
transaction.commit();//[트랜잭션 커밋]

엔티티 매니저는 트랜잭션을 커밋하기 직전까지 내부 쿼리 저장소에 INSERT SQL을 차곡차곡 모아둔다. 그리고 트랜잭션 커밋시에 모아둔 쿼리를 DB에 보내는데, 이것을 트랜잭션을 지원하는 쓰기 지연(transactional write-behind) 이라고 한다.

 

트랜잭션을 커밋하면 엔티티 매니저는 영속성 컨텍스트를 플러시한다. 즉, 변경내용을 DB에 동기화하는 작업을 한다.

(내부 쿼리 저장소에 모아둔 내용을 DB에 보내서 DB와 영속성 컨텍스트 내용을 일치시킨다.)

+++) 잘 활용하면 등록 쿼리를 한번에 날리기 때문에 성능을 최적화 할 수 있다고 한다....(찾아볼 필요가 있어보인다)

 

4.3) 엔티티 수정

수정쿼리의 문제점에 대해 생각해보자.
기존 프로젝트에 이름과 나이를 수정하는 쿼리가 있었는데, 등급을 수정하는 기능이 추가되면 새로운 SQL문이 추가된다.
이번엔 주소와 전화번호가 추가되었다고 생각해보자... 계속해서 수정을 위한 SQL문이 늘어나는 현상이 발생한다.
(모든 컬럼을 변경하는 쿼리로 해결할 수 있지만, 실수로 컬럼하나라도 입력하지 않을 수 있다.)

이렇게 SQL문을 작성/변경하지 않고 변경된 내용을 알아서 감지하고 알아서 변경해주면 정말 편할 것 같다...
=> JPA의 변경감지로 해당 기능이 구현된다!! (개인적으로 JPA가장 큰 강점이라고 생각한다.)
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();//[트랜잭션 커밋]

 

위처럼  update메소드 조차 호출하지 않고도 변경을 감지해 DB에 자동으로 반영해주는 기능을 변경 감지(dirty checking)라고 한다.

 

JPA는 엔티티를 영속성 컨텍스트에 보관할 떄, 최초 상태를 복사해서 저장해두는데 이를 스냅샷이라 한다. 그리고 플러시 시점에 스냅샷과 엔티티를 비교하여 변경된 것을 찾는다. 변경된 엔티티가 있는경우 쿼리를 생성해 SQL 저장소에 보내고 역시 SQL 저장소 내용을 한꺼번에 DB에 적용한다. 

단, 변경감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔터티에만 적용된다.
변경된 컬럼만 변경되도록 SQL이 생성되는게 아니라 모든 컬럼값이 변경되도록 수정쿼리가 생성된다.
전송하는 양은 커지지만, 동일한 쿼리를 보내면 DB는 이전에 파싱된 쿼리를 재사용하기에 성능상 이득을 볼 수 있다.
(필드가 30개 이상이면 DynamicUpdate도 있는데, 필드가 30개를 넘는 것은 테이블 설계상 책임이 분리되지 않았을 가능성이 높다.)

4.4) 엔티티 삭제

삭제하기 위해서는 먼저 삭제 대상 엔티티를 조회해야한다.

Member memberA = em.find(Member.class,"memberA");
em.remove(memberA); //삭제

등록과 동일하게 삭제역시 지연 쓰기 SQL저장소에 등록한다. 이후 플러시 될 때 DB에 적용된다.

단, remove메소드 호출시 영속성 컨텍스트에서는 제거된다.(삭제 후에는 재사용 x 가비지 컬렉션이 데려가도록 냅둔다.) 

 

5.플러쉬(flush)

영속성 컨텍스트의 변경내용을 DB에 적용하는 작업

플러쉬 발생시점

1. EntityManager.flush(); 호출시 // 거의 쓰지않음

2. 트랜잭션 커밋시

3. JPQL 쿼리 실행시 

em.persist(member1);
em.persist(member2);
em.persist(member3);
//영속성 컨텍스트에 추가 (아직 DB에 적용되기 전)

//JPQL
Query query = em.createQuery("select m from Member m ",Member.class);
//JPQL 실행시 중간에 flush 된 후 DB에 JPQL 쿼리가 날아가고 정상적으로 조회가 됨

- 플러쉬를 하더라도 영속성 컨텍스트는 계속 유지 / DB에 변경사항만 적용하는 작업이다.

- 트랜잭션이라는 작업단위가 중요함, 커밋 직전에만 동기화 하면 됨!  

 

참고자료: 자바 ORM 표준 JPA 프로그래밍 / 김영한 

github : github.com/holyeye/jpabook/github.com/holyeye/jpabook/tree/master/ch02-jpa-start1

 

728x90