서비스를 운영하다 보면 조회 성능 때문에 캐시를 고민하게 됩니다. 보통은 Redis를 먼저 떠올리지만, 모든 환경에서 Redis를 쓸 수 있는 건 아닙니다. 망 분리, 비용, 네트워크 레이턴시 같은 현실적인 이유로 외부 캐시가 어려운 경우도 많습니다.
이럴 때 생각보다 잘 먹히는 선택이 로컬 캐시(Local Cache) 입니다.
조건만 맞으면, DB 부하를 크게 줄이면서 응답 속도도 확실히 개선할 수 있습니다.
로컬 캐시는 언제 쓰는 게 맞을까?
아무 데나 쓰는 건 아니고, 아래 조건이면 효과가 확실합니다.
1. Redis 같은 외부 캐시를 쓰기 어려운 경우
- 폐쇄망 / 온프레미스 환경
- 네트워크 호출 자체가 부담인 구조
- 인프라 비용 최소화가 필요한 상황
이럴 때는 애플리케이션 메모리를 그대로 쓰는 게 가장 단순하고 빠릅니다.
2. 조회 비중이 높은 데이터
- 공통 코드 (국가 코드, 상태 코드 등)
- 설정값
- 거의 변경되지 않는 데이터
읽기가 대부분이면 캐시 효율이 잘 나옵니다.
3. 응답 속도가 중요한 API
로컬 캐시는 네트워크를 타지 않습니다.
메모리 접근이라서 Redis보다도 빠릅니다.
로 컬 캐시를 쓰면 얻는 이점
실제로 체감되는 건 아래 3가지입니다.
- 응답 속도 개선 → DB 접근 감소
- DB 부하 감소 → 트래픽 몰릴 때 안정적
- 구조 단순화 → 외부 캐시 없이 운영 가능
특히 조회 트래픽이 많은 서비스에서 효과가 큽니다.
Spring에서 적용하는 방법 (Caffeine 기준)
실무에서는 단순 Map 대신 Caffeine을 많이 씁니다.
1. 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
2. 기본 설정
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("codeCache");
cacheManager
.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000) // 메모리 보호
.expireAfterWrite(10, TimeUnit.MINUTES) // TTL
);
return cacheManager;
}
}
여기서 핵심은 두 가지입니다.
maximumSize → 메모리 제한 (필수)
expireAfterWrite → 데이터 갱신 전략
3. 사용 예시 (Cache-Aside 패턴)
@Service
public class CodeService {
@Cacheable(value = "codeCache", key = "#key")
public Code getCode(String key) {
return codeRepository.findByKey(key);
}
}
흐름은 단순합니다.
- 캐시 확인
- 없으면 DB 조회
- 결과를 캐시에 저장
Init 시 캐시 미리 올릴까?
자주 조회되는 데이터라면 초기 로딩을 해두는 게 낫습니다.
@Component
@RequiredArgsConstructor
public class CacheWarmUp {
private final CodeRepository codeRepository;
private final CacheManager cacheManager;
@PostConstruct
public void init() {
Cache cache = cacheManager.getCache("codeCache");
codeRepository.findAll()
.forEach(code -> cache.put(code.getKey(), code));
}
}
이렇게 하면 첫 요청이 느려지는 문제를 피할 수 있습니다.
TTL은 꼭 설정해야 할까?
케이스에 따라 다릅니다.
TTL이 필요한 경우
- 데이터가 변경될 수 있음
- 정합성이 중요한 경우
없어도 되는 경우
- 코드 테이블처럼 거의 변하지 않는 데이터
애매하면 TTL을 넣는 쪽이 안전합니다.
메모리 관리 (이거 안 하면 장애 납니다)
로컬 캐시는 JVM 메모리를 직접 쓰기 때문에 관리가 필수입니다.
1. 최대 크기 제한
.maximumSize(10_000)
이거 없으면 계속 쌓입니다.
2. 큰 객체 그대로 넣지 않기
필요한 데이터만 캐싱하는 게 좋습니다.
3. TTL 또는 eviction 전략 필수
오래된 데이터는 정리되어야 합니다.
4. GC 영향 고려
캐시가 커질수록 GC 부담이 커집니다.
잘못하면 응답 지연으로 이어집니다.
다중 서버 환경에서 반드시 고려할 점
여기서 많이 놓칩니다.
로컬 캐시는 공유되지 않는다
- 서버 A 캐시 ≠ 서버 B 캐시
- 서로 동기화되지 않음
그래서 데이터 불일치가 생깁니다.
해결 방법은 3가지
1. TTL로 자연스럽게 맞추기
.expireAfterWrite(5, TimeUnit.MINUTES)
- 일정 시간 후 자동 갱신
- 간단하지만 완벽하진 않음
2. 이벤트 기반 캐시 무효화
데이터 변경 시 모든 서버에 알림을 보내 캐시를 제거하는 방식입니다.
// 데이터 변경 후 이벤트 발행
eventPublisher.publishEvent(new CodeUpdatedEvent(key));
// 이벤트 수신 후 캐시 제거
@EventListener
public void handle(CodeUpdatedEvent event) {
cacheManager.getCache("codeCache")
.evict(event.getKey());
}
정합성이 중요한 경우 이 방식이 필요합니다.
3. 로컬 캐시 + Redis 같이 사용
Local Cache → Redis → DB
속도와 정합성을 동시에 가져가는 구조입니다.
정리
로컬 캐시는 다음 상황에서 효과적입니다.
- Redis를 사용하기 어려운 환경
- 조회 중심 서비스
- 빠른 응답이 중요한 API
- 데이터 변경이 적은 경우
대신 반드시 고려해야 합니다.
- 메모리 제한 설정
- TTL 또는 eviction 전략
- 캐시 미스 처리
- 다중 서버 환경에서의 동기화 전략
'백엔드 개발' 카테고리의 다른 글
| Spring Boot 3.x → 4.x 마이그레이션 (실무 기준 변경 포인트 정리) (0) | 2026.04.06 |
|---|---|
| Spring 동시성 문제 해결 (synchronized부터 Redis Lock, Lua, Redisson까지 정리) (0) | 2026.04.06 |
| java.lang.NullPointerException 원인 정리 (0) | 2026.03.31 |
| Spring Boot + SAP JCo 연동 SDK 만들기 (Builder 패턴 적용) (0) | 2026.03.27 |
| Spring Boot Redis 실무 활용: 클러스터/싱글 + 동기/비동기 + TTL + LocalDateTime (0) | 2026.02.13 |