본문 바로가기
Back-end/JPA 개념

3. JPA의 내부구조와 동작

by devraphy 2022. 3. 24.

0. 개요

- 이전 포스팅에서 기본적인 JPA의 구조에 대해서 알아보았다.

- 이번 포스팅에서는 JPA가 왜 이러한 기본구조를 갖는지, 그 내부구조와 동작은 어떻게 되는지 알아보자. 

 

1. JPA의 핵심

- JPA는 OOP와 RDB의 관계 표현 형식을 서로가 이해할 수 있도록 매핑하는 것이 주요 기능이다.

- 즉, OOP 형식으로 짜인 Java 코드를 RDB가 이해할 수 있는 형식으로 번역되어야 한다.

 

- 그러므로 이를 위해서는 다음 2가지를 구현해야 된다.

    → 객체를 이용한 RDB 형식의 매핑(= 구조적 특징)

    → 객체를 이용한 RDB 매핑을 가능하게 하는 내부구조 및 흐름(= 내부 동작 원리)

 

- 위의 2가지 특징을 JPA에서는 어떻게 구현하는지 차근차근 알아보자.

 

2. Persistence Context

- 우선 영속성 콘텍스트의 개념에 대해서 간단히 알아보자.

 

- 영속성 콘텍스트란, JPA의 내부 흐름 또는 동작 원리 그 자체를 의미한다.

- 그러므로 하나의 기술이나 기능이라기보다는 논리적인 개념에 가깝다.

 

- 흔히 영속성 콘텍스트를 설명할 때 다음과 같이 표현한다. 

   → Entity를 영구적으로 저장하는 것이다.  (* 영구적으로 저장한다 == DB에 저장한다.)

 

- 그러나 영속성 콘텍스트는 단순히 DB에 객체 데이터를 저장하는 것 이상의 의미를 가진다.

- 왜냐면 DB에 저장하는 행위는 Entity를 영속화시키기 위한 하나의 방법에 불과하기 때문이다.

- 즉, 개념적으로 접근하자면 영속성 콘텍스트의 진정한 의미는 Entity를 영속화시키는 것이다. 

 

- 아직 무슨 말인지 잘 모르겠다. 그러므로 JPA가 어떻게 동작하는지 하나씩 알아가 보자.

 

3. Entity & Entity Manager

a) Entity

- Entity는 DB에 존재하는 하나의 Table을 클래스로 구현한 것이다.

- 쉽게 말해서, DB Table의 column과 대응하는 속성을 가진 클래스를 의미한다.

- 참고로 Entity 객체를 Entity라고 부른다.  

 

b) Entity Manager

- Entity Manager는 위에서 설명한 Entity의 구현체(= 객체)를 생명주기(= Lifecycle)를 관리한다.

- Entity Manager는 Entity 객체를 영속성 콘텍스트(persistence context)에 저장하여 관리한다.

- 즉, Entity와 Persistence Context 사이에 존재하는 역할이다.

 

4. Entity Lifecycle

- Spring Bean에도 생명주기가 있듯이 Entity 또한 생명주기를 가진다.

- Entity의 생명주기는 다음과 같다.

 

a) Transient(= new, 비영속 상태)

  → Entity가 영속성 콘텍스트와 전혀 관계가 없는 상태

  → Entity가 생성되고 Entity Manager에게 등록되지 않은 상태를 말한다.

public class Jpa {
   public static void main(String args[]) {
   
      User userA = new User();
      userA.setId("userA");
      userA.setName("김철수");
   }
}

 

b) Managed(= 영속 상태)

  → Entity가 영속성 콘텍스트에 의해 관리되는 상태

  → 즉, Entity가 영속성 콘텍스트에 등록되어있는 상태를 의미한다.

public class Jpa {
   public static void main(String args[]) {
   
      User userA = new User();
      userA.setId("userA");
      userA.setName("김철수");
   
      EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
      EntityManager em = emf.createEntityManager();
      
      em.getTransaction().begin(); // 트랜잭션 시작
      em.persist(userA); // 영속성 컨텍스트에 Entity를 저장 => 영속 상태
   }
}

 

* 오해하지 말자!

→ 영속 상태가 되었다고 DB에 바로 저장되는 것이 아니다.

→ DB에 저장되는 시점은 persist()를 commit()한 다음, 해당 Transaction이 온전히 종료됐을 때다.

 

c) Detached(= 준영속 상태)

  → Entity가 영속성 콘텍스트에 저장되었다가 분리된 상태

public class Jpa {
   public static void main(String args[]) {
   
      User userA = new User();
      userA.setId("userA");
      userA.setName("김철수");
   
      EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
      EntityManager em = emf.createEntityManager();
      
      em.getTransaction().begin(); // 트랜잭션 시작
      em.persist(userA); // 영속성 컨텍스트에 등록 => 영속 상태
      em.detach(userA); // 영속성 컨텍스트에서 분리됨 => 준영속 상태
   }
}

 

 

d) Removed(= 삭제 상태)

  → DB에 저장되어있는 Entity가 삭제된 상태

  → 즉, DB로부터 삭제된 Entity의 상태를 의미한다. 

 

5. Entity Manager의 역할

- 위에서 Entity Manager는 Entity와 Persistence Context 사이에 존재한다고 말했다.

- 그렇다면 왜 Entity Manager는 중간에 위치하는 것일까?

 

- 쉽게 말하면, 1차적으로 끝날 과정에 하나의 단계를 추가함으로써 중간 처리단계를 만들 수 있기 때문이다.

- 즉, 객체 데이터를 Persistence Context에 바로 저장하면 데이터에 대한 후처리를 진행할 수 없다.

 

- 단순히 데이터의 후처리를 위한 것이라면, 후처리까지 끝내 놓고 저장하면 되는 것 아니냐는 반문을 할 수 있다.

- Entity Manager가 제공하는 기능을 이해한다면 이에 대한 설명이 추가적으로 필요 없을 것이다.

 

 Entity Manager는 단순히 Entity를 Persistence Context에 등록/저장하는 것 외 다음과 같은 기능을 한다.

 

* 오해하지 말자!

- 이제부터 Entity Manager를 통해 사용되는 다양한 기능에 대해서 설명하려고 한다.

- 사실 설명되는 기능들은 모두 Persistence Context의 기능이다.

- 그러나 마치 Entity Manager 덕분에 해당 기능을 사용할 수 있다는 식으로 이해할 수 있다.

- 엄연히 Entity Manager와 Persistence Context는 Entity Manager를 사용하는 것이

   결국 Persistence Context를 사용하는 것이므로 이처럼 표현한 것이니 오해하지 말자.

 

a) 1차 캐시

- Entity Manager는 1차 캐시를 가지고 있다.

- Entity Manager를 통해 Entity를 Persistenct Context에 등록하면 1차 캐시에 등록이 된다.

- 1차 캐시는 다음과 같이 동작한다.

 

public class Jpa {
   public static void main(String args[]) {
   
      EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
      EntityManager em = emf.createEntityManager();
      
      em.getTransaction().begin();
      em.persist(userA); 
      em.find(userA); // Entity Manager의 1차 캐시에서 Entity를 조회
   }
}

 

- Entity Manager를 통해 Entity를 조회하는 경우, DB를 조회하기 전에 1차 캐시를 우선 조회한다.

- 이처럼 1차 캐시를 우선 조회하는 경우, 응답속도가 훨씬 빠르다는 장점이 있다.

 

- 만약 1차 캐시에 존재하지 않는 다른 Entity를 조회한다면, 1차 캐시를 조회한 후 DB에 접근하여 조회한다.

- 이후 DB에서 반환된 값을 Entity Manager의 1차 캐시에 저장하고 나서 Client에게 반환한다.

 

* 오해하지 말자!

  → Entity Manager는 하나의 트랜잭션이 시작하면서 생성되고 종료되면서 삭제된다. 

  → 그러므로 1차 캐시는 하나의 트랜잭션 안에서만 사용되는 캐시다.

  → 애플리케이션 전체가 공유하는 캐시는 2차 캐시라고 부른다. 

 

b) 영속된 Entity의 동일성 보장

- Entity Manager는 소속된 트랜잭션과 동일한 Lifecycle을 가진다.

- 그러므로 하나의 트랜잭션 동안 동일한 객체를 여러 번 조회하면 이를 같은 값으로 처리한다.

- 아래의 코드를 살펴보자.

 

public class Jpa {
   public static void main(String args[]) {
   
      EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
      EntityManager em = emf.createEntityManager();
      
      em.getTransaction().begin();
      em.persist(userA); 
      
      User a = em.find(userA);
      User b = em.find(userA);
      System.out.println(a == b); // true 반환
   }
}

 

- 사실 자바의 관점에서 살펴보면 a와 b는 서로 다른 주소 값을 갖는 객체다.

- 하지만 JPA는 하나의 트랜잭션 안에서 조회되는 동일한 객체에 대해서 같은 객체로 처리한다.

- 즉, 동일성이 보장된다.

 

- 이 동일성 또한 Entity Manager의 1차 캐시 덕분에 가능하다.

- 동일한 SQL을 반복해서 수행하면 DB로부터 값을 조회하지 않고, 1차 캐시에서 조회하기 때문에 가능한 것이다.

 

c) 쓰기 지연

- commit()을 수행하기 전까지 Entity Manager는 SQL을 작성하지도, DB에게 전달하지도 않는다.

- 이 기능을 쓰기 지연이라고 부른다.

- 다음 예시 코드를 살펴보자.

public class Jpa {
   public static void main(String args[]) {
   
      EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
      EntityManager em = emf.createEntityManager();
      EntityTransaction transaction = em.getTransaction(); // 트랜잭션 생성
      
      transaction.begin(); // 트랜잭션 시작 
      
      em.persist(userA);
      em.persist(userB); 
      
      transaction.commit(); // SQL이 DB에게 전달되는 시점
   }
}

- 위의 코드가 진행되는 과정을 설명하면 다음과 같다.

   1. em.persist(userA)가 실행된다.

   2. INSERT 쿼리가 생성되며 Persistence Context 내부의 쓰기 지연 SQL 저장소에 쌓인다.

   3. em.persist(userB)가 실행된다.

   4. INSERT 쿼리가 생성되며 Persistence Context 내부의 쓰기 지연 SQL 저장소에 쌓인다.

   5. transaction.commit()이 실행된다.

   6. 쓰기 지연 SQL 저장소에 쌓여있던 쿼리 명령어가 DB에게 전달된다. → flush 

   7. DB에서 전달받은 쿼리 명령어를 실행하고 결과를 저장한다. → commit

 

* 쓰기 지연 기능을 사용하는 이유는 무엇일까?

- 그 이유에는 DB와의 네트워킹 횟수가 있다.

 

- 실제로 DB에서 한 번의 커밋은 하나의 트랜잭션을 의미한다.

- 즉, DB의 값이 변경되는 작업의 처리 단위를 의미하는 것이다.

 

- 쿼리 하나당 한 번의 커밋을 수행한다는 것은 10개의 쿼리를 수행하기 위해 10번의 커밋을 필요한다는 것이다.

- 이를 백엔드의 관점에서 보면 10번의 쿼리를 수행하기 위해 10번의 DB 통신을 필요로 하는 것이다.

- 즉, 10개의 쿼리를 처리하기 위해 10개의 트랜잭션(= Entity Manager)을 생성하고 10번의 커밋을 수행하는 것이다.

 

- 만약 1만 개의 요청으로 1만 개의 쿼리를 수행한다고 하면 DB와 1만 번의 통신을 필요로 한다.

- 그러나 쿼리를 10개씩 묶어서 처리한다면 DB와 천 번의 통신을 필요로 한다.

- 즉, 네트워킹 횟수가 현저히 줄어들게 되고 이로 인해 서비스 또는 시스템의 부하가 감소하게 되는 것이다.

 

- 이처럼 다수의 쿼리를 묶어서 처리하는 기능으로 JDBC Batch가 있으며

- Hibernate에서는 hibernate.jdbc.batch_size 옵션을 이용하여 설정할 수 있다.(persistence.xml에 설정)

 

d) Entity 수정 및 변경 감지(Dirty Checking)

- 다음 예시 코드를 살펴보자.

public class Jpa {
   public static void main(String args[]) {
   
      EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
      EntityManager em = emf.createEntityManager();
      EntityTransaction transaction = em.getTransaction(); 
      
      transaction.begin();
      
      User user1 = em.find(User.class, "userA"); // DB에 저장된 userA를 찾는다.
      
      user1.setName("HELLO");
      
      transaction.commit();
   }
}

- 우선 DB에서 userA라는 값을 가진 객체를 조회한다.

- 그리고 조회된 객체의 값을 변경하였다. → user1.setName("Hello");

 

- 값을 변경한 다음 em.persist(user1)를 실행해야 되는 것 아닌지 의문이 들 수 있다. 

- 결론부터 이야기하자면 필요 없다.

 

- JPA의 핵심은 DB의 데이터를 객체처럼 다루도록 하는 것이다. 

- 즉, 자바 collections에 데이터를 저장하고 수정하는 것처럼 DB의 데이터를 다루도록 한다.

 

- 예를 들어, 배열에 저장된 값을 변경하고 다시 해당 값을 배열에 저장하지 않는 것처럼 말이다.

- 배열에 저장된 값을 변경하면 그것을 끝으로 배열은 변경된 값을 유지한다.

- 이와 똑같은 원리로 이해하면 된다.

 

* JPA 내부적으로는 어떻게 동작할까?

- 위의 코드를 실행하면, JPA 내부적으로 update 쿼리가 생성된다.

 

어떻게 이것이 가능할까?

- JPA는 어떻게 값이 변경된 것을 알 수 있을까?

- 결론부터 이야기하자면 1차 캐시를 사용하기 때문이다.

 

- 위에서 DB의 데이터를 조회하는 경우, 조회된 데이터를 1차 캐시에 저장한 후 Client에게 반환한다고 설명했다.

- 그럼 위의 예시 코드에서 "userA"를 조회한 시점에서 DB로부터 반환된 객체의 값이 1차 캐시에 저장된다.

   → 해당 객체가 DB로부터 가장 최근에 조회된 값을 1차 캐시의 snapshot 속성에 저장한다. 

 

- transaction.commit()이 호출되면 DB에게 쿼리를 전달하기 전에 JPA 내부에서는 flush()가 호출된다. 

- flush()가 호출된 시점에서 user1의 값과 1차 캐시에 저장된 snapshot을 비교하여 데이터의 변경을 감지하는 것이다.

- 이 비교 과정에서 데이터의 수정이나 변경이 감지되면 update 쿼리가 생성되고 쓰기 지연 SQL 저장소에 저장된다.

- 이후 쓰기 지연 SQL 저장소에 쌓인 쿼리를 DB에게 전달한다. 

 

6. flush()

- flush()는 영속성 콘텍스트의 변경 내용을 DB에 반영하는 것이다.

- 즉, 영속성 콘텍스트가 가지고 있는 SQL을 DB에게 전달하는 것이다.

 

a) flush()의 역할

- flush()는 이전 포스팅에서 설명한 다음의 역할/기능을 수행한다.

   → 변경 감지(Dirty Checking)

   → 변경된 Entity의 내용을 쓰기 지연 SQL 저장소에 등록

   → 쓰기 지연 SQL 저장소에 등록되어 있는 쿼리를 DB에게 전달

 

b) flush() 사용방법

   1. 직접 호출 방법

      → EntityManager.flush();

 

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

      → EntityTransaction.commit();

 

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

      → EntityTransaction.createQuery();

 

7. 준영속 상태

a) 영속 상태란?

- 영속 상태란, Persistence Context에 의해 Entity의 상태가 관리되고 있는 것을 말한다.

- 간단하게 Persistence Context의 1차 캐시 내부에 Entity의 값이 기록되어 있는 경우에 해당한다.

- EntityManager.persist() 또는 EntityManager.find()를 통해 Entity가 비영속 상태에서 영속 상태가 된다.

 

b) 준영속 상태란?

- Persistence Context에 의해 관리되던 Entity가 더 이상 관리를 받지 않는 상태를 말한다.

- 즉, Entity가 Persistence Context로부터 떨어져 나온/분리된 상태다. (= detached)

- 그러므로 준영속 상태의 Entity는 Persistence Context가 제공하는 기능을 사용할 수 없다.

 

c) 준영속 상태가 되는 방법

1. 특정 Entity의 상태

 

2. 영속성 콘텍스트 초기화 

→ EntityManager.clear();

 

3. 영속성 콘텍스트 종료

→ EntityManager.close();

'Back-end > JPA 개념' 카테고리의 다른 글

5. Field(칼럼) Mapping  (0) 2022.03.28
4. Entity Mapping  (0) 2022.03.25
2. JPA의 기본 구조와 기능  (0) 2022.03.23
1. JPA의 등장  (0) 2022.03.22
0. JPA를 사용하는 이유  (0) 2022.03.21

댓글