영속성 관리

김민우·2022년 8월 27일
0

JPA

목록 보기
2/10

JPA에서 가장 중요한 2가지를 알아보자.

  • 객체와 관계형 데이터베이스 매핑하기
    (Object Relational Mapping)
  • 영속성 컨텍스트와 JPA 내부 동작방식 매커니즘

이 둘중 이번엔 영속성 컨텍스트와 JPA 내부 동작방식 매커니즘에 대해 자세히 알아보자.


EntityManagerFactory, EntityManager

  • EntityManagerFactory 를 미리 생성해 놓고 고객의 요청이 올 때 마다 EntityManager 을 생성한다.
  • EntityManager는 내부적으로 데이터 베이스 커넥션을 사용해서 DB를 사용하게 된다.

영속성 컨텍스트

JPA를 이해하는데 가장 중요한 용어다. 실제로 존재하는 개념이 아니라 추상적, 논리적 개념이여서 눈에 보이지는 않는다.
위의 EntityManager를 통해 영속성 컨텍스트에 접근을 한다.

쉽게 말하자면, EntityManager 안에 영속성 컨텍스트라는 눈에 보이지 않는 공간이 생긴다고 이해하자.

참고

EntityManager.persist(entity); 

DB에 저장한다는 뜻은 아니다. 영속성 컨텍스트를 통해서 이 entity를 영속화 한다는 뜻이다. 더 정확히 말하면 위의 persist 메소드는 DB에 저장하는 것이 아니라 entity 를 영속성 컨텍스트 라는 곳에 저장한다는 뜻이다.

J2SE 환경

  • EntityManager와 영속성 컨텍스트가 1:1 이다.

J2EE, 스프링 프레임워크 같은 컨테이너 환경

  • 엔티티 매니저와 영속성 컨텍스트가 N:1 이다.

엔티티의 생명주기

엔티티의 생명 주기는 크게 4가지로 구분된다.

  • 비영속 (new/transient)

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

  • 영속 (managed)

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

  • 준영속 (detached)

    영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제 (removed)

    삭제된 상태

하나씩 알아보자.

비영속 (new/transient)

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
// 객체를 생성만 하고 별도로 영속성 컨텍스트에 주입하지 않았다.
  • JPA와 관련이 없다.
  • 단순히 객체만 생성하는 것이므로 DB에 들어가지도 않는다.

영속 (managed)

// 객체 생성
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

// EntityManagerFactory를 통해 EntityManager 생성
EntityManager em = emf.createEntityManager();

// EntityManager를 통해 Transaction을 얻고 시작.
em.getTransaction().begin();

// 생성한 객체를 저장한 상태(영속)
em.persist(member);
  • 생성한 객체를 영속성 컨텍스트에 저장하여 영속성 컨텍스트에서 관리되는 상태이다.
  • DB에 저장이 되는 것 처럼 보이지만 저장되진 않는다.
    (이는 데이터 베이스 쿼리가 나가지 않는 것으로 확인이 가능하다.)

참고) DB에 저장되지 않는 것 확인

해당 코드를 실행해보자.

 // 영속
System.out.println("=== BEFORE ===");
em.persist(member);
System.out.println("=== AFTER ===");

<결과>

=== BEFORE ===
Hibernate: 
    call next value for MEMBER_SEQ_GENERATOR
=== AFTER ===
Hibernate: 
    /* insert hellojpa.Member
        */ insert 
        into
            Member
            (name, id) 
        values
            (?, ?)
  • ===AFTER===이 출력된 후 INSERT QUERY가 출력되는 것을 볼 수 있다.
  • 이를 통해 persist() 에선 DB에 저장되는 것이 아닌 영속성 컨텍스트에 저장되는 것을 알 수 있다.
  • transactioncommit 하는 시점에 영속성 컨텍스트에 있는 것이 DB에 쿼리로 날라가게 된다.

준영속

//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);
  • 영속성 컨텍스트에 있는 객체를 영속성 컨텍스트에서 분리하는 것이다.
    (영속 -> 준영속)
  • 영속성 컨텍스트에서 관리되지 않지만 DB에는 존재한다.
  • 즉, JPA에서 관리를 안한다.
  • 당연히 영속성 컨텍스트가 제공하는 기능을 사용하지 못한다.

참고) 준영속 -> 영속
em.find()를 했는데 엔티티 객체가 영속성 컨텍스트에 없고 DB에 있으면 1차 캐시에 등록이 되므로 영속 상태가 된다.

준영속 상태로 만드는 방법

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

이러한 준영속 상태는 나중에 복잡한 웹 어플리케이션을 개발할 때 자세히 알 수 있다.

삭제

 //객체를 삭제한 상태(삭제)
 em.remove(member);
  • 완전히 삭제된 상태
  • DB에 있는 데이터를 지우겠다 라는 의미

영속성 컨텍스트의 이점

지금까지 어려운 개념인 영속성 컨텍스트를 알아봤다. 알아보면서 굳이 이걸 왜쓰는지에 대한 의문이 들 것이다. 영속성 컨텍스트를 사용하면 많은 이점이 있기 때문이다.

영속성 컨텍스트의 이점은 크게 5가지로 볼 수 있다.

  • 1차 캐시

  • 동일성(identity) 보장

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

  • 변경 감지(Dirty Checking)

  • 지연 로딩(Lazy Loading)

하나씩 알아보자.

엔티티 조회, 1차 캐시

여기서는 EntityManager 자체가 영속성 컨텍스트라 생각해자.
(물론 미묘한 차이는 존재하지만 일단은 이렇게 생각하자.)

내부에는 1차 캐시라는 것이 존재한다. (이 1차캐시를 영속성 컨텍스트라 이해해도 되긴한다... 아오 복잡해)

  • 1차 캐시 형태는 다음과 같다.
    • key : pk값
    • value : 객체

  • 이러한 1차 캐시는 조회할 때 주로 사용된다. em.find() 메소드로 조회를 하면 JPA는 우선적으로 DB가 아닌 1차 캐시에서 pk값(key)를 통해 객체를 조회한다. 그래서 캐시에 해당 값이 있으면 그 값을 그대로 가져온다.

  • 그러나 찾는 값이 1차 캐시에 없고 DB에 있으면 어떻게 될까? 그러면 DB에서 찾고 그 값을 1차 캐시에 저장을 한 후 리턴을 한다.
  • 재조회를 하면, 이 값은 1차 캐시에 저장이 되었으므로 DB까지 가지 않는다.

참고
코드를 통해 조회 후 출력하는 과정에서 데이터베이스의 셀렉트 쿼리가 출력되지 않음을 확인할 수 있다.

이것이 성능적인 측면에서 크게 도움은 안된다. EntitiyManager(영속성 컨텍스트)라는 것은 고객의 요청이 올 때 마다 생성되는 것이므로 고객의 요청이 와서 비지니스 로직을 실행 후 끝나면 사라지기 때문이다. 이 때 1차 캐시또한 날아간다.
따라서, 1차 캐시는 찰나의 순간에서만 존재하기 때므로 크게 도움이 되지 않는다.

물론, 비지니스가 매우 복잡한 경우 쿼리가 줄어들어서 이점이 있다. 그러나 계속 언급하지만 크게 성능적인 면에서 도움은 주지 않는다.
성능적인 이점보다 컨셉이 주는 이점이 있다. (좀 더 객체지향적으로 코드를 작성할 수 있다거나...)

동일성 보장

자바의 컬렉션에서 같은 참조인 객체를 2번 꺼내온다고 생각해보자. 꺼내온 두 객체의 참조는 같을 것이다.
JPA 또한 마찬가지이다. em.find()에서 같은 pk값으로 객체를 두 번 꺼내온다고 하면 그 두개의 참조는 같다. 이를 영속성의 동일성을 보장해준다라고 한다.

즉, 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭 션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다.

이를 코드로 확인하면 다음과 같다.

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");
System.out.println(a == b); // 동일성 비교 true
  • 두 객체 a, b는 서로 같은 참조임을 알 수 있다.

이는 별거 아닐 수 있지만 JPA는 이러한 DB안의 값들을 자바의 컬렉션처럼 활용할 수 있게 해준다는 점에서 의미가 있다고 할 수 있다.

엔티티를 등록할 때 트랙잭션을 지원하는 쓰기 지연

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.

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

em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
  • persist() 하면 1차 캐시에 값이 저장되는 동시에 JPA가 해당 entity 를 분석 후 INSERT QUERY를 생성 후 별도의 저장소(쓰기 지연 SQL 저장소)에 쌓아둔다. (Query를 DB에 보내진않는다.)
  • 그 후, commit()하면 INSERT QUERY를 보내어 DB에 데이터가 저장된다.
  • 즉, 엔티티를 등록할 때 저장될 데이터를 한 번에 넘긴다.

그림으로 자세히 확인해보자.

  • 객체가 1차 캐시에 저장되고 INSERT QUERY가 쓰기 지연 SQL 저장소라는 별도의 저장소에 저장된다.

  • commit()하는 순간 쓰기 지연 SQL 저장소에 있던 모든 INSERT QUERY가 DB로 전달된다. (이를 JPA에선 flush라 한다)
  • 그리고 실제 DB가 commit된다.

이를 통해 버퍼링이라는 기능을 사용할 수 잇다. persist()를 할 때 마다 바로 DB로 값을 넘기면 뭔가 최적화할 수 있는 여지 자체가 없어진다.

참고
하이버네이트는 최대 모아둘 수 있는 데이터양을 별도로 설정할 수 있다.

중요
JPA는 기본적으로 리플렉션같은 기능이나 프록시 객체등을 내부적으로 사용하기 때문에 동적으로 객체를 생성해야 하는 경우가 존재한다.
따라서, 엔티티 객체는 디폴트 생성자가 반드시 존재해야 한다.

변경 감지

JPA는 DB에 접근하는 방식이 마치 자바의 컬렉션에 접근하는 방식과 유사하다. (동일성 보장에서도 확인할 수 있었다.)

우리가 자바의 컬렉션의 값을 수정하기 위해선 참조를 가져와 값을 수정만 하면 됬다. 별도로 저장하는 로직은 필요가 없었다. 이는 JPA에서 DB에 저장된 값을 바꿀때도 마찬가지다.

단순히 값을 꺼내와서 수정만 하고 별도로 저장하지 않아도 된다. (update() 메소드를 호출할 필요없다.)

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에 수정된 값이 저장된다.

값이 저장되는 과정을 그림으로 살펴보자.

  • JPA는 트랜잭션을 commit을 하면 내부적으로 flush가 호출된다.
  • flush는 엔티티와 스냅샷을 비교한다.(1차 캐시에는 key와 value외에 스냅샷이라는 또다른 속성이 별도로 존재)
  • 스냅샷에는 최초로 값을 읽어온 시점의 객체 상태가 저장된다. 그리고 flush가 호출되는 시점에 스냅샷과 엔티티를 일일히 비교했을 때 두개가 다르면 UPDATE QUERY를 쓰기 지연 SQL 저장소에 저장한다. 그 후, DB에 반영을 하고 commit 한다. 진짜 신기하다..

엔티티를 변경할 때 update() 메소드를 별도로 호출하지 않으면 개발자의 실수를 줄일 수 있어 효율적이다.

엔티티 삭제

DB에 저장된 데이터를 완전히 삭제한다. 작동 원리는 변경 감지(Dirty Checking)과 같다.

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

em.remove(memberA); //엔티티 삭제

플러시

영속성 컨텍스트의 이점을 살펴보면서 flush를 언급했다. 대충 이것의 의미를 파악할 수 있었을 것이다.

플러시는 영속성 컨텍스트의 변경내용을 데이터 베이스에 반영한다.
(쓰기 지연 SQL 저장소에 값들을 DB에 보낸다)

플러시 발생 시

플러시가 발생하면 다음과 같은 로직이 실행된다.

  1. 변경 감지
  2. 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
  3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송
    (등록, 수정 삭제 쿼리가 들어가 있음)
  4. 1차 캐시를 지우는 것이 아니다. 오직 영속성 컨텍스트에 있는 쓰기 지연 SQL 저장소에 있는 것들이 DB에 반영(동기화)한다.

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

  • em.flush()

    직접 호출
  • 트랜잭션 커밋

    플러시 자동 호출 (지금까지 했던 방식)

  • 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은 SQL로 번역이 되서 실행이된다. (즉, DB에서 가져올 것이 없다.)
따라서, 문제 발생을 방지하기 위해 플러시가 자동으로 호출이 된다.
어려운 개념이니 일단은 이정도로 간단하게만 이해하자.

플러시 모드 옵션

em.setFlushMode(FlushModeType.COMMIT)
  • FlushModeType.AUTO
    커밋이나 쿼리를 실행할 때 플러시 (기본값)
  • FlushModeType.COMMIT
    커밋할 때만 플러시

거의 도움이 되지않아 쓸 일이 없다.

정리

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

참고
JPA는 기본적으로 어떤 데이터를 맞추거나 동시성에 관련된 것들은 다 DB 트랜잭션에 위임해서 한다.

0개의 댓글