캐시 Cache
데이터나 값을 미리 복사해 놓은 임시 장소를 가리킨다. 캐시를 사용하면 DB나 서버에서 반복적으로 동일한 데이터를 가져오거나 연산이 필요해 시간이 오래 걸리는 경우 사용할 수 있다. 캐시를 사용하면 서버의 부하를 줄이고 응답 속도와 성능을 높일 수 있다.
Local Cache vs Global Cache
예전에 면접에서 ‘로컬 캐시를 사용하지 않고 굳이 redis 같은 캐시를 따로 사용하는 이유가 뭐냐’ 라는 질문을 받았었다. 답변을 제대로 못했었는데, 이에 질문자가 ‘여러 대의 서버를 사용하는 경우 서버마다 따로 로컬 캐시를 관리하면 데이터가 일치하지 않을 수 있다. 캐시 서버를 따로 구축해서 모든 서버가 하나의 캐시 서버를 사용하여 동일한 데이터 사용할 수 있도록 해야한다.’고 알려주었다.
캐시를 사용할 때는 데이터의 일치성이 중요하다. 현재 DB에 있는 데이터와 일치해야 하고, 각 서버에서 사용하는 캐시 데이터도 모두 동일해야 한다. 상황에 따라 로컬 캐시가 아닌 글로벌 캐시를 따로 구축하여 사용한다.
레디스 Redis
캐시 시스템 하면 레디스가 가장 먼저 떠오른다. 레디스는 캐시 시스템 중 가장 유명한 메모리 기반 비관계형 데이터베이스다. key-value 구조로 다양한 비정형 데이터를 저장하고 관리할 수 있다.
캐시 히트 Cache hit
요청한 데이터가 캐시에 존재하는 경우
캐시 미스 Cache miss
요청한 데이터가 캐시에 존재하지 않는 경우 혹은 최신 데이터가 아닌 경우
캐시 적용하기
우리 서비스는 모종의 이유로 앱에서 같은 api 호출이 많이 일어나기도 하고, 동일한 데이터를 외부 api를 통해서 가져오는 경우가 많다. 하루 한번만 업데이트 되는 데이터나 앱에서 사용하는 key-value 값, 이미지 등 업데이트가 거의 되지 않으면서 동일한 응답 값을 반환하는 데이터들을 캐싱하려고 한다.
스프링 내장 캐시를 사용하는 이유
스프링 내장 캐시는 서버와 생명 주기가 같아서 서버를 재시작하면 초기화된다. 우리 서비스는 단일 서버를 사용하고 있고 사용자도 많지 않아 스프링 내장 캐시면 충분하다고 판단했다. 스프링에서 제공해주는 캐시 패키지는 추후에 캐시 시스템을 변경해도 그대로 사용할 수 있다. 여러 캐시 시스템에 대한 별도 설정이 필요하지 않고, 사용하는데 어렵지 않아 선택하였다.
gradle에 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-cache'
메서드에 어노테이션을 추가하여 사용한다.
@Cacheable
@Cacheable(value = "appImages")
public List<AppImageListDto> getAppImages() {
...
}
@Cacheable(value = "getMemberInfo", key = "#memberId")
public MemberDto getMemberInfo(Long memberId) {
...
}
- @Cacheable 어노테이션을 추가하면 메서드 실행 전 캐시에서 데이터를 가져와 반환하고 데이터가 없으면 메서드를 실행하여 값을 반환하기 전 캐시를 업데이트
- key는 문자열로 저장되며 파라미터 사용 가능
- 객체 안의 값을 key로 사용하고 싶으면 #member.id 이런식으로 작성할 수 있다.
@CacheEvict
@CacheEvict(value = "appImages")
public Long savePageImage(ImageSaveRequest request) {
...
}
@CacheEvict(value = "getMemberInfo", key = "#request.memberId")
public void updateMemberInfo(UpdateMemberDto request) {
...
}
- 삽입, 수정, 삭제시 캐시를 제거할 수 있도록 설정
- key를 지정하면 해당 key에 대한 데이터만 제거
- 캐시 제거시 유의 사항
- 캐시를 제거할 때 저장된 모든 appImages 데이터를 삭제하려면 allEntries = true 설정을 추가해야 한다. 그러지 않으면 삭제가 되지 않는다.
- @CacheEvict(value = "appImages", allEntries = true)
@Caching - 다중 캐시 관리 가능
@Caching(evict = {
@CacheEvict(value = "getMembersNickname", allEntries = true),
@CacheEvict(value = "getMembers", allEntries = true)
})
캐시 라이브러리
spring-boot-starter-cache만 사용할 때 캐시가 어디에 저장이 되는지 궁금했는데, 공식 문서를 보면 특정 캐시 라이브러리를 추가하지 않으면 simple provider(in-memory)를 저장소로 사용한다고 한다. 지원하는 캐시 저장소는 아래와 같고 별도 설정이 없으면 위부터 차례로 감지한다.
- Generic
- JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
- Hazelcast
- Infinispan
- Couchbase
- Redis
- Caffeine
- Cache2k
- Simple
이 중에서 EhCache와 Caffeine 중에 하나로 고르려고 한다.
EhCache
- 가장 널리 사용되는 JAVA 기반 캐시로 직렬화된 데이터 객체를 저장하는 메모리 블럭이다.
- in-memory, offHeap, 디스크 3개의 스토리지에 저장이 가능하다.
- offHeap 공간: 메모리 힙 외부에 추가 유형의 메모리 저장소 사용. GC가 적용되지 않아 큰 캐시를 생성할 수 있다.
- LRU(Least Recent Used), LFU(Least Frequency Used), FIFO(First In First Out) 세 가지 알고리즘으로 메모리 공간이 가득 차면 요소를 제거한다.
Caffeine
- 자칭 높은 성능과 최적의 캐싱 라이브러리
- Google의 오픈소스 Guava Cache와 ConCurrentHashMap을 바탕으로 개선
- in-memory 저장
- Window TinyLfu 제거 정책 사용

https://dgraph.io/blog/refs/TinyLFU - A Highly Efficient Cache Admission Policy.pdf

https://www.sobyte.net/post/2022-04/caffeine/
Main Cache(전체 용량의 99%)
- Protected Cache(20%) - 자주 사용하는 데이터(제거되지 않음)
- Probation Cache(80%) - 자주 사용하지 않는 데이터(LRU 적용)
Window Cache(전체 용량의 1%)
- 새로운 데이터가 가장 먼저 쓰이는 곳
- 가득 찰 경우 LRU 적용하여 밖으로 제거
- TinyLFU 알고리즘에 의해 제거되거나 Probation Cache 영역에 저장
- Probation Cache 영역 데이터에 일정한 횟수 이상 접근 되면 Protected Cache 영역으로 승격
- Protected Cache 영역이 가득 차면 오래된 데이터 밖으로 이동되어 TinyLFU 알고리즘에 의해 제거되거나 Probation Cache 영역에 저장
TinyLFU 제거 매커니즘
- Window Cache, Protected Cache로부터 제거되는 데이터 ⇒ Candidate
- Probation Cache에서 제거되는 데이터 ⇒ Victim
- Candidate Cache 접근 > Victim Cache 접근 → Victim 제거
- Candidate Cache 접근 < Victim Cache 접근 && Candidate 접근 횟수 5번 이하 → Candidate 제거
- 둘 중 랜덤 제거
Caffeine 캐시 내부 알고리즘은 LFU와 LRU의 장점을 통합
- 서로 다른 캐시 영역에 서로 다른 특성을 가진 캐시 항목을 저장하여 최근에 생성된 캐시 데이터가 Window Cache로 들어가 제거되지 않음
- 자주 호출되는 데이터(LFU)는 Protected 영역에 들어가 LRU 적용이 안됨
- 호출 횟수, 호출 시간 두 자원에 대해 밸런스가 잘 되어있음
- 자주 호출되고 최근에 생성된 데이터들은 가능한 캐시에 유지
- 전통적인 LRU/LFU로 처리할 수 없던 케이스를 잘 처리함
EhCache vs Caffeine
참고 자료를 통해 비교했을 때, Caffeine이 EhCache보다 사용하기 쉽고 제거 알고리즘이 우월하여 단순 메모리 캐시 사용하기에 적합하다고 생각했다.
Caffeine 캐시는 만료시간 후 바로 제거되지 않고 이벤트가 발생할 때 초기화되는 문제가 있어 스케줄러로 따로 관리를 해야한다고 한다. 우리 서비스에서는 간단한 로컬 캐시로 사용하고 캐시 데이터가 많지 않을 것이라고 예상되어 Caffeine을 선택하기로 했다.
https://medium.com/naverfinancial/니들이-caffeine-맛을-알아-f02f868a6192
https://docs.spring.io/spring-boot/reference/io/caching.html
https://velog.io/@bey1548/스프링-캐시Cacheable-CacheEvict
'개발 > Spring' 카테고리의 다른 글
| 리팩토링 - 패키지를 넘나드는 캐시 (0) | 2025.02.12 |
|---|---|
| [Spring] Caffeine Cache 적용 (1) | 2025.02.11 |
| Spring 서버 로깅 1 - 로그 레벨 (0) | 2024.11.04 |
| 토비의 스프링 - 서비스 추상화 (0) | 2024.10.25 |
| [Querydsl] PageableExecutionUtils count query 오류 (0) | 2024.10.20 |