BackEnd/JPA

[JPA] 영속성 관리 - 내부 동작 방식

샤아이인 2022. 4. 2.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

1. 영속성 컨텍스트 1

엔티티는 테이블과 매핑되는 하나의 클래스이다.

 

우선 엔티티 메니저 팩토리를 통해 엔티티 매니저를 생성하는 과정을 살펴봅시다.

출처 - 인프런 김영한 JPA

EntityManagerFactory는 어플리케이션 로딩 시점에 딱 하나만 생성되어야 한다.

 

이후 사용자의 요청이 들어올때마다 EntityManager를 생성하게 됩니다. 고객의 요청이 들어오면 EntityManager를 생성하고, 요청에 의한 쿼리문이 다 작업이 끝나면 다시 close() 하기를 반복합니다.

(EntityManager는 Thread 공유를 하면 안됩니다!)

 

EntityManager는 커넥션 풀(connection pool)을 형성하여, 미리 커넥션이 연결되어 있는 pool을 사용해 DB를 핸들링 합니다.

 

● 영속성 컨텍스트란?

- "엔티티를 영구 저장하는 환경" 이라는 뜻을 갖고있다.

 

- EntityManager.persist(entity)

persist를 하면 DB에 저장한다기 보다는 영속성 컨텍스트를 통해 엔티티를 영속화 한다는 의미이다.

이는 영속성 컨텍스트에 엔티티를 저장한다는 의미이다. 이후 commit을 진행하면 비로소 DB에 저장되게 된다.

 

- 영속성 컨텍스트는 논리적인 개념이다. => 눈에 보이지 않는다.

 

- 엔티티 매니저를 통해서 영속성 컨텍스트에 접근하게 된다.

 

예를 들어 J2SE 와 같은 환경에서는 EntityManager안에 영속성 컨텍스트가 1:1로 맵핑된다고 생각하면 된다.

출처 - 인프런 김영한 JPA

 

● 엔티티의 생명주기

비영속 (new / transient) 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태, 최초 생성된 객체의 상태

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

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

삭제 (removed) 삭제된 상태

출처 - 인프런 김영한 JPA

ps. find() 도 영속화부터 해야한다. 일단 DB에서 영속성 컨텍스트로 불러와야 조회를 할 수 있다.

 

● 비영속

출처 - 인프런 김영한 JPA

그냥 객체가 딱 생성된 시점의 상태가 비영속 상태이다.

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

 

● 영속

출처 - 인프런 김영한 JPA

객체가 생성된 후, persist(member)를 통해 영속화 된 상태이다. 코드로 보면 다음과 같다.

// 비영속
Member member = new Member();
member.setId(100L);
member.setName("HelloJPA");

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

// 영속화
entityManager.persist(member);
 

하지만 아직 DB에 저장되지는 않았다!

트랜잭션을 commit 하는 시점이 되서야 영속성 컨텍스트에 있던 data가 DB에 쿼리로 넘어가게 된다.

 

● 준영속, 삭제

//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);

//객체를 삭제한 상태(삭제)
em.remove(member);
 

2. 영속성 컨텍스트 2 (영속성 컨텍스트의 이점)

그럼 이러한 영속성 컨텍스트를 사용하게 됨으로써 얻는 이점은 무엇일까? 지금부터 알아보자!

● 1차 캐시와 엔티티 조회

 

1) 1차 캐시

영속성 컨텍스트 안에는 다음과 같이 1차 캐시가 있다.

출처 - 인프런 김영한 JPA
// 엔티티를 생성한 상태 (비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

// 1차 캐시에 저장됨
em.persist(member);
 

객체를 생성한후(비영속 상태), persist(member)를 통해 영속화 시켰다.

이때 1차 캐시에 member가 key : value 쌍으로 저장되게 된다.

key는 PK에 해당되고, value는 Entity에 해당된다.

 

이후 find 매서드를 통해 다시 맴버를 찾아와 보자. 코드는 다음과 같다.

// 1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");

transaction.commit(); // 커밋
 

1차 캐시를 통하여 우선적으로 조회한 후, 이후 commit 이 진행된다.

따라서 member는 1차 캐시에서 찾아오게 된다. DB에 접근하여 찾아오는 것 이 아니다!

당연히 member를 찾아오는 쿼리 또한 생성되지 않는다. 1차 캐시에서 찾아왔기 때문이다.

 

이후 commit()을 했기 때문에 이때 비로소 insert 쿼리문이 생성된다.

다음 사진을 보면 찾아온 member의 정보를 먼저 출력한후, insert 쿼리문이 출력된다.

만약 조회 했는데 1차 캐시에 member가 없다면? 어떤 방식으로 처리할까?

조회 시 영속 컨텍스트 안에서 1차 캐시를 조회 후 해당 엔티티가 없을 경우 데이터베이스에서 조회 해 온다.

조회한 데이터를 1차 캐시에 저장한 후, 이를 반환해 준다.

출처 - 인프런 김영한 JPA

데이터베이스 트랜잭션 범위 안에서 만들고 종료되기 때문에, 하나의 비즈니스 로직이 종료될 경우 1차캐시는 다 사라진다.

따라서 성능 개선에 큰 도움이 되지는 않는다. (비즈니스 로직이 복잡하다면 성능 개선의 효과가 있다.)

 

2) 영속성 엔티티의 동일성 보장

우선 다음 코드를 살펴보자.

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

System.out.println(a == b); //동일성 비교 true
 

같은 id를 사용해 find로 1차 캐시에서 갖어온 Member는 동일하다.

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

 

예를 들어 다음 코드를 살펴보자.

101L 라는 id값으로 member를 2번 조회하고 있다. 만약 1차 캐시가 없었다면, 동일한 SELECT 쿼리가 2번 만들어졌어야 한다.

 

하지만 위에서 보듯 처음 가져올때만 SELECT 쿼리를 날려 가져오고, 이를 1차 캐시에 저장해 둔다.

두번째 find() 메서드가 호출될때는 1차 캐시에서 가져오기 때문에 SELECT 쿼리를 만들지 않는다.

 

즉, 두 find() 메서드 호출 모두 동일한 member를 찾아오게 되는것 이다.

 

3) 엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연

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

//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작

Member memberA = new Member(150L, "A");
Member memberB = new Member(160L, "B");

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

//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋
 

2개의 맴버 memberA, memberB를 persist()할때는 1차 캐시에 저장된다. 아직 commit()을 하지 않았기 때문에 DB에 저장되지는 않는다.

 

대신 persist()를 호출할때 미리 해당되는 INSERT 쿼리를 만들어 쓰기 지연 SQL 저장소 에 저장해 둔다.

다음 그림으로 살펴보자.

출처 - 인프런 김영한 JPA

위의 그림에서는 memberA 와 memberB는 둘다 1차 캐시에 저장되면서, 쓰기 지연 SQL 저장소에 INSERT문이 생성되어 저장된다.

memberA를 INSERT하는 쿼리와 memberB를 INSERT하는 쿼리 총 2개가 생성되어 쓰기 지연 SQL 저장소에 저장된다.

 

이후 commit()을 호출하는 시점이 되어서야 비로소 쓰기 지연 SQL 저장소에 있던 쿼리문이 flush되어 DB에 저장하게 된다.

출처 - 인프런 김영한 JPA

- 왜 쓰기지연을 사용하는가?

버퍼링 처럼 한번에 모아서 write할수 있다는 장점이 있다. 쿼리를 여러번 날리지 않기 때문에 성능 최적화에 매우 중요한 내용이다.

<property name="hibernate.jdbc.batch_size" value="10"/> 를 통해 10개씩 쌓일 때 마다 적용하게 하는 기능을 사용할수 있다.

 

4) 엔티티 수정 - 변경 감지(Dirty Checking)

다음 코드를 수행하기전 ID 150 의 NAME은 "A" 이다.

transaction.begin();

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

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

// em.update(member) 와 같은 코드가 있어야 하지 않을까??????????

transaction.commit();
 

위 코드를 보면 find로 findMember를 찾아 온 후, setter를 사용하여 데이터를 수정하고 있다.

수정한 후에 update(member)와 같이 변경 사항을 저장해줘야 하지 않을까?

=> 아니다, update() 같은것은 필요없다. 그냥 commit하면 다음과 같이 update문이 생성된다.

위 코드를 실행한 결과는 다음과 같다.

ID 150 의 NAME이 "ZZZZZ"로 변경되었음을 확인할수 있다.

 

이를 이해하기 위해 변경 감지 기능에 대하여 알아보자.

출처 - 인프런 김영한 JPA

맨 처음 1차 캐시에 Entity가 저장될때(최초에 영속성 컨택스트에 들어왔을때) 스냅샷 이라는 기능을 통해 원본을 저장해 둔다.

 

이후 member의 데이터를 수정한후, commit을 호출하면 이 시점에 스냅샷과 변경된 member간에 차이가 있는지 비교한다.

원본 스냅샷과 차이가 있다면 이를 적용한 update 쿼리를 쓰기 지연 SQL 저장소에 만들어 저장해둔다.

이후 flush되면서 변경사항을 DB에 반영한다.

 

(JPA는 값을 바꾸면 트랜잭션이 commit되는 시점에 변경을 반영한다)

 

5) 엔티티 삭제

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

3. Flush

플러시는 영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 작업이다. (sync)

 

● 플러시 발생시 일어나는 일

1. 변경사항 감지하기 (Dirty Checking)

2. 수정된 엔티티에 대한 쿼리를 쓰기 지연 SQL 저장소에 등록하기

3. 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.

 

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

1. em.flush() : 직접 호출

2. 트랜잭션 커밋: 플러시 자동 호출

3. JPQL 쿼리 실행: 플러시 자동 호출

=> JPQL 쿼리 실행시 자동으로 Flush가 호출되는 이유는?

em.persist(memberA);
em.persist(memberB);
em.persist(memberC); // 아직 DB에 insert 안함

//중간에 JPQL 실행
query = em.createQuery("select m from Member m ", Member.class) // DB에 조회하려 든다.
List<Member> members = query.getResultList();
 

위와 같은 상황을 보면, persist로 memberA, memberB, memberC 를 1차 캐시에 저장해 둔다. 아직 DB에 반영되지 않았다.

이후 JPQL이 실행되면서 Member 테이블로 부터 데이터를 찾아오려 하면 데이터가 없기 때문에 문제가 된다.

 

따라서 JPQL은 무조건 Flush가 선행으로 자동 호출 된다.

우선적으로 Flush가 호출되어 1차 캐시와 DB간의 동기화 과정을 마친후, 원하던 JPQL이 실행되어 DB로부터 데이터를 얻어온다.

 

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

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

- 트랜잭션이라는 작업 단위가 중요하다. → 커밋 직전에만 동기화를 하여 DB에 변경사항을 날려주면 된다.

 

4. 준영속 상태 (Detach)

1차 캐시에 저장되어 영속 상태인 Entity가 영속성 컨텍스트에서 분리되는 것 이다.

이렇게 되면 영속성 컨텍스트가 제공하는 기능을 사용하지 못하게 된다.

 

● 준영속 상태로 만드는 방법

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

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

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

 

예시로 detach()를 알아보자.

transaction.begin();

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

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

// 준영속화
em.detach(findMember);

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

위 코드를 실행하면 맨처음 find()해올때 member를 DB에서 찾아오기 위해 SELECT 문이 수행된다.

찾아온 member는 1차 캐시에 저장되면서 영속화 된다.

 

이후 detach()가 호출되어 member가 준영속 상태가 된다.

따라서 commit()을 호출되어 Flush가 진행되어도 1차 캐시에 저장된 내용이 없어 끝나게 된다.

 

"============" 이후에 다른 SQL을 확인할수가 없다.

준영속화가 되지 않았다면 member의 NAME을 변경했기 때문에 UPDATE 쿼리문이 "============" 이후에 보였어야 한다.

하지만 detach 되었기 때문에 1차 캐시는 비어있게 되고, 다음과 같이 보이는것 이다.

'BackEnd > JPA' 카테고리의 다른 글

[JPA] 프록시와 연관관계 관리  (0) 2022.04.06
[JPA] 고급 매핑  (0) 2022.04.05
[JPA] 다양한 연관관계 매핑  (0) 2022.04.04
[JPA] 연관관계 매핑 기초  (0) 2022.04.03
[JPA] 엔티티 매핑  (0) 2022.04.02

댓글