article thumbnail image
Published 2021. 10. 18. 00:01
반응형

JPA 영속성 컨텍스트에 관하여

클라이언트 요청부터 DB까지 동작하는 로직

  • 웹 애플리케이션에 클라이언트 요청이 들어오면 EntityMangagerFactoryEntityManager 객체를 생성하게 됩니다.
  • 각각 생성된 EntityManger 객체는 DB 커넥션 풀과 연결되게 됩니다.

영속성 컨텍스트


  • 엔티티를 영구 저장하는 환경입니다.
  • DB에 저장하는 것이 아니라 영속성 컨텍스트 에 저장하는 것입니다.
  • EntityManager.persist(entity); 와 같이 엔티티를 EntityManager에 저장하여 사용할 수 있다는 것입니다.
  • 즉, 'EntityManager' 를 통해 영속성 컨텍스트 에 접근하는 것입니다.
  • 더 쉽게 이해하려면 EntityManager -> PersistenceContext 와 같이 1대1 로 쌍을 이루게 생성이 되고,
    이와 같이 접근을 하게 됩니다.
    하지만 여기서는 헷갈리므로 EntityManager를 영속성 컨텍스트라고 전제하고 설명을 하도록 하겠습니다.

엔티티의 생명주기


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

비영속

아래와 같이 JPA와 관련된 코드가 어디에도 없는 순수 자바 상태입니다.
즉, 영속 상태가 아닌 것입니다.

Member member = new Member();
member.setId("memebr1");
member.setUsername("회운1");

영속

이번에는 EntityManager(영속성 컨텍스트)에 em.persist(member)를 이용해 Member객체를 영속 상태로 만든 상태 입니다.
하지만 영속 상태를 만드는 em.persist(member);는 영속성 컨텍스트에 관리되도록 한 것 뿐이고, 실제 DB에 쿼리를 적용한 시점은 아닙니다.
즉, 영속 상태는 DB에 반영하는 시점이 아닙니다.
DB에 반영되는 시점은 flust()가 실행될 때 입니다.(commit() 메서드에 flust()가 있음.)

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

EntityManger em = emf.createEntityManager();
em.getTrasaction().begin();

// 객체를 영속성 컨텍스트에 저장한 상태(영속), DB에 반영되지 않음. 즉, 쿼리 안생김.
em.persist(member);

// 이 때 내부적으로 flush()가 실행 되면서, DB에 반영.
em.commit();

준영속

영속성 컨텍스트에 관리되는 객체를 관리되지 않도록 분리하는 것 입니다.
이 상태는 비영속 상태와는 다른 상태입니다.
em.detach(member)

삭제

em.remove(member); 를 사용하여, DB에서 지우는 작업입니다.
즉, 먼저 영속 상태가 되어야 가능하겠죠?

영속성 컨텍스트에 관리되면 뭐가 좋은데?


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

엔티티 조회, 1차 캐시

1차 캐시를 사용하기 때문에 조회 시 미세한 성능 향상이 있습니다.
하지만 그렇게 큰 장점은 아닙니다. 이유는 아래 같이 설명 하겠습니다.

위의 그림, 코드를 보면 em.persist() 를. 하면 영속성 컨텍스트에 저장하게 됩니다. 이 때 영속성 컨텍스트에 있는
1차 캐시에 @Id 로 설정한 값과 Entity(객체)자체를 값으로 가지게 됩니다.

이 때 조회를 하게 된다면 DB에 접근하는 것이 아니고 먼저 차적으로 1차 캐시에서 가져오게 됩니다.

만약 member2와 같이 1차 캐시에 없다면, DB에서 조회하여 1차 캐시에 반환을 하고 1차 캐시에 있는 값을 조회 가져오게 됩니다.
결국 find를 하게되면 영속 상태가 되는 것입니다. 즉, 다시 조회하게 되면 1차 캐시에서 가져오게 되므로 쿼리가 실행되지 않게 됩니다.

하지만 그렇게 큰 장점은 아닙니다.
어차피 요청 하나 당 하나의 DB 트랜잭션에 대해서만 관리되기 때문에 요청이 끝나면 삭제되게 되기 때문입니다.

영속 엔티티의 동일성 보장

만약 MyBatis로 완전 동일한 쿼리를 실행 시켜서 "member1"에 대한 값을 반환 받았다고 했을 때
findMember1, findMember2는 다른 주소를 가집니다. 결국 == 비교를 하였을 때 동일하지 않은 객체라고 판단 됩니다.
하지만 JPA는 1차 캐시에 저장된 것을 가져오는 것이기 때문에 컬렉션에 이미 있는 값을 가져오는 것과 같은 효과를 누릴 수 있습니다.
결국 findMember1과 findMember2는 동일성이 보장 됩니다.

그래서 이게 왜 이점이 되지??

동일성이 보장되지 않는다면 1차 캐시의 이점이 적용되지 않습니다.
1차 캐시는 이미 존재하는 객체는 DB를 통하지 않고 1차 캐시에서 가져오는 것인데
매번 다른 객체가 생성된다면 이는 1차 캐시에서 새로 생성해서 가져오는 것이기 때문입니다.
하지만 이 내용은 저의 추론입니다.

트랜잭션을 지원하는 쓰기 지연

그림으로 동작 원리를 살펴보도록 하겠습니다.

  1. memberA를 persist를 하면 쓰기 지연 SQL 저장소에 INSERT 쿼리를 쌓아 놓습니다.
    동시에 1차 캐시에 객체를 저장합니다.
  2. memberB를 persist를 하면 memberA와 동일하게 동작을 하게 됩니다.
    결국 쓰기 지연 SQL 저장소에는 memberA, memberB의 INSERT 쿼리가 들어있어 총 2개가 쌓여있게 됩니다.
  3. 이 단계 까지는 아직 DB에 반영이 안된 상태입니다.
  4. 마지막으로 transaction.commit() 을 하면, flush를 하는데 이 때, DB에 쿼리를 적용 시킵니다.
  5. 그리고 flush 후에 commit을 하게 되고, 최종적으로 DB에 반영이 되게 됩니다.

참고로 batch_size 라는 옵션으로 쿼리를 몇개 한방에 커밋할건지 결정할 수 있습니다.
즉, batch_size가 1이면, 쓰기 지연 저장소에 있는 쿼리를 하니씩 날려서 commit을 하기 때문에 아무래도 느릴테고,
batch_size가 10이면, 10개를 한번에 날리고 commit을 하기 때문에 빠를 수 있다는 의미 입니다.

변경 감지(Dirty Checking)

JPA의 목적은 자바 컬렉션을 다루듯 DB의 데이터를 다루는 것입니다.
예를 들어 자바 컬렉션에서 값을 가져와서 변경을 했는데 다시 컬렉션에 따로 저장을 하나요? 아닙니다.
하지만 DB관점에서 보면, member1을 조회해 와서 변경을 하려면 update 쿼리를 따로 해주어야 합니다.
이게 DB와 객체관점의 차이입니다.
그래서 JPA는 객체 중심적으로 DB 데이터를 컨트롤 하기 위해 변경 감지라는 기능이 적용 됩니다.
이런 원리는 영속성 컨텍스트에서 살펴 볼 수 있습니다. 그림으로 살펴 보겠습니다.

flush(플러시)


위에서 flush라는 단어를 많이 언급했습니다. 알아보도록 하겠습니다.

  • 변경 감지
  • 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
  • 쓰기 지연 SQL 저장소의 쿼리를 DB에 전송
    전송만 한 것이지 commit한 시점은 아님.

간단하게 정리 해보면, 영속성 컨텍스트의 쓰기 지연 SQL 저장소에 쌓인 쿼리를 DB에 전송하는 과정입니다.
즉, flush가 없으면 DB에 전송을 할 수 없는 것이며, DB에 commit된 것은 아닙니다.

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

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

  • Em.flush() - 직접 호출
  • em.commit() - 플러시 자동 호출
  • JPQL 쿼리 실행 - 플러시 자동 호출

준영속 상태


  • 영속 -> 준영속, 비영속과는 다르다.
  • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
  • 영속성 컨텍스트가 제공하는 기능을 사용 못함.
  • 하지만 거의 쓸 일은 없다. 하지만 아주 가끔 쓸 일이 있다고 함.
    예를 들어, 조회를 해서 영속상태가 된 엔티티를 준영속 상태로 만들어야 하는 특수한 상황일 때.

준영속 상태로 만드는 방법

  • em.detach(entity)
    특정 엔티티만 준영속 상태로 전환
  • em.clear()
    영속성 컨텍스트를 완전히 초기화
  • em.close()
    영속성 컨텍스트를 종료
반응형
복사했습니다!