해당 글 스프링의 엔티티매니저 관리와 상관 없는 기초적인 JPA의 동작 원리를 기술하고 있습니다.
1. 기본 개념
JPA (Java Persistence API)는 관계형 데이터베이스를 사용하는 방식을 정의하는 인터페이스로, Java ORM 기술의 표준 명세라고 할 수 있습니다.
💡 ORM(Object Relational Mapping)
객체를 통해 간접적으로 DB 데이터를 다루는 방법으로, DB 데이터와 Object 필드를 매핑합니다.
예) JPA, Hibernate
💡 SQL Mapper
SQL문으로 DB를 직접 조작합니다.
예) MyBatis, jdbcTemplate
JPA를 다룰 때 필요한 엔티티 매니저(EntityManager)는 인터페이스로 정의되어 있습니다.
public interface EntityManager {
public void persist(Object entity);
public <T> T merge(T entity);
public void remove(Object entity);
public <T> T find(Class<T> entityClass, Object primaryKey);
...
}
1.1 JPA 구현체 : Hibernate
Hibernate는 JPA 인터페이스의 구현체로서, 자바용 ORM 프레임워크입니다.
내부적으로 JDBC API를 사용하고 있습니다.
EntityManagerFactory, EntityManager, EntityTransaction을 SessionFactory, Session, Transaction으로 상속받아 구현되어 있습니다.
즉, JPA를 사용할 때 구현체인 Hibernate를 사용하지 않아도 됩니다.
(*하지만 굳이 사용하지 않을 필요도 없습니다.)
2. 엔티티 관리
2.1. 엔티티 (Entity)
@Entity
Public class Member {
}
엔티티는 영속성을 가진 객체로, DB 테이블에 보관할 대상으로, 영속 컨텍스트에 속한 객체입니다.
@Entity로 선언된 객체 클래스가 해당됩니다.
2.2. 엔티티 매니저 팩토리 (Entity Manager Factory)
EntityManagerFatory emf = Persistence.createEntityManagerFactory("설정에 따른 DB명");
EntityManager em = emf.createEntityManager();
try {
...
} finally {
...
}
entityManagerFactory.close(); // DB Connection Pool에 대한 리소스 relase
엔티티 매니저는 여러 스레드(=요청)이 동시에 접근할 때, 동시성 문제가 생기기 때문에 공유할 수 없는 자원입니다.
이 때 엔티티 매니저에 대한 관리(생성/반환)를 엔티티 매니저 팩토리가 담당합니다.
✅ 특징
- 애플리케이션이 로딩될 때 DB마다 1개만 생성되어야 합니다. 따라서 일반적으로 애플리케이션 생성 시 싱글톤으로 생성해 관리합니다.
- WAS가 종료되는 시점에 EntityManagerFacotry를 닫으면, DB Connection Pool에 대한 리소스가 relase됩니다.
2.3. 엔티티 매니저 (Entity Manager)
EntityManagerFatory emf = Persistence.createEntityManagerFactory("설정에 따른 DB명");
EntityManager em = emf.createEntityManager();
try {
...
} finally {
em.close(); // DB Connection Pool 반환
}
entityManagerFactory.close();
엔티티 매니저는 말 그대로 엔티티를 관리하는 역할입니다.
스레드가 생성될 때마다(=요청이 올 때마다) 엔티티 매니저를 생성하는데, 이 때 영속성 컨텍스트가 1:1로 생성되어 참조하게 됩니다.
이 엔티티 매니저를 이용하여 영속성 컨텍스트 안에 엔티티를 영속화시키면 그때부터 엔티티를 관리할 수 있게 됩니다.
즉, 엔티티 매니저를 통해 DB와 어플리케이션 사이에서 객체를 생성/수정/삭제할 수 있게 됩니다.
✅ 특징
- DB Connection Pool을 통해 데이터베이스에 접근합니다.
- 트랜잭션 단위를 수행할 때마다 생성해, 스레드(=요청)마다 사용하고 닫습니다.
- 스레드간에 공유하면 안되기 때문에, 트랜잭션이 끝나면 DB Connection Pool을 반환해야 합니다. (em.close())
2.4. 엔티티 트랜잭션 (Entity Transaction)
EntityManagerFatory emf = Persistence.createEntityManagerFactory("설정에 따른 DB명");
EntityManager em = emf.createEntityManager();
EntityTransaction tx= entityManager.getTransaction();
try {
tx.begin();
...
tx.commit();
} finally {
em.close();
}
entityManagerFactory.close();
데이터를 변경하는 CRUD 작업의 경우, 동시성 이슈로 인해 트랜잭션 안에서 이루어져야 합니다.
3. 영속성 컨텍스트 (Persistence Context)
영속성 컨텍스트는 엔티티를 영구 저장하는 환경입니다.
애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 데이터베이스와 같은 역할을 합니다.
엔티티 매니저를 통해 엔티티를 저장하면, 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리합니다.
- 영속성 컨텍스트는 엔티티 매니저를 생성할 때 하나가 만들어집니다. (1:1)
3.1 생명주기
3.1.1. 비영속(new/transient)
영속성 컨텍스트와 전혀 관계가 없는 상태입니다.
엔티티 객체를 생성했지만 아직 영속성 컨텍스트에 저장하지 않은 상태입니다. (new/transient)
Member member = new Member();
3.1.2 영속(managed)
영속성 컨텍스트에 저장된 상태입니다.
엔티티 매니저를 통해서 엔티티를 영속성 컨텍스트에 저장한 상태를 말하며 영속성 컨텍스트에 의해 관리되고 있다는 의미입니다.
em.persist(member);
3.1.3. 준영속(detached)
영속성 컨텍스트에 저장되었다가 분리된 상태입니다.
영속성 컨텍스트가 관리하던 영속 상태의 엔티티 더이상 관리하지 않는 상태입니다. 특정 엔티티를 준영속 상태로 만드려면 em.datach()를 호출하면 됩니다.
- 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않습니다.
- 식별자 값을 가지고 있습니다.
// 엔티티를 영속성 컨텍스트에서 분리해 준영속 상태로 만든다.
em.detach(member);
// 영속성 콘텍스트를 비워도 관리되던 엔티티는 준영속 상태가 된다.
em.claer();
// 영속성 콘텍스트를 종료해도 관리되던 엔티티는 준영속 상태가 된다.
em.close();
3.1.4 삭제(removed)
엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제한 상태입니다.
em.remove(member);
3.2. 특징
3.2.1. 동일성 보장
영속성 컨텍스트는 엔티티의 동일성을 보장합니다.
이는 실제 인스턴스가 같다는 의미입니다. (== 비교로 참조가 같음)
💡 동일성 vs 동등성
동일성 : 실제 인스턴스(참조)가 같아 ==으로 비교할 수 있습니다.
동등성 : 실제 인스턴스는 다를 수 있지만 인스턴스의 값이 같습니다.
3.2.2. 1차 캐시
영속성 컨텍스트 내부의 캐시를 1차 캐시라고 하는데, 영속 상태의 엔티티를 이곳에 저장(em.persist())합니다.
엔티티를 조회할 때 DB가 아닌 1차 캐시를 우선적으로 탐색합니다.
📈 조회 순서
- 1차 캐시에서 엔티티를 찾습니다.
- 있으면 메모리에 있는 1차 캐시에서 엔티티를 조회합니다.
- 없으면 데이터베이스에서 조회합니다.
- 조회한 데이터로 엔티티를 생성해 1차 캐시에 저장합니다. (엔티티를 영속상태로 만듭니다)
- 조회한 엔티티를 반환합니다.
3.2.3. 식별자(@Id)로 조회
영속성 컨텍스트는 엔티티를 식별자 값으로 구분하기 때문에, 영속 상태의 엔티티는 식별자 값이 반드시 있어야 합니다.
// em.find(엔티티 클래스 타입, 식별자 값);
Member member = em.find(Member.class, "member1");
3.2.4. 1차 캐시 구성
- @Id
- Entity
- Snapshot (값을 읽어온 최초의 상태)
💡 2차 캐시
여러 명이 사용하는 Application 전체에서 공유하는 캐시
3.3. 지연 로딩 (Lazy Loading)
JPA에서 데이터를 조회할 때는 즉시 로딩과 지연 로딩의 2가지 방식을 사용할 수 있습니다.
✅ 즉시 로딩
엔티티를 조회할 때 연관된 모든 객체의 데이터를 한 번에 불러오게 됩니다. (N:N 관계에 있는 모든 엔티티를 조회)
이 때 불러온 데이터를 모두 영속성 컨텍스트에 저장하게 됩니다.
✅ 지연 로딩
엔티티를 조회할 때 연관된 객체의 데이터는 필요할 경우에만 불러올 수 있습니다.
조회 엔티티만을 영속성 컨텍스트에 저장하고, 필요 시에만 추가로 조회함으로써 1)SQL 조회 쿼리의 수도 줄일 수 있고, 2)영속성 컨텍스트에서 관리하는 데이터 또한 줄일 수 있습니다.
3.4. 트랜잭션을 지원하는 쓰기 지연
엔티티 매니저는 트랜직션을 커밋하기 직전까지 **내부 쿼리 저장소(=쓰기 지연 SQL 저장소)**에 INSERT SQL을 모아둡니다.
트랜잭션이 커밋되는 시점에 모아둔 쿼리를 DB에 보내는데, 이것을 트랜잭션을 지원하는 쓰기 지연이라고 합니다.
3.5. 변경 감지 (Dirty Checking)
앞선 쓰기 지연 특성으로 인해, JPA는 엔티티의 변화를 바로 반영하지 않습니다.
트랜잭션을 커밋했을때, 영속성 컨텍스트 속 엔티티의 상태와 비교하여 때 변경이 감지되면 데이터베이스에 반영합니다.
📈 변경감지 순서
- 트랜잭션을 커밋하면 엔티티 매니저 내부에서 flush가 호출됩니다.
- 엔티티와 스냅샷을 비교해 변경된 엔티티를 찾습니다.
- 변경된 엔티티가 있으면 수정 쿼리를 생성하여 쓰기 지연 저장소에 저장합니다.
- 쓰기 지연 저장소의 SQL을 flush합니다.
- 데이터베이스 트랜잭션을 커밋합니다.
@DynamicUpdate
@Entity
@DynamicUpdate
public class EntityClass...
dirty chekcing의 update 쿼리는 모든 필드를 대상으로 합니다.
이 때, 엔티티 수정 시 변경된 필드만 반영되게 할 수 있습니다.
3.6. 플러시(flush)
JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 저장된 엔티티를 데이터 베이스에 반영하는데, 이를 플러시라고 합니다.
이 때, 영속성 컨텍스트의 내용이 변경된 경우, 영속성 컨텍스트의 엔티티를 지우는게 아니라 변경 내용을 데이터베이스에 동기화합니다.
하지만 쓰기지연 저장소의 SQL들은 트랜잭션이 커밋되기 전에 플러시가 동작되면 모두 지워집니다.
📈 플러시 순서
- 변경 감지가 동작해서 스냅샷과 비교해서 수정된 엔티티를 찾는다.
- 수정된 엔티티에 대해서 수정 쿼리를 만들거 SQL 저장소에 등록한다.
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송한다.
✅ 플러시하는 방법
- em.flush()
- 트랙잭션 커밋시 자동 호출
- JPQL 쿼리 실행시 자동 호출
'백엔드 Backend > JPA' 카테고리의 다른 글
Entity에 Serializable을 구현하는 이유 (0) | 2025.01.08 |
---|---|
JPA 설정하기 (순수 Java / Spring Data JPA) (0) | 2024.08.12 |