본문 바로가기
Tech Stack/Database

🗃️ [JPA-03] JPA 아키텍처 구조 및 주요 API 동작 원리

by 박수무당벌레 2026. 7. 9.

1. JPA 아키텍처 (Architecture)

JPA의 핵심 아키텍처는 애플리케이션과 데이터베이스 사이에서 객체를 관리하는 영속성 컨텍스트(Persistence Context)와 이를 관리하는 메니저/팩토리 구조로 이루어져 있다.

1) 내부 아키텍처 구조

+-------------------------------------------------------------------+
|                        Application                                |
|                                                                   |
|   +-----------------------+     +-----------------------------+   |
|   | EntityManagerFactory  | --► |       EntityManager         |   |
|   +-----------------------+     +-----------------------------+   |
|                                                |                  |
|                                                ▼                  |
|                                 +-----------------------------+   |
|                                 |    Persistence Context      |   |
|                                 |  (1차 캐시 / Entity 관리)   |   |
|                                 +-----------------------------+   |
|                                                |                  |
|                                                ▼                  |
|                                 +-----------------------------+   |
|                                 |      EntityTransaction      |   |
|                                 +-----------------------------+   |
+-------------------------------------------------------------------+
                                                 | (JDBC API)
                                                 ▼
                                     +----------------------+
                                     |  Database (RDB)      |
                                     +----------------------+

2) 주요 구성요소 설명

  • *EntityManagerFactory (엔티티 메니저 팩토리)
    • 역할: EntityManager를 생성하는 공장 역할을 한다.
    • 생성 비용: DB 연결 커넥션 풀을 생성하고 매핑 정보를 해석하는 등 JPA 동작에 필요한 모든 준비(부팅)가 필요하기 때문에 생성 비용이 매우 크다. (무겁다)
    • 생명주기:
      • 애플리케이션 실행 시 생성되며, DB당 하나만 생성하여 애플리케이션 전체에서 공유한다.
    • 스레드 안정성: 스레드 세이프(Thread-safe)하므로 여러 스레드가 동시에 접근해도 안전하다.
  • EntityManager (엔티티 메니저)
    • 역할: 영속성 컨텍스트에 접근하여 엔티티의 CRUD 작업을 수행한다.  (엔티티 생명주기를 다루는 핵심 주체)
    • 생성 비용: 생성 비용이 거의 들지 않는다. (가벼운 편)
    • 생명주기: 고객의 요청(HTTP 요청/트랜잭션)이 들어올 때마다 생성하고, 작업이 끝나면 버림.
    • 스레드 안정성 (Thread-safety 불보장): 절대로 스레드 간에 공유하면 안 되며, 요청마다 독립적으로 사용해야 한다. (버려짐)
  • Persistence Context (영속성 컨텍스트)
    • 개념: "엔티티를 영구 저장하는 환경"을 의미하는 논리적인 개념. 자세한 내용은 여기서.
    • 접근 및 관리 방식: 애플리케이션은 직접 접근하지 않고, EntityManager를 통해 영속성 컨텍스트에 접근하고 내부 엔티티를 관리(조작)한다.
    • 핵심 역할 (메모리 영역):
      • EntityManager 내부에 엔티티 인스턴스들을 일차적으로 보관하고 관리하는 메모리 영역.
      • 내부적으로 1차 캐시로 빠른 조회를 돕고, 쓰기 지연 버퍼(Transactional Write-Behind Queue) 역할을 함께 수행함으로서 DB 부담을 줄인다.
    • 주요 기능(동작 메커니즘): 변경 감지(Dirty Checking)로 수정을 알아서 처리해 준다.
  • EntityTransaction (트랜잭션 관리)
    • JPA의 모든 데이터 변경(INSERT, UPDATE, DELETE)은 트랜잭션 안에서 수행되어야 한다.
더보기

✨ EntityManagerFactory 생성 시 일어나는 내부 부팅 과정 (Bootstrapping)

 

아래는 다음 코드가 실행되었을 때의 내부적 작업 단계와 그에 대한 설명을 기술.

// EntityManagerFactory 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("mainDB");

 

1) 기반 로딩 및 XML 설정 해석 단계 (사전 준비)

  1. Hibernate 구현체 메모리 로드: 자바의 PersistenceProvider 메커니즘에 의해 구동 주체인 JPA 구현체(Hibernate)의 핵심 클래스들(internal 엔진)이 자바 메모리(JVM)에 먼저 로드된다.
    • JPA 진입점 클래스 동작: PersistenceProvider 구현체(Hibernate의 경우 HibernatePersistenceProvider)를 찾아 호출한다
  2. persistence.xml 읽기 및 해석(파싱): 메모리에 올려진 Hibernate 엔진이 persistence.xml 파일에 접근하여 텍스트를 읽고 해석(Parsing)하기 시작.
    • HibernatePersistenceProvider는 내부적으로 PersistenceXmlParser라는 전용 클래스를 작동시킴.
    • 이 클래스가 자바 클래스패스(META-INF/persistence.xml)에서 XML 파일을 찾은 뒤, XML 파서(SAX/DOM Parser)를 이용해 텍스트를 하나씩 읽어 읽어들인다.
    • 읽어들인 결과를 메모리상의 설정 객체(ParsedPersistenceXmlDescriptor)로 변환.

2) 실제 설정 객체 조립 및 JPA 엔진 구동 단계 (EntityManagerFactory 객체 생성)

  1. DB 드라이버 로드 및 연결 검증: XML에서 읽은 Driver, URL, 계정 정보(계정 이름, 비밀번호)로 DB 드라이버를 로드하고 실제로 DB에 접속이 잘 되는지 테스트 연결을 수행한다.
  2. 커넥션 풀(Connection Pool) 생성 및 유지: DB와 미리 커넥션을 맺어두는 통로를 생성.
  3. 엔티티 매핑 정보 분석 및 캐싱: @Entity가 붙은 자바 클래스들을 분석하여 어떤 테이블/컬럼과 매핑되는지 파악하고 메모리에 로드한다.
  4. DDL 자동 실행 (hbm2ddl.auto): 설정에 따라 DB에 테이블을 생성(create)하거나 변경(update)하는 DDL을 전달한다.
  5. DB 방언(Dialect) 엔진 초기화: 해당 DB 전용 SQL을 만들어낼 변환 엔진을 준비한다.
💡 위에서 해석된 설정을 바탕으로, 실제 외부 자원(DB)과 연결하는 DB 커넥션(커넥션 및 트랜잭션 관리 엔진), 매핑 메타데이터(영속성 및 매핑 메타데이터 관리 엔진), 방언 엔진 등을 구동하여 거대한 EntityManagerFactory 객체를 완성(조립)하는 단계.

 

3) 내부(internal) 엔진에 포함되는 대표적인 컴포넌트

  • EntityManagerFactory가 조립될 때 함께 구동되는 Hibernate 내부 엔진들을 뜻함.
  역할
방언 변환 엔진 (Dialect) JPQL을 특정 DB(MySQL, Oracle, H2 등) 문법에 맞는 SQL로 번역
쿼리 파싱 엔진 (QueryEngine / JPQL Parser) 작성된 JPQL, Criteria 문법을 분석하고 추상 구문 트리(AST)로 변환
Schema/DDL 엔진 (SchemaManagementTool) hbm2ddl.auto 옵션에 따라 DB 테이블(스키마)를 생성·수정·검증
커넥션/트랜잭션 엔진 (ConnectionProvider / TransactionCoordinator) DB 커넥션 풀을 관리하고 트랜잭션의 시작, 커밋, 롤백을 제어
1차 캐시 및 영속성 관리 엔진
(PersistenceContext / State Management)
엔티티의 스냅샷 저장, 변경 감지(Dirty Checking), 객체 동일성 보장 등을 관리하는 메모리 처리 엔진.
2차 캐시 관리 엔진
(CacheRegionFactory / CacheEngine)
(설정 시) 애플리케이션 전체에서 공유할 2차 캐시(L2 Cache) 처리

 

5) 기타 설명

  • 보통 "JPA 구동"과 코드 수준에서 "EntityManagerFactory 생성"을 동일한 의미로 쓴다. 
    • JPA를 구동/시작한다 (개념): 애플리케이션 시작 시 persistence.xml 설정을 읽어 JPA 엔진을 띄운다. 
    • EntityManagerFactory 생성 (실제 동작 과정): 바로 그 JPA 엔진을 메모리에 실제로 띄우는 코드(Persistence.createEntityManagerFactory(...))를 실행한다. (JPA 구현체(Hibernate 등) 메모리에 올리고 DB 연결을 맺는 초기화하는 내부 동작 포함)
  • EntityManagerFactory 생성은 애플리케이션 시작할때 딱 1개만 수행하여 공유된다. (무겁고 비용 큰 작업이라서)
    • DB 요청이 들어올 때마다 가벼운 EntityManager(조작 객체)를 계속 찍어내서 요청을 처리한다.
더보기

ℹ️ 핵심 요약: JPA의 전체 실행 흐름 (Lifecycle)

  1. JPA 시작 (부팅/초기화  단 1회):
    • EntityManagerFactory 생성 ➔ DB 연결, 커넥션 풀 생성, @Entity 매핑 분석, DDL 실행.
    • 결론: EntityManagerFactory 생성 = JPA 전체 시스템의 부팅(Bootstrapping)
  2. JPA 사용 (요청 처리  반복):
    • 요청마다 가벼운 EntityManager 생성 (emf.createEntityManager()) ➔ 실제 DB CRUD 쿼리 수행 및 트랜잭션 관리 후 종료.
  3. JPA 종료 (애플리케이션 종료 시):
    • EntityManagerFactory 종료 (emf.close()) ➔ 커넥션 풀 반환 및 DB 연결 자원 해제.
더보기

ℹ️ 영속성 및 매핑 메타데이터 관리 엔진

 

해당 역할 수행하는 핵심 주체는 Hibernate 내부의 Metadata / MetadataImplementor 객체와 이를 빌드하는 MetadataBuildingProcess (메타데이터 구축 엔진)

 

1) 매핑 메타데이터를 파싱하고 모으는 주체

  • MetadataSources & MetadataBuilder:
    • persistence.xml 또는 Spring 설정 파일에서 읽어들인 엔티티 클래스 목록(@Entity), XML 매핑 파일(orm.xml), @Table, @Column 등의 어노테이션 정보를 수집하고 스캔하는 역할을 한다.

2) 메타데이터를 검증하고 객체화하는 실제 핵심 엔진

  • MetadataBuildingProcess / Metadata:
    • 수집된 자바 클래스와 어노테이션 정보들을 바탕으로 데이터베이스 연관관계(@ManyToOne, @OneToMany 등), PK 생성 전략, 컬럼 타입 매핑이 올바른지 검증 및 분석(Mapping Analysis)한다.
    • 분석이 완료되면 모든 매핑 정보를 담고 있는 거대한 메타데이터 객체인 Metadata (구현체: MetadataImpl)를 완성하여 메모리에 캐싱한다.

 

[ Interface ]  Metadata  (공개용 기본 인터페이스)
                  ▲
                  │ (상속 / extends)
[ Interface ]  MetadataImplementor  (하이버네이트 내부엔진 전용 확장 인터페이스)
                  ▲
                  │ (구현 / implements)
[ Class ]      MetadataImpl  (실제 동작하는 구현체 클래스)

 

3) 영속 상태 및 메타데이터를 전역 관리하는 팩토리

  • SessionFactoryImpl (EntityManagerFactoryImpl):
    • 위에서 완성된 Metadata 객체를 기반으로 최종 구축되는 최상위 객체.
    • 내부적으로 EntityPersister와 CollectionPersister 항목들을 매핑 테이블별로 만들어두어, 향후 EntityManager가 엔티티의 상태(영속성)를 관리하고 SQL을 생성할 수 있도록 모든 영속성/매핑 정보를 전역으로 관리한다.
더보기

ℹ️ EntityPersister란?

 

EntityManagerFactory가 초기화될 때, 하이버네이트는 분석된 매핑 메타데이터를 바탕으로 엔티티 클래스 1개당 1개의 EntityPersister 구현체(기본값: SingleTableEntityPersister)를 생성하여 메모리에 상주시킨다.

EntityManager가 persist(), find(), flush() 등의 요청을 받으면, 직접 SQL을 만드는 것이 아니라 해당 엔티티 담당 EntityPersister에게 작업을 위임한다.

 

1) EntityPersister의 핵심 역할

  • SQL 쿼리 사전 생성 및 캐싱: 애플리케이션 부팅 시점에 기본 CRUD SQL(INSERT, UPDATE, DELETE, SELECT by ID) 구문을 미리 조합하여 메모리에 만들어 둡니다. (런타임마다 매번 SQL 문자열을 조립하는 비효율을 방지)
  • 엔티티 상태 값 추출 및 바인딩: 자바 객체(Entity)의 필드 값을 DB 컬럼 타입에 맞게 파라미터(?)로 바인딩합니다.
  • DB ResultSet ➔ 자바 객체 변환: SELECT 결과로 돌아온 DB Row 데이터를 자바 엔티티 객체의 필드에 대입(Mapping)해 줍니다.

2) 실제 API 호출 시 EntityPersister의 작동 흐름

  1. em.find(Member.class, 1L) 호출 시:
    • Member 전용 EntityPersister를 호출하여 미리 준비된 SELECT ... FROM member WHERE id = ? SQL을 가져와 JDBC로 실행하고, 결과 ResultSet을 Member 객체로 변환하여 반환한다.
  2. em.flush() (Dirty Checking) 발생 시:
    • EntityPersister가 변경된 필드만 추적하여 최적화된 UPDATE SQL을 생성 및 실행한다.
 
[개발자 코드]               [EntityManager / Session]             [EntityPersister]
     |                                 |                                  |
     |--- 1. em.persist(member) ------>|                                  |
     |                                 |--- 2. member 전담 Persister 찾기 ->|
     |                                 |                                  |--- 3. 미리 생성된 INSERT SQL 가져오기
     |                                 |                                  |--- 4. Member 객체 필드값 SQL 파라미터 바인딩
     |                                 |<-- 5. 준비된 SQL/Action 반환 -----|
     |                                 |
     |                                 |---> (쓰기 지연 저장소에 등록 후 Flush 시 DB 전송)

 

3) 요약하자면

  • EntityPersister: 만들어진 설계도를 바탕으로 런타임에 실제 SQL을 쏘고 객체를 조립하는 현장 감독/일꾼.
  • 부팅 시 매핑 분석(XML/어노테이션 읽기) 및 메타데이터 관리 엔진: MetadataBuildingProcess(설계 과정) / MetadataImpl(결과물)
  • 런타임 시 개별 엔티티의 영속성/매핑 실행 관리자: EntityPersister (하이버네이트 내부에서 엔티티당 1개씩 생성되어 DB 테이블 매핑 및 SQL 생성을 전담하는 객체)

 2. 주요 API 통합 실습 코드(Post 엔티티)

// 1. 저장 (영속화): INSERT 쿼리를 생성하여 쓰기 지연 버퍼에 저장
em.persist(post);

// 2. 조회: 1차 캐시 확인 후 없으면 DB SELECT 쿼리 실행
Post foundPost = em.find(Post.class, postId);

// 3. 수정 (Dirty Checking): 별도의 update() 없이 객체 필드값 변경만으로 UPDATE 준비
foundPost.setTitle("수정된 게시글 제목");

// 4. 삭제: DELETE 쿼리를 생성하여 쓰기 지연 버퍼에 저장
em.remove(foundPost);

// 5. 플러시 (Flush): 쓰기 지연 버퍼의 SQL(INSERT/UPDATE/DELETE)을 DB로 전송
em.flush();

3. 주요 API 호출 시 내부 동작 프로세스

1) em.persist(post) — 저장 동작

  1. 인자로 전달된 엔티티 객체를 1차 캐시에 등록(저장)한다.
  2. 엔티티를 분석해 INSERT SQL 생성 후 쓰기 지연 SQL 저장소(ActionQueue)에 등록. (= 쌓는다)
  3. 트랜잭션 commit 시 flush가 실행되어 DB로 SQL 전송.
    • 💡 참고: @GeneratedValue 전략이 IDENTITY인 경우에는 PK 값을 알기 위해 persist() 시점에 즉시 INSERT 쿼리가 실행된다.

2) em.find(Post.class, postId) — 조회 동작

  1. 1차 캐시 먼저 확인: 영속성 컨텍스트 내의 1차 캐시에서 먼저 식별자(@Id)로 데이터가 있는지 찾는다. 
  2. [1차 캐시 Hit]: 존재하면 DB 조회 없이 즉시 객체 반환.
  3. [1차 캐시 Miss]: 없으면 아래 단계를 거친다 (순서대로)
    • JDBC API를 통해 DB로 SELECT 쿼리를 전송한다. (조회)
    • 가져온 Row 데이터를 바탕으로 엔티티 객체를 생성한다.
    • 해당 객체를 1차 캐시에 보관하고, 최초 상태 복사본(스냅샷)을 생성한다.
    • 마지막으로 객체를 반환한다.

3) foundPost.setTitle("New Title") (Dirty Checking) — 수정 / 변경 감지 동작

자세한 내용은 여기서.

  1. 자바 코드에서 엔티티 수정: 영속 상태인 엔티티 객체의 필드 값을 변경한다.
  2. 상태 비교 (Dirty Checking): 트랜잭션 커밋 또는 플러시 시점에, 영속성 컨텍스트가 현재 엔티티 상태와 최초 1차 캐시 저장 시점의 스냅샷을 비교(Dirty Checking)한다.
  3. UPDATE SQL 생성: 최초 스냅샷 대비 변경된 부분이 감지되면, JPA가 자동으로 UPDATE SQL을 생성하여 쓰기 지연 SQL 저장소에 등록한다.
  4. DB 반영: 플러시/커밋 시점에 쓰기 지연 저장소의 UPDATE SQL이 DB로 전송되어 변경 사항이 최종 반영.

5) em.remove(entity) — 삭제 동작

  1. 삭제할 엔티티를 삭제 상태(Removed)로 변경한다.
  2. DELETE SQL을 생성하여 쓰기 지연 SQL 저장소에 등록한다.
  3. 1차 캐시에서는 플러시/커밋 시점에 실제 제거됨.

4) em.flush() — 플러시 동작

  • 개념: 영속성 컨텍스트의 변경 내용(등록, 수정, 삭제)을 데이터베이스에 반영(동기화)하는 작업.
  • 호출 시점: 트랜잭션을 커밋(tx.commit())할 때, JPQL 쿼리를 실행할 때, 또는 em.flush()를 직접 명시적으로 호출할 때 자동으로 동작한다.
  • 내부 동작 순서:
    1. 변경 감지(Dirty Checking)가 동작하여 영속성 컨텍스트 내의 모든 엔티티와 스냅샷을 비교한다.
    2. 수정/삭제/등록 대상이 있다면 각각의 SQL(UPDATE, DELETE, INSERT)을 생성하여 쓰기 지연 SQL 저장소에 등록한다.
    3. 쓰기 지연 SQL 저장소의 모든 쿼리를 DB로 일괄 전송한다. (동기화)
  • ⚠️ 주의: 플러시는 쓰기 지연 저장소의 SQL을 DB로 보내는 동기화 작업일 뿐이며, 1차 캐시 메모리를 비우는 작업 아니다.