| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 |
- static-factory-method
- ReverseNested
- 레벨1
- 회고
- SpringBoot
- 포트앤어댑터 아키텍처
- design-pattern
- 다짐글
- constructor
- object-creation
- UserLand
- Level2
- 글또10기
- 헥사고날 아키텍처
- builder-pattern
- 글쓰기세미나
- 글또
- 코딩테스트
- HashMap
- 클린 아키텍처
- React
- 3계층 아키텍처
- 코엑스그랜드볼룸
- 클라우드아키텍처
- DevOps
- axios
- OpenSearch
- 가용영역
- 프로그래머스
- QueryDSL
- Today
- Total
oguri's garage
대규모 트래픽을 위한 고성능 캐싱 전략: 1차(L1) & 2차(L2) 캐시 본문
개요
게시글 조회나 목록 조회처럼 로직은 단순하지만 많은 사용자가 반복적으로 호출하는 API는 데이터베이스에 큰 부하를 준다. 이런 상황에서 캐싱을 적용하면 응답 속도를 극적으로 개선할 수 있다. 실제로 146ms에서 8ms로, 842ms에서 6ms로 개선된 사례들이 있다.
이번 글에서는 최고의 속도와 확장성을 동시에 확보하기 위한 2-Level 캐싱 전략에 대해 정리한다.
2-Level 캐싱이란?
2-Level 캐싱은 로컬 캐시(L1)와 분산 캐시(L2)를 조합하는 전략이다. 마치 학생의 책상과 도서관의 관계와 같다.
- L1 캐시 (책상): 가장 자주 보는 자료를 손만 뻗으면 바로 집을 수 있도록 준비해 두는 곳 (초고속 접근)
- L2 캐시 (도서관): 책상에 없는 자료를 찾으러 가는 곳. 조금 느리지만 모든 학생이 공유하며, 책상보다 훨씬 많은 자료를 저장할 수 있다 (분산 및 대용량 저장)
- DB (학교 서고): 도서관에도 없는 자료를 최종적으로 찾는 곳. 가장 느리지만 원본 데이터가 모두 있다
1차 캐시 (L1 Cache): 로컬 캐시
1차 캐시는 애플리케이션 서버 자체의 메모리, 즉 JVM 힙 메모리 내에 위치한다.
주요 특징
| 구분 | 설명 |
|---|---|
| 대표 도구 | Caffeine (Java용 고성능 캐싱 라이브러리) |
| 위치 | 애플리케이션 인스턴스 내부 (JVM 힙) |
| 속도 | 초고속 (네트워크 왕복이나 디스크 I/O 없이 getIfPresent() 호출만으로 데이터 반환) |
| 데이터 공유 | 해당 인스턴스에서만 사용 가능 (분산 공유 불가) |
장점
가장 자주 호출되는 핫 데이터(Hot Data)에 대해 최소 지연 시간을 제공하여 다단계 캐시 효과를 극대화한다.
주의사항
JVM 힙에 너무 많은 로컬 캐시를 저장하면 GC(Garbage Collection) 지연이 발생할 수 있다. 이는 메모리 청소 주기마다 응답이 수십~수백 밀리초 멈추는 현상으로 나타날 수 있다.
2차 캐시 (L2 Cache): 분산 캐시
2차 캐시는 애플리케이션 서버 외부에 존재하는 독립적인 캐시 서버다.
주요 특징
| 구분 | 설명 |
|---|---|
| 대표 도구 | Redis (Remote Dictionary Server) |
| 위치 | 별도의 외부 인메모리 서버 |
| 속도 | 매우 빠름 (메모리 기반, 1ms 미만의 응답 시간) |
| 데이터 공유 | 여러 애플리케이션 인스턴스 간 데이터 공유 가능 |
장점
- JVM 힙 메모리의 부담을 Redis에게 위임
- 대용량 저장 가능
- 클러스터링을 통한 확장성 제공
- 디스크 백업을 통한 지속성 제공
주의사항
Redis 인스턴스가 설정된 메모리 한도를 초과하면 OOM(Out Of Memory) 에러가 발생하여 새로운 데이터 저장이 불가능해질 수 있다. 또한 네트워크 지연도 발생할 수 있다.
2-Level 캐싱의 동작 방식
Caffeine(L1)과 Redis(L2)를 함께 사용하는 Composite 전략은 다음과 같이 동작한다.
조회 흐름
요청 → L1 캐시 조회 → (Miss) → L2 캐시 조회 → (Miss) → DB 조회 → L1, L2 저장 → 응답
↓ (Hit) ↓ (Hit)
응답 L1 저장 → 응답
| 단계 | 동작 | 목적 |
|---|---|---|
| 1단계 | L1 캐시 조회 (Caffeine) | 가장 빠른 속도로 데이터를 찾아본다 |
| 2단계 | L2 캐시 조회 (Redis) | L1에서 미스가 발생하면 네트워크를 통해 L2 캐시를 확인한다 |
| 3단계 | L1 저장 및 반환 | L2에서 히트하면 데이터를 반환함과 동시에 L1에도 저장한다 (Write-Through) |
| 4단계 | DB 조회 및 저장 | L2에서도 미스하면 DB에서 데이터를 불러와 L1과 L2 두 곳 모두에 저장한다 |
Composite 전략의 이점
다단계 캐시 효과 극대화
- 트래픽이 빈번한 페이지 요청에서 로컬 캐시(L1)의 초고속 조회 성능을 활용한다
장애 방지 (Fault Tolerance)
- Redis 서버(L2)에 장애가 발생하더라도 로컬 캐시(L1)에 남아 있는 데이터를 이용해 어느 정도 임시 서빙이 가능하다
운영 시 고려사항
캐싱은 성능 최적화의 강력한 도구이지만, 잘못 설계하면 오히려 시스템에 부정적인 영향을 줄 수 있다.
1. GC 지연 및 OOM 조율
JVM 힙(L1)과 Redis 메모리(L2) 사용량을 적절히 조율하여 GC 딜레이나 Redis OOM 같은 부정적 영향을 피해야 한다.
권장사항:
- L1 캐시 크기는 JVM 힙의 10-20% 이내로 제한
- Redis 메모리는
maxmemory-policy설정으로 관리 (예:allkeys-lru)
2. TTL 설정 및 Staleness
캐시의 만료 정책(TTL)이 너무 길면 캐시 데이터가 최신 데이터와 차이가 나는 'staleness' 상태가 될 위험이 있다. 반대로 너무 짧으면 캐시 미스율이 올라간다.
권장사항:
- 데이터 특성에 맞는 TTL 설정 (자주 변경되는 데이터는 짧게, 정적 데이터는 길게)
- 중요 데이터는 업데이트 시 명시적으로 캐시 무효화(Cache Invalidation) 수행
3. 모니터링
캐시 계층이 복잡해질수록 모니터링이 필수적이다.
주요 메트릭:
cache_hits: 캐시 히트 횟수cache_misses: 캐시 미스 횟수evictions: 캐시 제거(Eviction) 횟수hit_rate: 캐시 히트율 (hits / (hits + misses))
목표:
- L1 캐시 히트율: 80% 이상
- L2 캐시 히트율: 95% 이상
실전 구현 예시
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// L1: Caffeine 캐시 설정
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
.recordStats());
// L2: Redis 캐시 설정
RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
);
RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(redisCacheConfig)
.build();
// L1 + L2 Composite 캐시 매니저
return new CompositeCacheManager(caffeineCacheManager, redisCacheManager);
}
}
마치며
2-Level 캐싱 전략은 대규모 트래픽 환경에서 성능과 안정성을 동시에 확보할 수 있는 효과적인 방법이다. 하지만 단순히 도입하는 것만으로는 부족하고, 적절한 모니터링과 튜닝이 반드시 필요하다.
핵심은 L1의 초고속 성능과 L2의 확장성 및 공유성을 적절히 조합하여, 각 계층이 서로의 단점을 보완하도록 설계하는 것이다.