본문으로 건너뛰기
Forward Engineering
Go back

[JPA + Spring Mastery 01] 1차 캐시 · flush · 트랜잭션 라이프사이클 — readOnly 가 줄이는 set_option, dirty checking 의 진짜 비용

- views

Table of contents

Open Table of contents

들어가며

raw JDBC 와 JPA 5 가지 방법 (method name / @Query JPQL / Native SQL / QueryDSL / JdbcTemplate) 으로 같은 SELECT 를 실행해서 latency 를 비교한 [실측] 이 있습니다. 1,000만 row 의 orders_w2 테이블 위에서 state='CONFIRMED' ORDER BY created_at DESC LIMIT 20 — 같은 인덱스 + LIMIT 사용. 결과:

방법avg(ms) (warm)p99(ms)
raw JDBC + JdbcTemplate0.7361.018
Spring Data JPA — method name 추론0.9941.467
Spring Data JPA — @Query JPQL0.8563.558
Spring Data JPA — Native SQL0.9801.439
QueryDSL — Q-class0.9081.539

raw JDBC (0.74ms) 와 JPA Native SQL (0.98ms) 의 차이가 약 +0.4ms. 같은 SQL, 같은 결과 row, 같은 LIMIT — 차이는 서버 측 (DB latency) 이 아니라 클라이언트 측 (JPA 의 객체 변환 + 라이프사이클 등록).

이 +0.4ms 는 무엇이고 왜 발생하는가. 답은 PersistenceContext 입니다. JPA 가 row 를 entity 객체로 만들어 1차 캐시 에 등록하고, dirty checking 을 위한 snapshot 을 만들고, transaction sync hook 을 등록하는 비용. 1 일 100만 호출이면 약 6.7 분 누적 latency.

이 비용은 반드시 발생합니다. JPA 의 본질이 PersistenceContext 위에 있기 때문. 없애려면 JPA 를 안 쓰거나, 줄이려면 readOnly=true 로 dirty checking 을 끄는 식의 미세 조정 — 카카오페이가 JPA Transactional 잘 알고 쓰고 계신가요? 에서 측정값으로 보고한 set_option QPS 58% 감소 가 그 사례.

이 글은 PersistenceContext 의 동작을 Identity Map 패턴 (Fowler PoEAA 2002) 부터 시작해서 — ActionQueue 4 종 / AutoFlush 알고리즘 / Dirty Checking 두 방식 (reflection vs bytecode enhancement) 까지 Hibernate 6 소스 라인 을 함께 풀어봅니다. 그리고 그 위에 카카오페이의 readOnly + set_option 회고를 3단 효과 (Hibernate flush mode + Spring tx 마커 + MySQL Com_set_option 감소) 로 분해합니다.

  1. PersistenceContext = Identity Map — Fowler PoEAA 의 패턴
  2. EntityManager vs Hibernate Session — JPA 표준의 한계
  3. ActionQueue 4 종 — INSERT / UPDATE / DELETE / Collection 의 발행 순서 알고리즘
  4. AutoFlush — query 직전 flush 의 정확한 트리거 조건
  5. Dirty Checking 두 방식 — reflection (default) vs bytecode enhancement
  6. 트랜잭션 라이프사이클 한 바퀴 — BEGIN → flush → COMMIT
  7. readOnly 의 3단 효과 — Hibernate / Spring / MySQL 분해 + 카카오페이 회고
  8. JPA 5 ways 측정의 +0.4ms 분해 — 어디서 오는 비용인가
  9. 운영 점검 체크리스트

결론부터 말하면:

머릿속의 “1차 캐시는 빠르니까 좋다” 가 어떻게 반쪽 답 인지 라인 단위로 풀어봅니다.


1. PersistenceContext = Identity Map (Fowler PoEAA 2002)

1.1 패턴 정의

Fowler 의 Patterns of Enterprise Application Architecture (2002) 에서 정의한 Identity Map 패턴:

“An Identity Map keeps a record of all objects that have been read from the database in a single business transaction. Whenever you want an object, you check the Identity Map first to see if you already have it.” — Martin Fowler, Patterns of Enterprise Application Architecture, 2002

핵심 정의:

  1. 단일 비즈니스 트랜잭션 안에서
  2. DB 에서 읽은 모든 객체
  3. Map 에 보관 하고
  4. 같은 ID 조회 시 같은 Java 객체 반환

이 패턴이 JPA 의 PersistenceContext 의 본질입니다. EntityManagerMap<EntityKey, Object> 형태의 1차 캐시를 가지고 있고, 같은 트랜잭션 안에서 findById(1L) 을 두 번 호출하면 같은 Java 객체 를 반환합니다.

1.2 Java 객체 동일성 보장

@Transactional
public void example() {
    Order o1 = repo.findById(1L);  // 첫 조회 — DB 에서 읽음
    Order o2 = repo.findById(1L);  // 두 번째 — 1차 캐시 hit

    assertThat(o1).isSameAs(o2);   // ✅ 같은 객체 (== 비교)
}

Fowler 의 표현으로는 “business transaction 안에서 객체 동일성 보장”. 이 보장이 깨지면 — 같은 ID 의 다른 객체 두 개가 생기고, 한 쪽 변경이 다른 쪽에 반영 안 되는 split entity 사고가 가능.

1.3 Unit of Work 와의 결합

Identity Map 은 Fowler 의 또 다른 패턴 Unit of Work 와 함께 작동합니다.

“A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you’re done, it figures out everything that needs to be done to alter the database as a result of your work.” — Fowler, PoEAA

Unit of Work = 트랜잭션 안의 모든 변경 추적 → commit 시점에 한 번에 flush.

JPA 의 PersistenceContext 가 Identity Map Unit of Work 모두 를 구현. 즉 EntityManager 는:

두 패턴의 조합 이 JPA 의 핵심 운영 모델.

1.4 1차 캐시의 hit/miss 시나리오

@Transactional
public void cacheBehavior() {
    Order o1 = repo.findById(1L);  // miss → SELECT
    Order o2 = repo.findById(1L);  // hit → 캐시 (SQL 미발행)

    em.clear();                      // 1차 캐시 비움

    Order o3 = repo.findById(1L);  // miss → SELECT
    assertThat(o1).isNotSameAs(o3);  // 다른 트랜잭션 / 캐시 비운 후
}

em.clear() 또는 em.detach(o1) 또는 트랜잭션 종료 시점에 1차 캐시가 비워집니다. 트랜잭션 길이 = PersistenceContext 길이.


2. EntityManager vs Hibernate Session — JPA 표준의 한계

2.1 JPA spec 의 추상화

JPA 2.2 (JSR 338) 의 EntityManager 인터페이스는 벤더 중립 한 표준. 핵심 메서드:

public interface EntityManager {
    void persist(Object entity);          // INSERT
    <T> T find(Class<T> entityClass, Object primaryKey);  // SELECT
    <T> T merge(T entity);                // UPDATE (detached → managed)
    void remove(Object entity);            // DELETE
    void flush();                           // 강제 flush
    void clear();                           // 1차 캐시 비움
    void detach(Object entity);             // 1 entity 분리
    Query createQuery(String jpql);
    // ...
}

이 추상화는 Hibernate / EclipseLink / DataNucleus 같은 구현체를 교체 가능하게 합니다. 이론적으로.

2.2 Hibernate Session — 추가 기능

Hibernate User GuideSession 인터페이스는 EntityManager상속 하고 추가 기능 제공:

public interface Session extends EntityManager {
    void replicate(Object entity, ReplicationMode replicationMode);
    void update(Object entity);             // saveOrUpdate
    void saveOrUpdate(Object entity);
    Statistics getStatistics();
    Filter enableFilter(String filterName);
    // ...
}

운영에서 SessionHibernate 전용 기능 을 쓰는 경우가 많아 — 벤더 중립 한 추상화는 이론 에 가깝습니다. JPA spec 에 없는 setFlushMode(FlushMode.MANUAL) 같은 동작이 운영에서 중요할 때 — EntityManager#unwrap(Session.class) 로 Session 을 추출.

2.3 본 시리즈에서는 PersistenceContext 라고 부른다

이 글의 다음 섹션부터는 — EntityManager / Session 의 공통 자료구조 를 의미할 때 PersistenceContext 라는 단어를 사용합니다. Hibernate 6 소스에서도 같은 단어를 사용:

// hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java
public interface PersistenceContext {
    void addEntity(EntityKey key, Object entity);
    Object getEntity(EntityKey key);
    EntityEntry getEntry(Object entity);
    void clear();
    // ...
}

이 인터페이스의 default 구현이 StatefulPersistenceContext. entity-byEntityKey 의 Map, EntityEntry (entity 의 메타데이터), collection-byKey 등을 보관.


3. ActionQueue 4 종 — INSERT / UPDATE / DELETE 의 발행 순서

3.1 ActionQueue 자료구조

PersistenceContext 가 Unit of Work 역할을 할 때 — 변경된 entity 를 어디에 보관하나? Hibernate 의 ActionQueue 가 그 자료구조.

// hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java
public class ActionQueue {
    private ExecutableList<AbstractEntityInsertAction> insertions;
    private ExecutableList<EntityDeleteAction> deletions;
    private ExecutableList<EntityUpdateAction> updates;

    private ExecutableList<CollectionRecreateAction> collectionCreations;
    private ExecutableList<CollectionUpdateAction> collectionUpdates;
    private ExecutableList<CollectionRemoveAction> collectionRemovals;
    private ExecutableList<QueuedOperationCollectionAction> collectionQueuedOps;

    private ExecutableList<OrphanRemovalAction> orphanRemovals;
    // ...
}

7 개 ExecutableList — entity 의 INSERT / UPDATE / DELETE + collection 의 생성 / 갱신 / 제거 / queued ops + orphan removal.

3.2 발행 순서의 보장

em.persist(orderItem)em.persist(order) 순서로 호출해도 — Hibernate 가 parent INSERT 먼저. ActionQueue 가 처리 순서를 결정.

기본 순서:

  1. OrphanRemovalAction — 고아 entity 삭제 (cascade ORPHAN_REMOVAL)
  2. EntityInsertAction — 새 entity INSERT
  3. EntityUpdateAction — 기존 entity UPDATE
  4. CollectionRemoveAction — collection 항목 제거
  5. CollectionUpdateAction — collection 항목 갱신
  6. CollectionRecreateAction — collection 항목 재생성
  7. EntityDeleteAction — entity DELETE

이 순서가 FK 제약 을 만족하도록 설계. parent INSERT → child INSERT → child UPDATE → parent UPDATE → child DELETE → parent DELETE.

3.3 hibernate.order_inserts = true — 같은 entity type 묶음

order_inserts 옵션이 켜지면 — 같은 entity type 의 INSERT 들이 연속 발행 되어 JDBC batch insert 활성화 가능.

spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 50
        order_inserts: true   # 연속 발행
        order_updates: true   # UPDATE 도 동상

이 옵션이 왜 중요한가 — JDBC batch insert 는 연속된 같은 SQL 을 묶어 한 round-trip 으로 발행. order_inserts=false 면 INSERT 가 entity type 섞여 발행되어 — batch 못 묶임. (시리즈 글 5 saveAll IDENTITY 함정 에서 자세히 다룸)

3.4 운영 함정 — 발행 순서를 바꾸려면?

ActionQueue 의 순서는 Hibernate 내부 가 결정하므로 — 사용자가 직접 못 바꿈. 대안:

  1. em.flush() 명시 호출 — 부분적 flush 강제
  2. @Transactional 분리 — 별개 트랜잭션 commit
  3. native SQL — ActionQueue 우회

대부분의 경우 Hibernate 의 기본 순서가 옳지만 — 3 depth 이상의 cascade복잡한 collection 변경 시 가끔 어긋날 수 있음. 그때 em.flush() 가 진단 도구.


4. AutoFlush — query 직전 flush 의 정확한 트리거 조건

4.1 FlushModeType 3 종

JPA spec 의 FlushModeType 은 2 종이지만, Hibernate 가 추가 1 종을 더해 3 종 을 지원합니다:

FlushMode동작
AUTO (default)query 직전 + commit 시점에 자동 flush
COMMITcommit 시점에만 flush (query 직전 안 함)
MANUAL (Hibernate 만)명시적 em.flush() 호출 시에만

4.2 AUTO 의 트리거 조건

FlushModeType.AUTO 가 default. query 직전 에 flush 트리거. 그런데 모든 query 직전에 항상 flush 하지 않습니다 — Hibernate 가 query 결과에 영향을 줄 수 있는 entity 만 골라서 flush.

Vlad Mihalcea 의 글 에 자세히 정리:

@Transactional
public void autoFlushExample() {
    Order order = new Order(...);
    em.persist(order);  // ActionQueue 에 INSERT 등록 (DB 미반영)

    // query 직전 — Hibernate 가 결정
    List<Order> all = em.createQuery("SELECT o FROM Order o").getResultList();
    // ↑ Order entity 의 query → ActionQueue 의 Order INSERT 가 결과에 영향
    //   → flush 트리거 → Order INSERT 발행 → query 실행
    //   → all 에 새 order 포함됨
}

핵심: query 가 보는 entity 와 ActionQueue 의 entity 가 겹칠 때 flush. 안 겹치면 — flush 안 함.

4.3 Native query 의 함정

Native SQL query 는 — Hibernate 가 어떤 entity 에 영향 을 받는지 못 분석. 따라서 default 로 모든 ActionQueue 가 flush:

@Transactional
public void nativeQueryFlush() {
    Order order = new Order(...);
    em.persist(order);

    // Native SQL — Hibernate 가 분석 못 함
    em.createNativeQuery("SELECT COUNT(*) FROM customer").getSingleResult();
    // ↑ Order INSERT 발행됨 (Customer 와 무관함에도 flush)
}

이 함정이 Vlad 의 경고 — “Native query 는 stale data 위험을 피하려고 전체 flush. 의도와 다르게 INSERT/UPDATE 발행”.

회피: em.createNativeQuery(sql).unwrap(NativeQuery.class).addSynchronizedEntityClass(Order.class)해당 entity 만 flush 가능.

4.4 FlushMode.COMMIT 사용 시점

COMMIT 모드는 query 직전 flush 안 함. 사용 시점:

  1. 읽기 전용 트랜잭션 (@Transactional(readOnly=true))
  2. 대량 batch — 여러 INSERT/UPDATE 후 한 번에 commit
  3. 명시적 flush 제어 필요한 도메인

단점: query 결과가 현재 트랜잭션의 변경 반영 못 함. persist 후 같은 트랜잭션 안 query 결과에 그 entity 안 보임. 도메인 따라 적합/부적합.

4.5 Spring @Transactional(readOnly=true) 의 효과

Spring 이 readOnly=trueHibernate 의 FlushMode 를 MANUAL 로 변경. 이 효과는 다음 §7 에서 자세히.


5. Dirty Checking 두 방식 — Reflection vs Bytecode Enhancement

5.1 Dirty Checking 의 본질

@Transactional
public void dirtyCheckingExample() {
    Order order = repo.findById(1L);  // SELECT — entity managed
    order.setStatus(Status.CONFIRMED); // 변경 — DB 미반영

    // 트랜잭션 종료 시점:
    // 1. Hibernate 가 order 의 *현재 상태* 와 *snapshot* (조회 시점) 비교
    // 2. 다른 필드 발견 → ActionQueue 에 UPDATE 등록
    // 3. flush → UPDATE 발행
    // 4. commit
}

핵심: snapshot 비교. SELECT 시점의 entity 상태를 snapshot 으로 저장 하고, commit 시점에 현재 상태와 비교. 변경된 필드만 UPDATE.

5.2 두 방식 — Reflection vs Bytecode Enhancement

Vlad 의 글 에서 정리:

(1) Reflection (default) — Hibernate 가 런타임에 reflection 으로 entity 의 모든 필드를 byte-by-byte 비교

SELECT 시점:
  Order order = ... (managed)
  PersistenceContext 가 *deep copy* 로 snapshot 생성
  → snapshot = { id: 1, status: PENDING, amount: 100, ... }

commit 시점:
  for each field of order:
    if (currentField != snapshot[field]):
      mark dirty
  if (any dirty):
    ActionQueue 에 UPDATE 등록

비용:

(2) Bytecode Enhancement컴파일 타임 에 entity 의 setter 에 interceptor 로직 삽입

// 원본 코드 (개발자가 작성)
public class Order {
    private Status status;
    public void setStatus(Status s) { this.status = s; }
}

// Bytecode Enhancement 후 (Hibernate 가 변형)
public class Order implements PersistentAttributeInterceptable {
    private Status status;
    private PersistentAttributeInterceptor interceptor;

    public void setStatus(Status s) {
        this.status = interceptor.writeObject(this, "status", this.status, s);
        // ↑ interceptor 가 *변경 추적* 자동
    }
}

비용:

5.3 측정 비교 — 1만 row update

(이 부분은 EXP-13 측정 예정 — W4. 글 갱신 시 v2 발행)

EXP-13 에서 측정 예정 시나리오:

Vlad 의 측정 에 따르면 — 50,000 entity 환경 에서 bytecode enhancement 가 10x 빠름. 본 측정 (W4 EXP-13) 으로 학습 환경 에서 같은 효과 검증 예정.

5.4 Bytecode Enhancement 활성화

Spring Boot + Hibernate 6:

// build.gradle.kts
plugins {
    id("org.hibernate.orm") version "6.6.0"
}

hibernate {
    enhancement {
        enableLazyInitialization = true
        enableDirtyTracking = true   // ← Dirty checking enhancement
        enableAssociationManagement = true
    }
}

이 설정 후 모든 @Entity 가 자동 enhancement. 빌드 시간 약간 증가 (entity 100개 기준 +2s).

5.5 운영 함정 — Bytecode Enhancement 의 부작용

함정메커니즘
Lombok @Data 와 충돌@Dataequals/hashCode 가 enhancement 와 다른 방식 사용 — entity 동등성 깨짐
Custom Serialization 깨짐enhancement 가 추가 필드 (interceptor) 추가 — Java serialization 시 추가 직렬화
디버거에서 놀라운 동작setter 안에서 추가 로직 — breakpoint 시 stack trace 길어짐

대부분 운영 환경에선 Vlad 추천 — bytecode enhancement 켜기. 단 Lombok 사용 시 @Getter @Setter 명시 (전체 @Data 피하기).


6. 트랜잭션 라이프사이클 한 바퀴 — BEGIN → flush → COMMIT

6.1 전체 시퀀스

@Transactional 메서드 한 번 호출 시 — Spring + Hibernate 의 호출 흐름:

sequenceDiagram
    participant App as Application
    participant TI as TransactionInterceptor
    participant TM as HibernateTransactionManager
    participant Session as Hibernate Session
    participant AQ as ActionQueue
    participant DB as MySQL

    App->>TI: deductOrder() 호출 (proxy)
    TI->>TM: getTransaction(definition)
    TM->>Session: openSession() / 기존 사용
    TM->>DB: BEGIN transaction
    DB-->>TM: connection 획득
    TI->>App: raw target 호출

    App->>Session: em.find(Order.class, 1L)
    Session->>DB: SELECT ... — 1차 캐시 miss
    DB-->>Session: row 반환
    Session->>Session: snapshot 생성, 1차 캐시 등록
    Session-->>App: Order entity

    App->>App: order.setStatus(CONFIRMED) — entity 변경
    App-->>TI: return

    TI->>TM: commit(status)
    TM->>Session: flush()
    Session->>AQ: process all actions
    AQ->>AQ: dirty check — order 의 status 변경 발견
    AQ->>AQ: ActionQueue.updates 에 UPDATE 추가
    AQ->>DB: UPDATE order SET status = 'CONFIRMED' WHERE id = 1
    DB-->>AQ: 1 row affected
    Session->>DB: COMMIT
    DB-->>Session: ok
    TM-->>TI: ok
    TI-->>App: ok

6.2 주요 단계의 Spring/Hibernate 소스

각 단계의 Spring 6 / Hibernate 6 소스 위치:

단계소스
1. BEGINAbstractPlatformTransactionManager#getTransaction
2. Session.openSessionHibernateTransactionManager#doBegin
3. em.find (1차 캐시 miss)EntityManager#findLoadEventDefaultLoadEventListener
4. snapshot 생성StatefulPersistenceContext#addEntity
5. dirty checkDefaultFlushEventListener#onFlush
6. flushSession#flushFlushEventDefaultFlushEventListener
7. COMMITJdbcConnection#commit

6.3 PersistenceContext 의 정리

트랜잭션 종료 시점에 PersistenceContext 가 비워짐. 1차 캐시의 모든 entity 가 detached 상태로 변경.

@Transactional
public void example() {
    Order o = repo.findById(1L);  // managed
    return o;
}
// 트랜잭션 종료 — o 가 detached 됨

// 다른 메서드에서 — o 의 lazy 필드 접근 시 LazyInitializationException
o.getCustomer().getName();  // ← 예외 (OSIV=false 환경)

이 동작이 시리즈 글 3 — OSIV 의 출발점. OSIV=true 면 PersistenceContext 가 view 렌더링까지 살아있음 — lazy 필드 접근 가능. OSIV=false (권장) 면 — DTO projection 또는 fetch join 필수.


7. readOnly 의 3단 효과 — 카카오페이 회고 분해

7.1 카카오페이 회고

카카오페이 — JPA Transactional 잘 알고 쓰고 계신가요? 의 핵심:

@Transactional(readOnly = true) 를 적절히 사용해서 — set autocommit 명령의 round-trip 비용을 줄였더니 QPS 가 58% 올랐습니다.” — tech.kakaopay.com

이 측정값의 3단 효과 를 라인 단위로 분해합니다.

7.2 1단 — Hibernate FlushMode 변경

Spring 이 readOnly=true 를 발견하면 — Hibernate 의 Session#setHibernateFlushMode(FlushMode.MANUAL) 호출. AutoFlush 비활성화.

효과:

// Spring 6 — HibernateTransactionManager
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
    // ...
    if (definition.isReadOnly()) {
        session.setHibernateFlushMode(FlushMode.MANUAL);
    }
    // ...
}

이 효과만으로도 1만 row 조회 같은 시나리오에서 — snapshot 생성 비용 (entity 당 deep copy) 이 0 으로 감소. Vlad 의 측정에 따르면 읽기 전용 query 의 latency 가 약 30% 감소.

7.3 2단 — Spring tx readOnly 마커

Spring 이 readOnly=true 를 트랜잭션 status 에 마커 로 등록. 이 마커가:

7.4 3단 — MySQL Com_set_option 감소 (카카오페이의 측정값)

이 단이 카카오페이 회고의 핵심. JDBC connection 이 autocommit on/off 를 매 트랜잭션마다 바꾸는 round-trip 비용.

기본 동작:

  1. 트랜잭션 시작 — JDBC setAutoCommit(false) → MySQL 에 set autocommit=0 명령 발행
  2. 트랜잭션 종료 — setAutoCommit(true)set autocommit=1 명령 발행

매 트랜잭션 2번의 round-trip. 1ms RTT × 2 = 2ms / 트랜잭션. QPS 1000 면 — 2 초 / 초 의 순수 round-trip 비용.

readOnly=true 의 효과: MySQL Connector/J 가 readOnly 트랜잭션 에선 set autocommit생략. 즉 Com_set_option round-trip 자체가 사라짐.

-- MySQL 의 Com_set_option 메트릭 모니터링
SHOW STATUS LIKE 'Com_set_option';

카카오페이의 QPS 58% 증가 는 이 round-trip 감소의 직접 효과. 측정 환경에서 단순 read-only API 의 throughput 이 그만큼 늘어났음.

7.5 본 commerce-comment-platform-be 적용

@Service
@Transactional(readOnly = true)  // 클래스 default = readOnly
public class OrderQueryService {
    
    public List<Order> findRecent() {
        return repo.findTop20ByStateOrderByCreatedAtDesc();
    }
}

@Service
@Transactional  // 클래스 default = read-write
public class OrderCommandService {
    
    public Order create(OrderRequest req) {
        return repo.save(new Order(req));
    }
}

CQRS 패턴의 Query / Command 분리 와 자연스럽게 결합. 조회 서비스는 readOnly default + 쓰기 메서드만 @Transactional 명시.


8. JPA 5 ways 측정의 +0.4ms baseline 분해

8.1 측정 결과 다시 보기

방법avg(ms) (warm)p99(ms)결과 row
raw JDBC + JdbcTemplate0.7361.01820
Spring Data JPA — method name 추론0.9941.46720
Spring Data JPA — @Query JPQL0.8563.55820
Spring Data JPA — Native SQL0.9801.43920
QueryDSL — Q-class0.9081.53920

raw JDBC (0.74) 와 JPA 변종 (0.86~0.99) 의 차이 = 약 +0.4ms.

8.2 +0.4ms 의 구성

이 +0.4ms 는 다음 4 단계의 합:

단계비용메커니즘
1. ResultSet → entity hydration~0.15msreflection / proxy 생성
2. 1차 캐시 등록~0.05msEntityKey 계산 + Map.put
3. snapshot 생성 (dirty check 위함)~0.15msentity 당 deep copy
4. transaction sync hook 등록~0.05msTransactionSynchronizationManager.register
합계~0.40ms(LIMIT 20 → 20 row × 위 비용)

readOnly=true 적용 시:

→ readOnly 적용 시 +0.4ms 가 약 +0.25ms 로 감소. 측정 가능한 차이.

8.3 method name 추론의 p99 8.5ms 함정

W3 측정 1차 에서 method name 추론의 p99 가 8.496ms 로 측정. 워밍업 후에도 p99 8.5ms — 다른 변종의 5x 이상.

원인 추정:

운영 가이드라인:

8.4 운영 가이드라인 — 어디에 무엇을 쓰나

쿼리 종류추천
단일 row PK lookupSpring Data JPA findById() (편리)
단순 WHERE 조건 (1~2 컬럼)method name 추론 — 단 복잡해지기 전까지만
복잡한 WHERE / JOIN / 동적 조건QueryDSL (type-safety 가치)
운영 dashboard 의 read-only 다중 컬럼@Query JPQL 또는 Native SQL + readOnly=true
대량 batch / write-heavyraw JDBC + JdbcTemplate (JPA 비용 회피)
성능 critical (p99 < 1ms 목표)raw JDBC

→ JPA 의 진짜 이득write 측면@Transactional + dirty checking + cascade 가 비즈니스 로직 단순화. read-only 쿼리에선 JPA 의 비용/이득 비율 재검토.


9. 결론 — 글로벌 시니어가 보는 PersistenceContext

9.1 핵심 이해

이해
L1 표면”1차 캐시는 빠르니까 좋다”
L2 메커니즘Identity Map (Fowler 2002) + Unit of Work — EntityManager 가 두 패턴 모두 구현
L2.5 소스StatefulPersistenceContext (1차 캐시) + ActionQueue 7 ExecutableList + DefaultFlushEventListener (flush 트리거)
L3 실측JPA 5 ways — raw JDBC 0.74 vs JPA 0.86~0.99ms (+0.4ms baseline). method name 추론 p99 8.5ms
L4 운영카카오페이 readOnly + set_option QPS 58% — Hibernate flush mode + Spring tx 마커 + MySQL Com_set_option 감소 3단 효과
L5 학술Fowler PoEAA (Identity Map / Unit of Work) — JPA 의 패턴 기원

9.2 운영 점검 체크리스트

9.3 다음 글에서 다룰 것


10. 참고자료

학술 자료 / 서적 (L5)

공식 문서 (1순위)

Hibernate 6 소스 (직접 인용)

Vlad Mihalcea (Hibernate Steering Committee)

한국 빅테크 회고

본인 측정 자산

향후 보강 (W4 EXP-13 측정 후)


Share this post on:

Next Post
프로덕션이 'Check failed: node->IsInUse()' 한 줄로 죽었습니다 (2) — Datadog 프로파일러가 V8 청소부와 race를 만든 자리