1. 개요
JPA를 사용하다 보면 자연스럽게 1차 캐시(First-Level Cache)를 사용하게 됩니다. 트랜잭션 내에서 같은 엔티티를 조회하면 DB 조회를 생략해주니까요. 하지만 1차 캐시는 트랜잭션이 끝나면 사라진다는 한계가 있습니다.
"다른 트랜잭션, 심지어 다른 사용자가 조회했던 데이터를 재사용할 수는 없을까?"
이 질문에 대한 해답이 바로 2차 캐시(Second-Level Cache)입니다. 애플리케이션 전체 범위에서 엔티티를 공유하여 성능을 높이는 방법을 소개합니다.
2. 간단한 설명
JPA의 캐싱 계층은 두 단계로 나뉩니다.
- 1차 캐시 (기본):
EntityManager(영속성 컨텍스트) 단위. 요청(트랜잭션)이 들어왔다 나갈 때까지만 유효합니다. - 2차 캐시 (옵션):
EntityManagerFactory단위. 애플리케이션이 종료될 때까지 살아있으며, 모든 클라이언트가 공유합니다.
[작동 방식]
- 엔티티 조회 요청(
findById등)이 들어옵니다. - 1차 캐시에 있는지 봅니다. (없으면 넘어감)
- 2차 캐시에 있는지 봅니다.
- 있으면: 원본 객체가 아닌 복사본(Copy)을 반환합니다. (동시성 문제 방지)
- 없으면: DB에서 조회 후 2차 캐시에 보관하고 반환합니다.
주로 Ehcache, Infinispan, Redis 같은 외부 라이브러리를 구현체로 연결해서 사용합니다.
3. 사용 사례
2차 캐시가 모든 상황에 만능이 아닙니다. 설정이 까다롭고, 잘못 쓰면 데이터 불일치(Inconsistency)가 발생하기 쉽습니다.
사례: 변경이 드문 기준 정보(Reference Data) 조회
'직급 정보', '카테고리 목록', '공통 코드'처럼 자주 읽히지만 거의 변하지 않는 데이터가 JPA를 사용하는데 적합한 대상입니다.
[Before] 매 요청마다 DB 조회
수천 명의 사용자가 메인 페이지에 접속할 때마다 '카테고리' 테이블을 조회한다면 DB 커넥션 풀이 모두 소진되어 버릴 수 있습니다.
// 일반적인조회: 2차 캐시 미적용 시 매번 SQL 실행
Category category = em.find(Category.class, 1L);
[After] @Cacheable 적용으로 DB 부하 감소
Hibernate 설정과 어노테이션 추가만으로 수천 번의 쿼리를 1번으로 줄일 수 있습니다.
1. 엔티티 설정 (Category.java)
```java
import jakarta.persistence.Cacheable;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Cacheable // JPA 표준
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY) // 하이버네이트 설정
public class Category {
@Id
private Long id;
private String name;
}
2. 설정 파일 (application.yml)
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
region:
factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory이제 em.find(Category.class, 1L)를 100번 호출해도, 실제 SQL은 처음에 딱 한 번만 실행됩니다. 나머지는 메모리(2차 캐시)에서 가져옵니다.
주의할 점: 동시성 전략 (Concurrency Strategy)
엔티티의 데이터 변경 빈도와 정합성 중요도에 따라 전략을 신중하게 선택해야 합니다.
READ_ONLY(읽기 전용)- 설명: 데이터가 절대 수정되지 않거나, 수정 시 전체 캐시를 날려도 되는 경우 사용합니다. 성능이 가장 좋습니다.
- 추천 사례: 국가 코드, 환율 코드, 변하지 않는 시스템 설정, 카테고리 대분류.
NONSTRICT_READ_WRITE(느슨한 읽기-쓰기)- 설명: 변경이 가능하지만, 아주 짧은 시간의 데이터 불일치(Stale Data)를 허용합니다. 락을 걸지 않아 빠릅니다.
- 추천 사례: 게시글의 '좋아요' 수, 댓글 목록, 블로그 글 본문 (수정 직후 0.1초 정도 과거 버전이 보여도 괜찮은 경우).
READ_WRITE(엄격한 읽기-쓰기)- 설명: 데이터가 수정될 때 'Soft Lock'을 걸어 엄격한 데이터 정합성을 보장합니다. 그만큼 오버헤드가 큽니다.
- 추천 사례: 상품 가격, 재고 수량, 사용자 프로필 (돈이나 개인정보와 직결되어 정합성이 필수적인 경우).
4. 맺음말
JPA 2차 캐시는 엔티티 중심의 강력한 성능 최적화 도구이지만, 그만큼 주의해서 사용해야 합니다. 특히 다음 두 가지를 명심해야 합니다.
- 복잡한 조회 쿼리(JPQL, QueryDSL DTO 조회)에는 적용되지 않습니다. (이건 쿼리 캐시 영역)
- 테이블이 아닌 객체(Entity) 단위로 캐싱됨을 명심해야 합니다.
** 객체 단위 캐싱이란?
JPA 2차 캐시는 엔티티의 @Id를 키(Key)로 사용해 식별합니다. 따라서 JDBC나 MyBatis 등을 통해 직접 SQL로 DB를 업데이트하면, 캐시는 이 변경 사항을 전혀 알 수 없습니다.
결국 DB에는 새 값이 들어갔는데, 애플리케이션은 캐시에 있는 옛날 값(Stale Data)을 계속 보여주는 심각한 문제가 발생할 수 있습니다. 2차 캐시를 쓴다면 모든 데이터 조작을 반드시 JPA를 통해서만 해야 안전합니다.
따라서 비즈니스 로직에 핏한 유연한 캐싱이 필요하다면 앞서 다룬 '서비스 레이어의 이중 캐시'가 낫고, DB 테이블과 1:1로 매핑되는 정적 데이터의 부하를 줄이고 싶다면 JPA 2차 캐시가 괜찮은 선택지가 될 것입니다.
'프로그래밍 PROGRAMMING > 자바 JAVA AND FRAMEWORKS' 카테고리의 다른 글
| [JAVA/Cache] 이중 캐시(Two-Level Cache) 전략: 성능과 정합성 모두 만족하기 (0) | 2026.01.29 |
|---|---|
| [JAVA/Cache] 캐시 스탬피드(Cache Stampede): 만료된 순간 시스템이 멈추는 이유와 해결책 (2) | 2026.01.28 |
| [Spring] 트랜잭션 전파 속성(Propagation) 심층 분석과 활용 전략 (0) | 2026.01.24 |
| [Spring/JPA] Spring JPA N+1 문제 정리: 원인, 예시 코드, Fetch Join/EntityGraph 해결 전략 (0) | 2026.01.16 |
| [Spring] @Transactional 동작 원리 및 트랜잭션 미적용 원인 분석 (0) | 2026.01.15 |