트래픽이 급증하는 서비스에서는 요청당 DB나 외부 API를 매번 호출하면 쉽게 병목이 발생합니다. 이 문제를 해결하기 위한 핵심 기술이 바로 캐싱(Caching)입니다.
1. 캐싱의 기본 개념
캐시는 자주 사용되는 데이터를 메모리나 고속 저장소에 임시로 보관하여, 다음 요청 시 빠르게 응답할 수 있도록 하는 기술입니다. 핵심 목적은 DB 부하를 줄이고, 요청 지연(latency)을 최소화하는 것입니다.
- Time-to-Live(TTL): 캐시된 데이터의 만료 시간
- Cache Miss: 캐시에 데이터가 없어 원본 데이터 소스(DB 등)에서 조회하는 경우
- Cache Hit: 캐시에서 데이터를 바로 반환한 경우
- Eviction Policy: 캐시 메모리 초과 시 오래된 데이터를 제거하는 정책 (LRU, LFU 등)
2. 캐싱 계층별 접근 전략
대규모 트래픽을 다루기 위해서는 단일 계층의 캐시로는 부족합니다. 보통 3단계 캐싱 구조를 설계합니다.
① 로컬 캐시 (In-memory Cache)
애플리케이션 내부에서 JVM 메모리를 활용해 데이터를 저장합니다. Spring Boot에서는 Caffeine이나 Ehcache 같은 라이브러리를 사용합니다.
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager("productCache");
manager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(10000));
return manager;
}
}
장점은 응답 속도가 매우 빠르다는 점이며, 단점은 인스턴스별로 캐시가 독립적이라 데이터 일관성이 깨질 수 있다는 점입니다.
② 분산 캐시 (Redis / Memcached)
여러 서버 간 공유 가능한 캐시로, 대규모 트래픽 환경에서는 사실상 필수입니다. Redis를 많이 사용하며, Spring Boot에서는 spring-boot-starter-data-redis로 쉽게 연동할 수 있습니다.
spring:
cache:
type: redis
data:
redis:
host: redis-server
port: 6379
@Cacheable(value = "userCache", key = "#userId")
public User findUser(Long userId) {
return userRepository.findById(userId).orElseThrow();
}
Redis는 캐시 외에도 Pub/Sub, 분산 락, 세션 스토리지로도 활용할 수 있어 범용성이 높습니다.
③ CDN 캐시 (CloudFront, Akamai 등)
정적 파일(이미지, JS, CSS, 동영상 등)은 S3와 같은 오브젝트 스토리지에 저장하고, CDN을 통해 전 세계 엣지 서버에 캐시합니다. 사용자와 물리적으로 가까운 서버에서 데이터를 전송하므로 지연 시간이 획기적으로 줄어듭니다.
요청 흐름: 사용자 → CDN → (캐시 HIT 시 즉시 응답) → 원본 서버
3. 캐시 갱신(Invalidation) 전략
캐시는 성능을 높이지만, 데이터의 최신성이 유지되어야 합니다. 이를 위해 캐시 무효화 정책을 잘 설계해야 합니다.
- TTL(Time-based): 일정 시간이 지나면 자동으로 삭제
- Write-through: DB에 쓰기와 동시에 캐시도 갱신
- Write-behind: 캐시에 먼저 반영 후 일정 주기로 DB에 반영
- Manual Eviction: 데이터 변경 시 특정 키만 수동 삭제
예를 들어, 상품 가격이 변경될 때 해당 상품 ID의 캐시만 삭제하도록 설계할 수 있습니다.
@CacheEvict(value = "productCache", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
4. 트래픽 폭주 시 캐시 전략
트래픽이 순간적으로 폭증할 때 발생하는 대표적인 문제는 Cache Stampede입니다. 이는 다수의 요청이 동시에 캐시 미스(Cache Miss)를 발생시키며, DB에 과도한 부하를 주는 현상입니다.
해결 방안
- Locking (분산락): 첫 요청만 DB에 접근하고, 나머지는 대기
- Soft TTL: 캐시 만료 전 백그라운드에서 미리 갱신
- Random TTL: 동일한 키의 만료 시간을 분산시켜 동시 만료 방지
- Read-through + Async Refresh: 캐시 미스 시 자동 비동기 갱신
예를 들어 Redis에서는 Redisson을 이용해 캐시 재생성 중 락을 걸어 두는 방식으로 대응할 수 있습니다.
5. 캐시 키 설계 원칙
캐시 키(Key)는 충돌과 중복을 방지하기 위해 규칙적으로 구성해야 합니다.
패턴 예시:
"entity:{entityType}:{entityId}"
"user:profile:12345"
"product:category:electronics"
- 접두사(prefix)로 캐시 그룹을 구분
- 환경별(DEV/QA/PROD) 네임스페이스 분리
- Key 길이를 짧게 유지 (Redis는 긴 키에 비효율적)
6. 실무에서의 캐시 계층 조합 예시
1️⃣ 로컬 캐시 (Caffeine)
→ 빠른 응답, 인스턴스 내부 재사용
2️⃣ 분산 캐시 (Redis)
→ 서버 간 데이터 일관성 유지
3️⃣ CDN 캐시 (CloudFront)
→ 정적 리소스 글로벌 캐싱
이렇게 다단계 캐시를 구성하면, 읽기 요청의 90% 이상을 캐시 계층에서 소화할 수 있습니다. 이는 대규모 트래픽 환경에서 필수적인 구조입니다.
7. 캐시 모니터링 및 운영
- Redis의
INFO stats명령어로 Hit/Miss 비율 확인 - Prometheus + Grafana로 캐시 성능 대시보드 구성
- Spring Boot Actuator를 활용한 캐시 상태 모니터링
- CloudFront Access Log를 통해 엣지 캐시 효율 분석
운영 중에는 Cache Hit Ratio가 80% 이상 유지되도록 관리하는 것이 이상적입니다.
주의할 점
- 캐시된 데이터가 오래된 경우 시스템 불일치 발생 가능 → TTL 관리 중요
- 동일 키에 대용량 객체를 저장 시 Redis 메모리 급증 가능
- DB와 캐시 간 동기화 이슈로 인해 Write-back 설계 시 주의 필요
- 운영 환경에서는 캐시 서버 장애 대비 이중화(Cluster) 구성 필수
캐시는 단순한 속도 향상 도구가 아니라, 대규모 트래픽에서 시스템을 지탱하는 핵심 인프라입니다. 로컬 캐시, 분산 캐시, CDN 캐시를 상황에 맞게 조합하고, TTL 및 갱신 정책을 체계적으로 관리하면 DB 부하를 획기적으로 줄이고 안정적인 서비스를 운영할 수 있습니다.
'개발 > JAVA' 카테고리의 다른 글
| 실무에서의 Spring Boot 성능 최적화 방법 - 빠르고 안정적인 서비스 운영을 위한 핵심 가이드 (0) | 2025.11.17 |
|---|---|
| DDD(Domain Driven Design)와 Spring 적용 — 복잡한 비즈니스를 코드로 명확하게 표현하기 (0) | 2025.11.16 |
| Spring Boot와 Elasticsearch 연동하기 — 대용량 검색 서비스의 핵심 구현 (0) | 2025.11.14 |
| Spring Boot와 AWS S3 파일 저장하기 - 안전하고 확장 가능한 파일 스토리지 구축 (0) | 2025.11.13 |
| Spring Boot와 AWS RDS 연동하기 - 안정적인 클라우드 데이터베이스 환경 구축 (0) | 2025.11.12 |
