본문 바로가기
Spring/Spring Data JPA

Hibernate(JPA) 영속성 (persistence)

by 아이티.파머 2022. 4. 22.
반응형

Hibernate(JPA) 영속성 (persistence)

JPA/ Hibernate 를 사용할때 대부분 생명주기를 모른상태에서 개발을 하다보면 실수? 혹은 왜 이렇게 되지 하는 경우를 겪게 된다. 알고 있더라도 왜이러지 이럴때가 있다! 어? 왜이렇게 동작하지 하고 말이다.

1. Entity life sycle (엔티티생명주기)

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

1.1 비영속성(new)

앞서 이야기 했듯이 영속성 컨텍스트와 전혀 관련이 없는 상태로 엔티티를 새로 생성한 단계이다.

MemberEntity memberEntity = new MemberEntity()

1.2 영속 (managed)

영속성 컨텍스트에 관리되는 상태를 말하며, 엔티티메니저(EntityManager)를 통해 엔티티(Entity)를 저장한 상태를 말한다.

  • 말그대로 영속성 컨데이터에 저장된상태이며, DB에 저장된 상태는 아니다.
  • 커밋 시점에 (flush())영속성컨텍스트에 있는 정보들이 DB에 저장된다.
em.pesist(memberEntity)

1.3 준영속성(detached)

영속성에서 관리되다가 더이상 관리대상이 되지 않을때 준영속성 상태가 된다.

// 엔티티를 영속상태에서 준영속상태로 만든다.
em.detach(memberEntity);
// 영속성 컨테이너를 클리어시키면 관리되던 엔티티는 준영속성 상태가 된다.
em.clear();
// 영속성 컨테이너를 닫으면 관리되던 엔티티는 clear()와 같이 준영속성 상태가 된다.
em.close();

준영속성 상태의 특징

  • 1차캐시, 쓰기지연, 변경감지, 지연로딩을 포함한 영속성컨텍스트에서 제공하는 어떠한 기능도 동작하지 않는다.

1.4 삭제 (removed)

삭제된 상태이며 엔티티를 영송석 컨텍스트와 데이터베이스에서 모두 삭제한다.

em.remove(memberEntity)

2. 영속성 컨테이너의 특징 및 이점

2.1 1차 캐시 지원

영속석컨텍스트(EntityManager)에는 내부에 1차 캐시가 존재한다. 엔티티를 컨텍스트에 저장하는순간 (persist()) 1차캐시에 key=@Id, value=Entity 자체로 캐시에 저장된다. (Map 형태이며 캐시에 저장할때 스냅샷도 함게 저장된다.)

1차캐시가 있으면 내부적으로 조회할때 이점이 생긴다. 이것은 조회를 할때 쉽게 설명 할 수 있는데 데이터를 find() 할때 먼저1차캐시를 조회해보고 이때 데이터가 없으면 DB 까지 다녀오게된다.
즉 1차캐시를 통해좀더 빠르고 안정적으로 데이터를 조회 하게된다.

MemverEntity memverEntity = new MemberEntity();
memberEntity.setMemberId(00001L);
memberEntity.setName("sangkil");
memberEntity.setAge(38);

// 1차캐시: 영속성 대상
em.persist(memberEntntity);

// 1차캐시에서 조회 
em.find(MemberEntity.class,00001);

당연히 1차캐시에 데이터가 없으면 DB 에서 조회를 한다.

단, 여기서 오해할수 있는게 있는데 1차케시는 글로벌 하지 않다. 하나의 트랜잭션 범위안에서만 사용하는 굉장이 짧은 캐시레이어이다. 즉 100번의 요청이 오면 100번 모두 1차캐시가 생성되게 되는 것이고 트렌젝션이 끝나는 시점에 모두 사라진다.

2.2 동일성 보장 (indentity )

영속성 컨텍스트에서는 엔티티의 동일성을 보장한다.

MemberEntity m1 = em.find(MemberEntity.class,00001);
MemberEntity m2 = em.find(MemberEntity.class,00001);
System.out.println("m1과 m2는 동일한가 : "+ m1==m2) // 동일성 비교 

// 결과값 
// m1과 m2는 동일한가 : true

1차 캐시에서 꺼내거나, 없더라도 DB에 다녀온뒤 1차캐시에 넣음으로 영속성상태(1차캐시) 안에서 데이터를 가져옴으로 동일한 데이터를 가져온다.

  • 동일성 : 실제 인스턴스가 같을경우 == (메모리참조값)
  • 동등성: 실제 인스턴스는 다르지만 가지고 있는 값이 같다. equals()

2.3 트렌젝션 쓰기지연 지원 (transactional wirite-behind)

트랜젝션 내부에서 persist()가 일어날때, 엔티티들은1차 캐시에 저장하고 논리적으로 쓰기지연 SQL 저장소라는 곳에 INSERT 쿼리들을 생성해서 쌓아놓고 기다린다.

commit 하는 시점에 DB에 동시에 쿼리들을 보내어 실행하게 하는데 , 이때 쌓여있는 쿼리들을 DB에 보내는동작이 flush() 이다. flush()는 1차캐시를 지우지않는다. 쿼리들을 DB에 보내고 DB와 싱크를 맞추는 역할을 한다. 실제 쿼리를 보내고난뒤에 commit() 한다. 즉 트랜젝션을 커밋하게 되면 flush() 와 commit() 두가지 일을 하게 되는것이다.

이렇게 트렌젝션 커밋을할때 모아둔 쿼리를 DB에 보내며 처리하는 작업을 트랜젝션을 지원하는 쓰기 지연이라 한다.

EntityManager em = emf.createEntityManager();
EntityManagerTransaction eTransation = em.getTransaction();

// 트렌젝션의 시작
transaction.begin();

// insert 문을 지연 SQL 저장소에 쌓아놓는다.
em.persist(memberA);
em.persist(memberB);
// 이때까지도 SQL 문을 데이터베이스에 보내지 않는다.

// 트렌젝션을 커밋하는 순간데이터베이스에 쌓여있는 insert sql 문을 보낸다.
transaction.commit()

hibernate.jdbc.batch_size 옵션을 이용하여 JDBC 일괄 처리 옵션을 설정 할수 있다.

hibernate.jdbc.batch_size=10

2.4 변경감지(Dirty Checking)

JPA 로 엔티티를 수정할 때는 단순히 엔티티를 조회 하고 데이터를 변경하면 된다.

변경감지는 영속성 컨텍스트가 관리하는 영속성 상태의 엔티티에서만 적용된다.

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

MemberEntity member = em.find(MemberEntity.class, 00001L);
member.setName("sangkil-2");

// 업데이트나 퍼시스트를 해줘야 하지 않을까? 
// em.update(member) or em.persist(member) 을 하지 않아도 된다. 

// transaction commit() 을 호출하면 엔티티 내부에서 먼저 flush()가 호출된다.
// 말그대로 영속성상태의 데이터의 변경을 감지하고 SQL을 쓰기지연 SQL 저장소에 저장한다. 
transaction.commit();

변경감지의 흐름도

  1. transaction commit() 을 하게 되면 entity manager 내부에서 먼저 flush() 가 호출된다.
  2. commit() or flush() 가 일어났음으로 . 변경된 엔티티가있는지 찾아본다
    1. Entity 와 Snapshot 을 비교하여 변경된 엔티티를 찾는다 (dirty checking - 변경감지 )
    2. 1차 캐시에 저장할때 동시에 스냅샷 필드도 저장한다 (참고)
  3. 스냅샷과 변경된 엔티티를 비교해서 수정쿼리를 생성하고 쓰기지연 SQL 저장소에 저장한다.
  4. 쓰기지연 저장소의 SQL을 플러시 한다.
  5. 데이터베이스에 트렌젝션을 커밋 한다.

변경감지를 지원하는 이유

다른블로그에서도 같은 생각을 가진거 같다. 업데이트를 할때 update() 를 지정해서 사용하면 편하지 않나? 하고 말이다. 근데 이건 사상의 문제라고 한다. 누가 말했는지는 모르겠다.

  • 이처럼 우리는 자바 컬렉션에서 리스트 값을 변경하고나서 리스트에 다시 그값을 담지 않는다.
    값을 변경하면 변경된 값을 유지하는것과 같은 컨셉이라고 한다.
  • 따라서 영속성 상태의 엔티티를 가져와서 값만 변경하고 트렌젝션을 종료 시키기만 하면 수정 작업은 완료된다.

업데이트를 할때 변경된 필드만 변경하고자 할때 @DynamicUpdate 지정

ref. @DynamicUpdate https://jojoldu.tistory.com/415

2.5 플러시(flush)

  • 플러시가 일어날때 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다. (준영속성 상태인경우 제외)
  • 트랜젝션 커밋이 일어날때 플러시가 동작하게 되는데, 쓰기지연 저장소에 쌓아 놨던 INSERT,UPDATE,DELETE 구문(SQL) 들이 데이터베이스로 날라가 실행된다.
  • 영속성 컨텍스트의 변경사항들을 데이터베이스와 싱크하는 작업이다.

2.5.1 플러시 발생시의 흐름

  • 엔티티 변경을감지한다 ( Dirty Checking)
  • 수정된 엔티티를 쓰기지연 SQL 저장소에 등록한다.
  • 쓰기지연SQL 저장소에 등록된 내역을 데이터베이스에 전송한다.
  • flush 다음에 commit 이 일어난다.

2.5.2 플러시(flush) 하는 방법

  • em.flush() 직접 호출
  • transaction commit() (트렌잭션 커밋) 시에 자동호출됨
  • JPQL 쿼리 사용시 자동호출 됨

2.3.3 호출방법 예제)

  • flush 직접 호출방법

  • // 객체 생성 MemberEntity memberEntity = new MemberEntity(); // 영속상태로 만든다 em.persist(memberEntity); // flush 호출 em.flush(); System.out.println("플러시 직접 호출하면 쿼리가 커밋 전 플러시 호출 시점에 나감"); transaction.commit();

  • JPQL 쿼리 실행 방법영속1~3까지 SQL 저장소에만 들어가 있는상태에서 JPQL 로데이터를 조회 하게되면 데이터가 없다고 나오게된다. 이를 방지하기 위해서 JPQL 사용시 강제로 플러시가 일어나고 데이터를 조회 할 수 있게된다.

  • em.persist(memberEntityA);//영속1 em.persist(memberEntityB);//영속2 em.persist(memberEntityC);//영속3 // 중간에 JPQL 실행 query = em.createQuery("select m from Member m", MemberEntity.class); List<MemberEntity> members = query.getResultList();

  • JPQL 사용히 flush() 가 자동으로 일어나는 이유는 다음과 같다.

2.5.4 플러시 정리

  • 플러시는 영속성 컨텍스트를 비우지 않는다. (사라지는건 트렌젝션이 종료될때-해당 Thread 의 종료)
  • 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화 한다.
  • 플러시가 동작할 수 있는 이유는 데이터베이스에 트랜젝션이라는 작업 단위가 있기 때문이다.
  • 트랜젝션이 시작되고 커밋되는 시점에만 동기화 해주면 되기 때문에 그사이에서 플로시 매커니즘의 동작이 가능하다는 것이다.
  • JPA는 기본적으로 데이터를 맞추거나 동시에 관련된 것을들 데이터베이스의 트랜잭션에 위임한다.

2.5.5 플러시 모드 옵션

플러시에도 옵션이 있다. em.setFlushMode(type=...)

  • FlushModeType.AUTO (기본값) : 커밋이나 쿼리 실행시 플러시
  • FlushModeType.COMMIT : 커밋시에만 플러시

3. 영속성 및 준영속성 상태 심화

3.1 영속상태

  • 1차캐시에 올라간 상태 이며, 엔티티메니저가 관리하는 상태
  • em.persist() 로 컨텍스트에 저장한 상태도 영속상태이며
  • em.find()로 조회를 할때도 1차캐시에 없어 DB에 조회하여 1차캐시에 저장한 상태도 영속상태다
// 트렌젝션 시작
transaction.begin();

// 영속상태
MemberEntity memberEntity = em.find(MemberEntity.class,00001);

memberEntity.setName("SangHyeon");
memberEntity.setAge(39);
// 커밋
transaction.commit()

위코드의 흐름도

  1. find() 로 1차 캐시 실행 (DB에 데이터를 조회해와도 영속상태임)
  2. setName 과 setAge 로 이름과 나이를 변경함,
  3. transaction.commit 을 함으로 flush 되고 영속상태이기 때문에 Dirty Checking이 일어남
  4. 변경된 엔티티의 스냅샷과 비교하여 UPDATE SQL 을 만들고 저장소에 SQL저장소에 남긴다
  5. commit 일어남으로 저장소 SQL 에있는 UPDATE SQL을 DB에 전송하고 실행한다. (데이터업데이트 됨)

3.2 준영속상태

생각하기에 준영속상태와 영속상태에서의 가장큰 차이점은 데이터를 엔티티메니저에서 하지않고 영속성 컨텍스트에서 분리된 상태이기 때문에 commit을 한다고해도 아무일도 일어나지 않는것이다. 이내용을 알아야 더티체킹(dirty checking) 의 의미를 알 수 있을것이라 생각한다.

아래코드를 보자

// 트렌젝션 시작
transaction.begin();

// 영속상태
MemberEntity memberEntity = em.find(MemberEntity.class,00001);

memberEntity.setName("SangHyeon");
memberEntity.setAge(39);

// 영속성 분리 (준영속성 상태)
em.detach(memberEntity);

// 커밋
transaction.commit()
  • em.detach(memberEntity) (clear(), close()) 으로 멤버엔티티를 영속성에서 분리함.
  • 데이터를 수정하고 트렌젝션을 커밋하여도, 영속성 컨텍스트가 제공하는 기능을 사용하지 못하고 UPDATE 문의 쿼리가 생성되지 않는다.

ref.

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의 (inflearn.com)
[JPA] 영속성 컨텍스트와 플러시 이해하기 (tistory.com)
JPA 영속성 컨텍스트란? (velog.io)

반응형