DB/JPA

[Hibernate/JPA] 캐싱

꾸준함. 2025. 6. 11. 14:39

1. 데이터베이스 캐싱

 

1.1 캐싱의 계층 구조

  • 캐싱은 데이터베이스 성능 최적화의 핵심
  • DB → OS → 애플리케이션 → (ORM) → 2차 캐시 → 1차 캐시 등 여러 계층에 걸쳐 존재
  • 디스크 접근을 최소화하고, DB/OS/애플리케이션 메모리에서 최대한 데이터를 제공하는 것이 핵심

 

 

1.2 캐시 동기화 전략

 

가. Cache-aside

  • 애플리케이션이 캐시에서 먼저 데이터를 찾고, 없으면 DB에서 조회 후 캐시에 넣음
  • 쓰기는 데이터베이스와 캐시를 모두 갱신해야 함 (동기화 주의 필요)

 

 

나. Read-through

  • 캐시에 데이터가 없을 때, 캐시 시스템이 DB에서 자동으로 데이터 조회 및 캐시 삽입
  • 애플리케이션에서는 항상 캐시만 조회

 

 

다. Write-through

  • 쓰기 작업이 캐시와 DB에 동시에 적용됨
  • 캐시 일관성 유지가 쉬움

 

 

라. Write-invalidate

  • 쓰기 시 캐시에서 해당 데이터를 삭제만 하고, 다음 읽기 때만 최신 데이터로 채움

 

 

마. Write-behind

  • 애플리케이션은 캐시에만 쓰고, 일정 주기/조건에 따라 DB에 배치로 반영 (flush)
  • 쓰기 성능 극대화, 데이터 일관성 보장에 추가 로직 필요

 

 

1.3 데이터베이스와 OS 캐싱

 

가. 데이터베이스 캐시

  • 대부분의 DBMS는 메모리 내 버퍼 풀 (Buffer Pool), 실행 계획 캐시 (Shared Pool), Redo Log Buffer 등 다양한 캐시 구조 제공
  • 실제 데이터 페이지, 인덱스, 쿼리 결과, 실행 계획, 임시 작업 결과 등 다양한 객체가 캐싱됨

 

나. OS 캐시

  • OS 레벨의 페이지 캐시, 파일 시스템 캐시 등
  • 일부 DB는 Double Buffering (같은 페이지가 OS와 DB buffer pool 모두에 중복 저장)
  • Direct I/O 지원 DB는 OS 캐시를 우회하여 중복 캐싱을 피함

 

1.4 주요 DBMS별 캐시 구조

 

가. Oracle

  • SGA (System Global Area): Buffer Pool (디스크 페이지 캐시, 8KB 등), Shared Pool (실행 계획, SQL, 메타데이터), Redo Log Buffer, Large Pool 등
  • PGA (Program Global Area): 각 세션별 작업 메모리, ORDER BY/JOINS/커서 실행 시 사용
  • 자동 메모리 관리 (AMM): SGA, PGA 크기 자동 조절 지원 (메모리 제한 가능)
  • Buffer Pool 사이즈 결정: V$DB_CACHE_ADVICE 뷰로 최적 크기 추정 (캐시 미스율, IO 감소 효과 등)

 

나. SQL Server

  • Buffer Pool: 8KB 페이지 단위, 테이블/인덱스 데이터를 RAM에 적재
  • 메모리 자동 관리: 시스템의 여유 메모리를 최대한 사용, min/max server memory 설정으로 제한 가능
  • 메모리 사용량/버퍼 풀 크기 조회 쿼리 제공

 

다. PostgreSQL

  • Shared Buffers: DBMS 내 캐시, 8KB 페이지, 전체 RAM의 15~25% 권장
  • OS 캐시 의존: Direct I/O 미지원, OS 파일 시스템 캐시와 Double Buffering 발생
  • Vacuum 프로세스: 오래된/삭제된 버전 페이지 정리, GC와 유사
  • effective_cache_size: OS 캐시 크기를 PostgreSQL 옵티마이저에 알려줌(쿼리 플랜 최적화)

 

라. MySQL(InnoDB)

  • Buffer Pool: 16KB 페이지, 전체 RAM의 80~90%까지도 할당 권장 (특히 대용량 시스템)
  • Direct I/O 지원: OS 캐시 우회 가능, Double Buffering 방지
  • SHOW ENGINE INNODB STATUS로 상세 메모리/버퍼 풀 통계 확인 가능

 

1.5 캐시 크기, 정책과 성능의 상관관계

  • Buffer Pool/Shared Buffers 크기가 작으면 캐시 미스율 증가 → 더 많은 디스크 IO → 성능 저하
  • 반면, 너무 크게 설정하면 GC, vacuum 등 유지 보수 작업이 느려지고, OS/DB 메모리 경쟁 심화
  • 따라서 적정 크기로 설정하는 것이 중요
    • Oracle/SQL Server/MySQL: RAM의 70~90%
    • PostgreSQL: RAM의 15~25% (나머지는 OS 캐시)

 

1.6 캐시 히트율과 최적화

  • 캐시 히트율이 높을수록 즉, 캐시 미스가 적을수록 DB나 디스크 접근 없이 메모리에서 데이터 제공 → 대폭적인 성능 향상
  • 캐시 미스 시에는 데이터베이스 또는 디스크에서 데이터를 읽어야 하므로 응답시간이 급격히 증가
  • 따라서 히트율을 정기적으로 모니터링하고, 빈번히 사용되는 쿼리/테이블/인덱스가 캐시에 적재되도록 테이블 구조, 인덱스, 쿼리, 메모리 크기 등 조정하는 것을 권장

 

1.7 기타 운영체제 캐시, Direct I/O 등 주의사항

  • Direct I/O 적용 시 OS 캐시를 우회, DB가 자체적으로 페이지 캐시 관리 (Oracle, MySQL 등)
    • Double Buffering 방지, RAM 중복 사용 최소화하는 것을 권장

 

  • PostgreSQL은 OS 캐시와 Shared Buffers 모두 사용
    • shared_buffers는 15~25%로 제한, 나머지는 OS 페이지 캐시 할당하는 것을 권장

 

2. 애플리케이션 레벨 캐싱

 

2.1 애플리케이션 레벨 캐시의 필요성

  • DB 캐시 (버퍼 풀, 인메모리 캐시)는 I/O 최적화에 탁월함
    • 디스크 읽기/쓰기 횟수 최소화, 읽기 성능 극대화
    • DB 내부에서만 관리되므로 일관성이 매우 강력 (변경 즉시 반영, 캐시 미스 = DB 최신)

 

  • 하지만 DB 캐시도 한계가 있음
    • 대용량 JOIN, 정렬, 윈도우 함수, JSON/ARRAY 처리, Recursive 쿼리 등 메모리/CPU 집약 연산은 DB 캐시만으로 커버 불가
    • 실행계획이 아무리 최적이어도 대량 데이터/연산은 느릴 수밖에 없음

 

  • 애플리케이션 레벨 캐시는 복잡한 다중 JOIN, 집계, 분석 쿼리 결과를 한 번 계산해서 애플리케이션 레벨 (분산 캐시 등)에 저장한 뒤 O(1)로 결과를 제공
    • 조회 성능 극대화, DB 부하 감소
    • 데이터베이스와 분리된 확장성, 장애 격리, 트래픽 버퍼 역할 수행
    • DB 장애/점검 시에도 읽기만 제공된다면 서비스 연속성 확보 가능
      • i.g. StackOverflow가 DB 정기점검 중에도 캐시 기반 읽기 서비스 유지

 

2.2 대표적 애플리케이션 레벨 캐시 솔루션

  • Redis: 분산 인메모리 키-값 저장소, 데이터 구조 지원, pub/sub, TTL 등
  • Memcached: 단순 분산 메모리 캐시, 키-값, TTL
  • Hazelcast, Ehcache: JVM 내장/분산 캐시 (자바 친화적)
  • Aerospike, Etcd: 분산 캐시/NoSQL DB, 고가용성/확장성
  • Kafka, Debezium 등: CDC (Change Data Capture) 이벤트 기반 캐시 동기화에 사용

 

2.3 캐싱 계층과 동기화 전략

  • 일반적인 구조는 다음과 같음
    • DB 캐시: 버퍼 풀/OS 캐시
    • 애플리케이션 캐시: 분산 캐시 i.g. Redis/Memcached
    • ORM 2차 캐시: Hibernate, JPA 2nd-level cache
    • 1차 캐시: Persistence Context, 세션 단위

 

  • 일반적으로 적용하는 전략은 다음과 같음
    • Cache-aside (Write/Read-through/Write-behind 등)
      • 애플리케이션이 DB와 캐시를 모두 직접 갱신
      • 캐시 미스시 DB에서 읽고, 수정/삭제시 캐시 무효화 또는 비동기 업데이트
    • Change Data Capture (CDC)
      • 트리거나 Redo Log, Binlog, WAL 등 DB 변경 이벤트를 추출하여 캐시 동기화
      • 대표적인 오픈소스는 Debezium (다수 DB 지원)

 

 

2.4 캐시 동기화 어려움과 일관성 이슈

  • DB가 단일 진실의 원본(Single Source of Truth): 캐시와 DB가 반드시 일관되게 동기화되어야 함
  • XA 트랜잭션 미지원: 분산 캐시는 XA/XID 트랜잭션을 지원하지 않는 것이 일반적이므로 동기화 실패 혹은 롤백 시 데이터 불일치 가능성 높음
  • 동기화를 위해 다음 방식을 채택하는 것을 권장
    • Invalidate:  롤백되더라도 캐시 미스만 발생하도록 DB 변경 후 캐시 항목 삭제 (가장 안전하지만 캐시를 자주 무효화하기 때문에 효율은 떨어짐)
    • Async Update: DB 커밋 후 캐시 비동기 업데이트 (성능↑, 일관성↓), 여러 트랜잭션이 짧은 시간에 구버전 캐시 데이터를 읽을 수 있음

 

2.5 CDC (Change Data Capture)와 트리거/로그 활용

데이터 변경을 추적하기 위해 다음 옵션을 채택할 수 있습니다.

  • 트리거 기반 감사 로그 테이블: INSERT/UPDATE/DELETE 시 트리거로 감사 로그에 기록하고 해당 로그를 기반으로 캐시 업데이트
    • i.g. book_audit_log 테이블에 old/new row, DML 유형, 타임스탬프 기록

 

  • Redo log/Binlog/WAL 기반 CDC: DB 트랜잭션의 커밋된 변경을 로그에서 추출 (비동기)
    • Debezium, Kafka 등으로 실시간 이벤트 스트림 생성, 캐시 동기화

 

2.6 대규모/대용량 데이터 캐시 관리의 어려움

  • 계층적 (aggregate) 데이터 전체를 캐시에 저장할 경우 부모 엔티티 (i.g. 회사명, 태그명 등) 변경 시, 모든 관련 캐시 데이터를 일일이 업데이트/무효화해야 함 (갱신 ripple effect)
  • 캐시 설계상 aggregate를 분해하여, 각 주요 파트 (게시물, 태그, 투표 등)를 키 단위로 별도로 캐싱하면 ripple effect를 완화할 수 있지만 키 수가 많아지고, 여러 번의 조회 및 저장이 필요함

 

2.7 실무 적용 팁

  • 읽기 부하가 크거나, DB 장애 시 가용성 보장이 필요하다면 반드시 애플리케이션 레벨 캐시 (분산 인메모리 캐시) 적극 도입하는 것을 권장
  • 캐시 동기화는 비즈니스 상황에 맞게 적용 필요
    • 동기(Invalidate), 비동기(CDC, 이벤트), TTL, 버전 관리 등 상황별로 조합

 

  • 캐시 일관성과 효율성 트레이드오프를 항상 인지하고 설계해야 함
  • CDC (Debezium 등) 활용 시 DB 트랜잭션과 캐시 동기화 파이프라인의 장애/지연/누락에 주의
  • 대규모 aggregate 캐시 설계 시 ripple effect를 고려해 분해 캐시, TTL, 대량 무효화 전략 등 다각적으로 대비하는 것을 권장

 

3. Hibernate 2차 캐시

 

3.1 Hibernate 2차 캐시란?

  • 1차 캐시
    • Hibernate의 Persistence Context (Session, EntityManager) 단위
    • 트랜잭션/세션 범위 내에서만 엔티티 객체를 메모리에 저장 (즉, DB 한 번만 로드)

 

  • 2차 캐시 (Second-level Cache):
    • 애플리케이션 전체 또는 EntityManagerFactory/SessionFactory 단위의 공유 캐시
    • 같은 엔티티 (또는 컬렉션, 쿼리 결과 등)를 여러 세션/트랜잭션에서 재사용 가능
    • DB 접근 (읽기/쓰기) 부하를 효과적으로 감소시킴

 

3.2 2차 캐시의 필요성

  • Primary/Replica 구조에서 읽기 트래픽 (scale-out)은 Replica 노드 추가로 쉽게 확장 가능
    • 쓰기 트래픽 (scale-up)은 Primary 노드 한 대에 집중하기 때문에 수직 확장 (scale-up)만 가능

 

  • 쓰기 부하를 분산하거나, Primary 부하를 줄이기 위해서는
    • Hibernate 2차 캐시를 활용하여 읽기 요청의 일부를 DB가 아닌 캐시에서 처리
    • 읽기-쓰기 트랜잭션에서 읽기 부하의 상당 부분을 캐시에 오프로딩

 

3.3 2차 캐시의 종류 및 옵션

  • Entity Cache: 엔티티 단위 캐싱
  • Collection Cache: 컬렉션 (List/Set) 관계 식별자 캐싱
  • Query Cache: 쿼리 결과 (식별자/프로젝션) 캐싱
  • Natural Id Cache: 비즈니스 키 기반 엔티티 식별자 캐싱

 

3.4 2차 캐시 동작 순서

  • 엔티티 조회 시 1차 캐시 (Persistence Context) → 2차 캐시 → DB 쿼리
    • 2차 캐시에 없는 경우, DB에서 로드 후 2차 캐시에 저장

 

가. 1차 캐시 (PersistenceContext, Session/EntityManager)

  • 영속성 컨텍스트는 현재 트랜잭션/세션 범위 내에서만 유효한 엔티티 캐시
  • JPA의 EntityManager와 Hibernate의 Session이 1차 캐시 역할을 수행
  • 엔티티를 조회하면 가장 먼저 1차 캐시에서 해당 엔티티의 인스턴스를 찾음
  • 1차 캐시에 이미 관리되고 있는 엔티티가 있다면, 별도의 DB 쿼리나 2차 캐시 접근 없이 즉시 반환됨
    • 동일 트랜잭션/세션에서 여러 번 조회해도 DB 쿼리는 1번만 발생

 

나. 2차 캐시(Second-Level Cache)

  • 1차 캐시에 엔티티가 없고 2차 캐시가 활성화된 경우, 2차 캐시에서 해당 엔티티를 찾음
  • 2차 캐시는 SessionFactory/EntityManagerFactory 단위로 여러 세션/트랜잭션에서 공유됨
  • 2차 캐시에서 엔티티를 찾으면(캐시 히트), DB 접근 없이 캐시 된 ‘로드 상태’로 엔티티를 복원해서 반환
  • 2차 캐시에 엔티티가 없다면(캐시 미스), 다음 단계로 넘어감

 

다. DB 쿼리

  • 1차/2차 캐시 모두에 엔티티가 없다면, Hibernate가 실제로 DB에 SELECT 쿼리를 실행하여 엔티티 데이터를 조회
  • DB에서 엔티티를 가져온 뒤 1차 캐시에 엔티티를 등록하고 2차 캐시가 활성화되어 있다면, 엔티티의 ‘로드 상태’를 2차 캐시에 저장 (다음 세션/트랜잭션에서 재사용)

 

3.5 Hibernate 2차 캐시 설정 방법

 

 

3.6 쿼리 캐시 설정 방법

 

 

3.7 Natural Id (비즈니스 키) 캐시

  • Natural Id (비즈니스 키)는 데이터베이스에서 엔티티를 고유하게 식별할 수 있는 속성으로, 보통 비즈니스적으로도 유일성이 보장되는 값
    • Primary Key와는 별도로, 비즈니스 로직에서 자주 사용하는 자연키 기반 조회가 필요할 때가 많음
    • i.g. 책 테이블의 ISBN, 상품의 SKU 등

 

  • @NaturalId + @NaturalIdCache를 통해 비즈니스 키 기반 엔티티 캐싱
    • Hibernate API를 사용하면 Natural Id로 엔티티를 직접 조회 가능
    • DB round-trip을 최소화하여 매우 빠름
    • 엔티티가 수정/삭제/추가되면 자연키 캐시와 엔티티 캐시가 함께 무효화되어 최신 상태 유지
    • JPA 표준에는 없고 Hibernate 고유 API
    • 단, 변경이 자주 발생하면 캐시 무효화와 동기화 비용이 증가하므로 Natural Id 값은 실제로는 거의 변하지 않아야 함
    • 또한, 캐시 제공자 (RegionFactory)에 따라 설정과 성능이 달라질 수 있으므로 운영 환경에서 캐시 동작/효율을 반드시 모니터링 필요

 

 

  • 동작 방식은 다음과 같음
    • slug 값으로 Natural Id 쿼리를 실행하면
    • Hibernate는 먼저 Natural Id 캐시에서 해당 slug → id 매핑을 찾음
    • 없으면 DB에서 SELECT id FROM post WHERE slug = 'slug' 실행 후 결과를 캐시에 저장
    • id 값을 찾으면, 곧바로 엔티티 캐시 (2차 캐시)에서 id로 엔티티를 찾음
    • 엔티티 캐시에도 없으면 DB에서 SELECT * FROM post WHERE id = ? 실행

 

4. Hibernate 2차 캐시 동시성 전략

  • Hibernate 2차 캐시는 데이터 일관성과 동시성 요구에 따라 네 가지 캐시 동시성 전략을 제공
    • READ_ONLY: 읽기 전용/불변 데이터에 적합 (immutable)
    • NONSTRICT_READ_WRITE: 데이터 변경이 드물고, 약간의 일관성 손실을 허용할 수 있을 때 채택하는 전략
    • READ_WRITE: 강력한 일관성이 필요할 때 적용 (동시성 제어, 소프트락 사용)
    • TRANSACTIONAL: JTA 트랜잭션 환경, XA 트랜잭션에 참여하는 경우

 

가. READ_ONLY 전략

  • 변경 불가 능(immutable)한 엔티티 및 컬렉션 캐시에만 사용
    • i.g. 코드 테이블, 읽기 전용 참조 데이터)

 

  • 해당 전략의 특징은 다음과 같음
    • 불변 엔티티/컬렉션에만 설정 가능 (@Immutable 강제)
    • 캐시 동기화 필요 없음, 캐시에 저장된 값이 영원히 유효
    • 모든 CRUD 작업에서 데이터베이스-캐시 일관성 유지가 쉬움

 

 

  • 해당 전략 채택 시 다음과 같이 동작 
    • persist 시 write-through로 캐시에 저장
    • 읽기 시 캐시 hit/miss 카운트로 통계 확인 가능
    • 업데이트/삭제 시 예외 발생 또는 무시

 

나. NONSTRICT_READ_WRITE 전략

  • 데이터 변경이 드물고, 복제된 환경이나 분산 캐시에서 '최종적 일관성 (eventual consistency)'만 보장해도 되는 경우 적용
    • 변경이 자주 일어나지 않는 데이터, 완벽한 일관성이 필요 없는 환경

 

  • 해당 전략의 특징은 다음과 같음
    • 업데이트/삭제 시 캐시에 있는 항목 무효화
    • 캐시 무효화와 DB 업데이트 사이에 시간 차이 발생 가능 (동시성 문제 가능성)
    • 분산 환경 (여러 웹 노드, 복제본 등)에서 락 없이 작동 가능

 

  • 해당 전략 채택 시 다음과 같이 동작 
    • persist 시에는 캐시에 저장되지 않음, 최초 조회 시에만 캐시 저장 (read-through)
    • 업데이트/삭제 시 캐시 즉시 무효화, 이후 최초 조회 시에 다시 캐시 됨

 

다. READ_WRITE 전략

  • 데이터 일관성이 매우 중요한 환경에서 적용
    • i.g. OLTP, 단일 노드/인스턴스와 같이 강력한 동시성 보장 필요한 환경

 

  • 해당 전략의 특징은 다음과 같음
    • 캐시에 소프트락 객체를 사용, 트랜잭션 커밋 시까지 락 보유
    • 동시 수정 충돌 방지, 커밋 전까지 다른 트랜잭션이 캐시 된 값을 볼 수 없음
    • 강력한 일관성 보장, 단일 노드 (혹은 분산 캐시에서 락 동기화 지원) 환경에서 적합

 

  • 해당 전략 채택 시 다음과 같이 동작
    • persist/update/delete 시 캐시와 DB 모두 write-through
    • update/delete 시 캐시에 소프트락이 생성, 커밋 후에만 실제 값으로 대체
    • 다른 트랜잭션이 락 상태를 만나면 DB에서 직접 로드

 

라. TRANSACTIONAL 전략

  • XA 트랜잭션 (JTA) 환경, 다수의 DB/캐시 시스템이 2단계 커밋(two-phase commit)에 참여할 때 적용
    • @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL) 어노테이션 지정 필요
    • XA 지원 DataSource, 트랜잭션 매니저, 캐시 프로바이더 필요

 

  • 해당 전략의 특징은 다음과 같음
    • JTA/XA 트랜잭션 매니저 필요 (Bitronix, Atomikos, Java EE JTA 등)
    • 캐시와 데이터베이스 커밋/롤백이 완전히 동기화됨 (모두 성공/모두 실패)

 

4.1 실무 적용 팁

  • 쿼리 캐시는 별도 활성화 필요
    • 쿼리 실행 결과 (식별자/프로젝션)만 캐시
    • 파라미터, 쿼리 문자열, 필터 등 모두가 캐시 키에 포함

 

  • 엔티티/컬렉션이 변경되면 관련 쿼리 캐시도 무효화하는 것을 권장
    • 쿼리 캐시가 조회될 때, 결과에 저장된 타임스탬프 < 현재 테이블 타임스탬프이면 해당 쿼리 결과가 더 이상 유효하지 않은 것으로 간주하고 무효화
    • Native SQL을 사용할 때는 .addSynchronizedEntityClass(Entity.class) 또는 .addSynchronizedQuerySpace("table_name") 메서드로 해당 쿼리가 어느 테이블/엔티티에 영향을 미치는지 Hibernate에 명시적으로 알려줌
      • 이렇게 적용하면 정확히 관련된 쿼리 캐시만 무효화되어, 나머지 쿼리 캐시는 계속 재사용 가능


 

  • 컬렉션 캐시는 항상 read-through
    • 컬렉션은 데이터베이스에서 처음 읽을 때만 캐시에 저장
    • 자식 엔티티들도 반드시 캐시 되어야 N + 1 문제 회피

 

참고

인프런 - 고성능 JPA & Hibernate (High-Performance Java Persistence)

반응형

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

[Hibernate/JPA] 트랜잭션과 동시성 제어 패턴  (0) 2025.06.09
[Hibernate/JPA] Fetching  (0) 2025.06.09
[Hibernate/JPA] Batching  (0) 2025.06.04
[Hibernate/JPA] Statement  (0) 2025.06.04
[Hibernate/JPA] 영속성 컨텍스트  (0) 2025.06.04