공부/JPA

JPA 영속성 관리

Stair 2024. 11. 28. 10:33
반응형

JPA에서 가장 중요한 2가지

1. 객체와 관계형 데이터베이스 매핑하기(ORM : Object Relational Mapping)

2. 영속성 컨텍스트

 

JPA 내부 구조

JPA의 내부 구조를 이해하기 위해서는 영속성 컨텍스트에 대해 이해해야 한다.

https://surrealcode.tistory.com/101

 

JPA 설정하기

기본 세팅은 아래 블로거가 정말 잘 요약해놓은 것이 있다. 이걸 참고하자https://velog.io/@chosj1526/JPA-Hello-JPA-프로젝트-생성 [JPA] Hello JPA 프로젝트 생성이 강의는 인프런 김영한님의'자바 ORM 표준 JP

surrealcode.tistory.com

이전에 확인하였듯이 웹 어플리케이션은 persistence를 통해 emf를 한개 생성후 요청이 올때마다 em을 생성하여 처리를 해준다.

 

그럼 영속성 컨텍스트란 대체 뭘까?

 

영속성 컨텍스트는 엔티티를 영구 저장하는 환경 이라는 뜻이다.

ex) EntityManager.persist(entity);     or     em.persist(entity);

 

영속성 컨텍스트는 논리적인 개념이다. 우린 EntityManager를 통해 영속성 컨텍스트에 저장한다.

사실 EntityManager를 생성하게 되면 그 안에 1:1로 영속성 컨텍스트가 생성이 된다. 즉 엔티티 매니저 안에 영속성 컨텍스트라는 공간이 생기는 것이다

 

엔티티의 생명주기

우선 엔티티의 생명 주기는 다음과 같다.

1. 비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태

2. 영속(merged) : 영속성 컨텍스트에 관리되는 상태

3. 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태

4. 삭제(removed) : 삭제된 상태

 

 

비영속(new/transient) : 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태 이다.

Member member = new Member();
member.setId("member1");
member.setName("userA");

와 같이 영속 컨텍스트와 아무 상관 없는 상태이다.

 

 

영속(merged) : 영속성 컨텍스트에 관리되는 상태

영속 상태는 맴버 객체를 생성한 다음 엔티티 매니저를 얻어와 이 엔티티 매니저에 persist해서 멤버 객체를 집어넣으면

EntityManager안에 있는 영속성 컨텍스트에 member 객체가 들어가면서 영속 상태가 된다.

//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId(111L);
member.setName("userA");

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

//객체를 저장한 상태(영속)
em.persist(member);

** 참고 : persist한다고 해서 DB에 바로 저장되는 것은 아니다. 트랜잭션을 커밋하는 시점에 DB에 저장된다.

준영속 상태는 em.detach(member); 한 상태이고

삭제는 em.remove(member);를 통해 삭제될 수 있다.

 

 

위에서 보았듯이 애플리케이션과 데이터 베이스에 중간 계층이 하나 있는 것을 알 수 있다.

이것이 영속성 컨텍스트이다. 영속성 컨텍스트의 장점과 같다.

1. 1차 캐시

2. 동일성(Identity) 보장

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

4. 변경 감지(Dirty Checking)

5. 지연 로딩(Lazy Loading)

 

 

1차 캐시

영속성 컨텍스트는 내부에 1차 캐시를 들고 있다.

Member member = new Member();
member.setId("member1");
member.setName("Hello");

em.persist(member);

 

1차 캐시에는 @Id로 정의한 PK와 Entity객체의 값을 가지고 있다.

만약 이 상태에서 em.find("member1')을 찾게 되면 DB를 찾지 않고 1차 캐시를 우선적으로 찾게 된다. 캐시에 값이 있으면 그대로 조회해 온다.

만약 em.find("member2")를 조회하는데 1차 캐시에 없다면, DB를 조회하여 member2를 1차 캐시에 저장하고 반환한다.

try {
    Member member = new Member();
    member.setId(112L);
    member.setName("Hello");

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

    Member findMember = em.find(Member.class, 112L);
    System.out.println("findMember.getId() = " + findMember.getId());
    System.out.println("findMember.getName() = " + findMember.getName());

    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}

 

결과를 확인해보면 

===BEFORE===
===AFTER===
findMember.getId() = 112
findMember.getName() = Hello
Hibernate: 
    /* insert for
        hellojpa.Member */insert 
    into
        Member (name, id) 
    values
        (?, ?)

 

select 쿼리가 나가지 않는다. DB에서 찾지 않고 1차 캐쉬에서 조회했기 때문이다.

try {

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


    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}

동일한 PK를 가진 값을 두번 조회할때도 마찬가지이다. 1차 캐시에 없기때문에 DB에서 조회하느라 처음엔 select 쿼리가 나가지만, 두번째로 찾을 땐 1차 캐시에 있기 때문에 select 쿼리가 나가지 않고, 1차 캐시에 있는걸 찾아온다.

Hibernate: 
    select
        m1_0.id,
        m1_0.name 
    from
        Member m1_0 
    where
        m1_0.id=?

 

 

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

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

System.out.println("result = " + (findMember2==findMember1));

result = true

== (동일성 비교)를 해보면 true 가 나오는 것을 알 수 있다.

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

 

 

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

JPA는 커밋 직전까지 SQL을 DB에 보내지 않고 쌓아둔다. 만약 커밋이 이루어지면 한번에 SQL쿼리를 날린다.

persist를 하면 SQL 쿼리를 생성해서 쓰기 지연 SQL 저장소에 쌓아둔다. 이 쌓아둔 SQL은 tx.commit()을 하는 시점에 DB에 날아간다.

try {

    Member member1 = new Member(250L, "A");
    Member member2 = new Member(260L, "B");

    em.persist(member1);
    em.persist(member2);

    System.out.println("=====================");

    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}

위 코드의 결과를 확인해보면 다음과 같다.

 

=====================
Hibernate: 
    /* insert for
        hellojpa.Member */insert 
    into
        Member (name, id) 
    values
        (?, ?)
Hibernate: 
    /* insert for
        hellojpa.Member */insert 
    into
        Member (name, id) 
    values
        (?, ?)

 

persist 시점에 쿼리가 날아가는 것이 아닌 commit()시에 insert 쿼리 두개가 날아가는 것을 볼 수 있다.

만약 persist할때마다 DB에 쿼리가 날아가게 되면 최적화 할 수 있는 여지 자체가 없다.

 

 

변경 감지(Dirty Checking)

JPA는 update할 때 persist를 할 필요가 없다.

//영속 엔티티 조회
Member member = em.find(Member.class, 150L);

//영속 엔티티 데이터 수정
member.setName("ZZZZZ");

//em.persist(member); //이런 코드가 필요하지 않을까?

 

 

Hibernate: 
    select
        m1_0.id,
        m1_0.name 
    from
        Member m1_0 
    where
        m1_0.id=?
=====================
Hibernate: 
    /* update
        for hellojpa.Member */update Member 
    set
        name=? 
    where
        id=?

 

persist가 없어도 update가 진행된다.

사실 1차캐시엔 스냅샷이라는게 있는데, 스냅샷이란 내가 값을 읽어온 최초 시점의 상태를 스냅샷으로 떠둔다.

JPA가 트랜잭션을 커밋하는 시점에 내부적으로 플러쉬를 호출하며 엔티티와 스냅샷과 비교를 하고, 그 상태에서 멤버의 값에 변경사항이 있으면, 업데이트 쿼리를 쓰기 지연 SQL 저장소에 만들어 둔다.

 

 

플러쉬(flush())

플러쉬는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.

플러쉬는 데이터베이스 트랜잭션이 커밋되면 자동으로 플러쉬가 발생한다.

 

플러쉬가 하는 일은 다음과 같다.

1. 변경 감지

2. 수정된 엔티티 쓰기 지연 SQL 저장소에 등록

3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제 쿼리)

 

영속성 컨텍스트를 플러쉬 하는 방법은 어떻게 될까?

em.flush()로 직접 호출을 하거나(테스트 외에 거의 쓰지 않음)

tx.commit()으로 플러쉬를 자동 호출 하거나

JPQL 쿼리를 실행하여 자동으로 호출하는 방법이 있다.

 

 em.flush()를 사용해보자.

try {

    Member member = new Member(2000L, "member2000");
    em.persist(member);

    em.flush();

    System.out.println(" ================== ");

    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}

Hibernate: 
    /* insert for
        hellojpa.Member */insert 
    into
        Member (name, id) 
    values
        (?, ?)
 ================== 

 

이전에는 ==== 이후 commit이 될 때 insert 쿼리가 날아갔지만. 현재는 em.flush() 메서드에 의해 ==== 전에 쿼리가 날아가는 것을 볼 수 있다.

 

혹시 플러쉬를 하게 되면 1차 캐시가 다 지워질까?

정답은 No다. flush는 오직 쓰기지연 SQL저장소에 있는 쿼리들이 DB에 반영이 되는 과정일 뿐이다.

 

JPQL은 왜 쿼리를 실행할 떄 플러쉬가 자동으로 호출되는 걸까?

아래와 같은 코드가 있다고 가정하자.

em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
query = em.createQuery("select m from Member m", Member.class);
List<Member> members = query.getResultList();

persist를 하고 나서 JPQL을 실행하는 쿼리이다.

JPQL이 플러쉬를 호출하지 않는다고 가정하면 문제가 하나 발생하는데, persist는 SQL 쿼리를 생성해서 쓰기 지연 SQL 저장소에 쌓아두는 역할만 할 뿐 아직 DB에는 데이터가 저장되지 않아, JPQL실행시 조회되지 않는 문제가 발생한다.

그렇기 때문에 JPQL을 쿼리 실행 시엔 시작 지점에 자동으로 플러쉬가 호출되어 DB에 저장하고 JPQL쿼리를 사용할 수 있게 해주는 것이다.

 

정리

- 플러시는 영속성 컨텍스트를 비우지 않는다.

- 영속성 컨텍스트의 변경내용을 데이터베이스에 동기화 한다.

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

 

 

준영속 상태

앞서 보았듯이 비영속 상태의 객체를 persist를 하여 영속상태로 변경하는 것을 볼 수 있었다. 준영속 상태는 영속 상태의 엔티티가 영속성 컨텍스트에서 분리하는 것이다.

 

영속 -> 준영속

- 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)

- 영속성 컨텍스트가 제공하는 기능을 사용 못함

 

준영속 상태로 만드는 방법은 여러가지가 있다.

1. em.detach(entity) - 특정 엔티티만 준영속 상태로 전환

2. em.clear() - 영속성 컨텍스트를 완전히 초기화

3. em.close() - 영속성 컨텍스트를 종료

 

em.detach 예제를 보자.

try {

    Member member = em.find(Member.class, 150L);
    member.setName("AAAAA");

    em.detach(member);
    //em.clear();
    //

    System.out.println(" ================== ");

    tx.commit();
} catch (Exception e) {
    tx.rollback();
} finally {
    em.close();
}

Hibernate: 
    select
        m1_0.id,
        m1_0.name 
    from
        Member m1_0 
    where
        m1_0.id=?
 ================== 

 

데이터를 변경한 후 detach를 했기 때문에 영속성 컨텍스트에서 더이상 관리를 하지 않아, select만 되고 update 쿼리가 날아가지 않는걸 볼 수 있다.

반응형

'공부 > JPA' 카테고리의 다른 글

연관관계 매핑 기초  (0) 2024.12.02
엔티티 매핑  (0) 2024.11.30
JPA 설정하기  (0) 2024.11.26
JPA란?  (0) 2024.11.25
애플리케이션 구현(도메인 개발)  (0) 2024.11.21