Caching#
Utilizing Caching Mechanisms#
캐싱은 데이터 접근 시간을 크게 단축시켜주는 기술이다.
HTTP 캐싱#
HTTP 캐싱은 웹 브라우저와 서버 사이에서 이루어지는 가장 기본적인 캐싱 형태이다.
1
2
3
| Cache-Control: max-age=3600, public
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 03 Apr 2025 13:22:05 GMT
|
위와 같은 HTTP 헤더를 통해 캐싱 정책을 제어할 수 있다. max-age
는 리소스가 유효한 시간(초)을 지정하며, public
은 모든 캐시가 응답을 저장할 수 있다는 의미이다. ETag
와 Last-Modified
는 리소스의 변경 여부를 판단하는 데 사용된다.
서버 사이드 캐싱#
서버 측에서 이루어지는 캐싱으로, 요청 처리 시간을 단축시켜 준다.
Redis를 활용한 예시#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import redis
# Redis 연결 설정
r = redis.Redis(host='localhost', port=6379, db=0)
def get_user_data(user_id):
# 캐시에서 데이터 조회 시도
cached_data = r.get(f"user:{user_id}")
if cached_data:
# 캐시에 데이터가 있으면 반환
return json.loads(cached_data)
else:
# 캐시에 없으면 DB에서 조회
user_data = database.query(f"SELECT * FROM users WHERE id = {user_id}")
# 조회한 데이터를 캐시에 저장 (1시간 유효)
r.setex(f"user:{user_id}", 3600, json.dumps(user_data))
return user_data
|
CDN (Content Delivery Network)#
전 세계에 분산된 서버 네트워크를 통해 사용자와 가까운 위치에서 콘텐츠를 제공함으로써 지연 시간을 최소화한다.
CDN 구성 예시 (AWS CloudFront)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // AWS CloudFront 배포 설정 예시
const cloudfront = {
distribution: {
origins: [{
domainName: 'example.com',
id: 'myWebsite'
}],
defaultCacheBehavior: {
targetOriginId: 'myWebsite',
minTTL: 0,
defaultTTL: 86400, // 24시간
maxTTL: 31536000, // 1년
viewerProtocolPolicy: 'redirect-to-https',
allowedMethods: ['GET', 'HEAD'],
cachedMethods: ['GET', 'HEAD'],
forwardedValues: {
queryString: false,
cookies: {
forward: 'none'
}
}
}
}
};
|
Application of Suitable Caching Patterns#
다양한 캐싱 패턴이 있으며, 애플리케이션의 특성과 요구사항에 따라 적절한 패턴을 선택해야 한다.
Cache-Aside (Lazy Loading) 패턴#
가장 일반적인 캐싱 패턴으로, 데이터가 필요할 때 캐시를 먼저 확인하고 없으면 원본 저장소에서 가져와 캐시에 저장한다.
1
2
3
4
5
6
7
8
9
10
11
12
| def get_data(key):
# 1. 캐시에서 데이터 조회
data = cache.get(key)
if data is None:
# 2. 캐시에 없으면 DB에서 조회
data = db.query(key)
# 3. 데이터를 캐시에 저장
cache.set(key, data, ttl=3600)
return data
|
장점:
- 자주 접근하는 데이터만 캐시에 저장되어 캐시 공간을 효율적으로 사용
- 구현이 단순하고 직관적
단점:
- 처음 요청 시 캐시 미스(cache miss)로 인한 지연 발생 가능
- 데이터 일관성 유지가 복잡할 수 있음
Write-Through 패턴#
데이터를 쓸 때 DB와 캐시를 동시에 업데이트하는 방식이다.
1
2
3
4
5
6
7
8
| def update_data(key, value):
# 1. DB에 데이터 저장
db.save(key, value)
# 2. 캐시에도 데이터 저장/갱신
cache.set(key, value, ttl=3600)
return success
|
장점:
- 캐시와 DB 간의 데이터 일관성이 높음
- 읽기 작업에서 캐시 미스가 거의 발생하지 않음
단점:
- 모든 쓰기 작업이 두 번 이루어져 지연이 발생할 수 있음
- 자주 접근하지 않는 데이터도 캐시에 저장되어 캐시 공간 낭비 가능성
Read-Through 패턴#
캐시 미스 발생 시 캐시 자체가 데이터를 로드하고 저장하는 방식이다.
1
2
3
4
5
6
7
8
9
10
11
| # 캐시 라이브러리/시스템 내부에 구현된 로직
class Cache:
def get(self, key):
data = self._get_from_cache(key)
if data is None:
# 캐시 미스 시 DB에서 데이터 로드
data = self._load_from_db(key)
self._save_to_cache(key, data)
return data
|
장점:
- 캐시 미스 처리 로직이 캐시 시스템에 캡슐화됨
- 애플리케이션 코드가 단순해짐
단점:
- 캐시 시스템이 데이터 소스에 직접 접근해야 함
- 첫 번째 요청에서 지연 발생 가능
Write-Behind (Write-Back) 패턴#
데이터를 먼저 캐시에만 쓰고, 나중에 DB에 일괄 처리하는 방식이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| def update_data(key, value):
# 1. 캐시에 데이터 저장
cache.set(key, value)
# 2. 쓰기 큐에 추가 (비동기적으로 DB에 기록됨)
write_queue.add(key, value)
return success
# 백그라운드 프로세스
def process_write_queue():
while True:
# 큐에서 데이터를 가져와 DB에 일괄 저장
batch = write_queue.get_batch(100)
db.save_batch(batch)
sleep(5) # 5초 간격으로 처리
|
장점:
- 쓰기 작업의 지연 시간이 크게 감소
- DB 쓰기 작업을 최적화된 배치로 처리 가능
단점:
- 캐시 장애 시 데이터 손실 위험
- 데이터 일관성 보장이 어려움
- 구현이 복잡함
Efficient Cache-Invalidation Strategies#
캐시된 데이터가 원본 데이터와 일치하지 않을 때 발생하는 문제를 해결하기 위한 전략이다.
시간 기반 무효화 (TTL, Time-To-Live)#
가장 단순한 전략으로, 일정 시간이 지나면 캐시 항목을 자동으로 만료시킨다.
1
2
3
4
5
| # Redis 예시
redis_client.setex("product:12345", 3600, product_data) # 1시간 후 만료
# Memcached 예시
memcached_client.set("product:12345", product_data, 3600)
|
장점:
- 구현이 단순하고 오버헤드가 적음
- 시스템 복잡성이 낮음
단점:
- 데이터 일관성을 정확히 보장할 수 없음
- TTL 동안은 오래된 데이터가 제공될 수 있음
이벤트 기반 무효화#
데이터가 변경될 때 관련 캐시를 즉시 무효화하는 방식이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| def update_product(product_id, data):
# 1. DB 업데이트
db.update_product(product_id, data)
# 2. 캐시 무효화 (삭제)
cache.delete(f"product:{product_id}")
# 관련 캐시도 함께 무효화
cache.delete(f"product_list:category:{data['category_id']}")
cache.delete("featured_products")
# 3. 이벤트 발행 (다른 서비스의 캐시도 무효화하기 위함)
message_broker.publish("product_updated", {
"product_id": product_id,
"timestamp": current_time()
})
|
장점:
- 데이터 변경 즉시 캐시가 무효화되어 일관성이 높음
- 필요한 캐시만 선택적으로 무효화 가능
단점:
- 모든 데이터 변경 지점에 캐시 무효화 코드 추가 필요
- 분산 시스템에서 일관성 있는 무효화가 어려울 수 있음
버전 기반 무효화#
각 리소스에 버전 정보를 부여하고, 데이터 변경 시 버전을 증가시켜 캐시 키에 반영하는 방식이다.
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
| def get_product(product_id):
# 제품의 현재 버전 조회
product_version = version_store.get(f"product_version:{product_id}")
# 버전을 포함한 캐시 키 생성
cache_key = f"product:{product_id}:v{product_version}"
# 캐시에서 조회
product_data = cache.get(cache_key)
if product_data is None:
# DB에서 데이터 로드
product_data = db.get_product(product_id)
# 버전과 함께 캐시에 저장
cache.set(cache_key, product_data, ttl=86400) # 24시간 유효
return product_data
def update_product(product_id, data):
# 1. DB 업데이트
db.update_product(product_id, data)
# 2. 버전 증가 (기존 캐시를 무효화하는 효과)
version_store.increment(f"product_version:{product_id}")
|
장점:
- 캐시 삭제 없이 무효화 가능 (기존 캐시는 자연스럽게 만료됨)
- 경쟁 상태(race condition)를 피할 수 있음
- 롤백이 용이함
단점:
- 버전 관리를 위한 추가 저장소 필요
- 구현이 다소 복잡함
실제 애플리케이션에서의 캐싱 전략 적용 사례#
전자상거래 플랫폼#
- 제품 상세 정보: Cache-Aside + TTL (1시간)
- 제품 카테고리 목록: Cache-Aside + 이벤트 기반 무효화
- 사용자 장바구니: Redis를 활용한 세션 저장
- 베스트셀러/추천 제품: 배치 작업으로 사전 계산 후 캐싱 (1일 TTL)
소셜 미디어 플랫폼#
- 뉴스피드: Write-Through + 무한 스크롤 페이지네이션
- 사용자 프로필: Cache-Aside + 이벤트 기반 무효화
- 인기 게시물: 주기적으로 계산 후 캐싱 (15분 TTL)
- 댓글/좋아요: Write-Behind 패턴으로 DB 부하 감소
금융 서비스 플랫폼#
- 계좌 잔액: 짧은 TTL + 중요 트랜잭션 시 즉시 무효화
- 시장 데이터: 다층 캐싱 (L1: 초단위 TTL, L2: 분단위 TTL)
- 거래 내역: 읽기 전용 복제본 DB + 캐싱
- 사용자 설정: 버전 기반 무효화
캐싱 관련 고려사항 및 모범 사례#
콜드 스타트(Cold Start) 문제 해결#
새로운 서버 인스턴스가 시작될 때 캐시가 비어 있는 문제를 해결하는 방법:
워밍업 스크립트: 서버 시작 시 자주 액세스하는 데이터를 미리 캐싱
1
2
3
4
5
6
7
8
9
10
| def cache_warmup():
# 주요 카테고리 캐싱
categories = db.get_all_categories()
for category in categories:
cache.set(f"category:{category.id}", category, ttl=86400)
# 인기 제품 캐싱
popular_products = db.get_popular_products(limit=100)
for product in popular_products:
cache.set(f"product:{product.id}", product, ttl=3600)
|
글로벌 캐시 계층: Redis 또는 Memcached 클러스터를 별도로 유지하여 서버 간에 캐시 공유
캐시 폭발(Cache Stampede) 방지#
여러 요청이 동시에 캐시 미스를 경험하여 데이터베이스에 과부하가 발생하는 문제를 해결하는 방법:
Singleflight 패턴: 동일한 키에 대한 여러 요청을 하나로 병합
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
31
32
33
34
| import threading
class SingleflightCache:
def __init__(self, cache_client, db_client):
self.cache = cache_client
self.db = db_client
self.locks = {}
self.lock_mutex = threading.Lock()
def get(self, key):
# 캐시에서 먼저 확인
data = self.cache.get(key)
if data is not None:
return data
# 캐시 미스 시 락 획득
with self.lock_mutex:
if key not in self.locks:
self.locks[key] = threading.Lock()
# 같은 키에 대한 중복 요청 방지
with self.locks[key]:
# 다시 캐시 확인 (다른 스레드가 이미 가져왔을 수 있음)
data = self.cache.get(key)
if data is not None:
return data
# DB에서 데이터 로드
data = self.db.query(key)
# 캐시에 저장
self.cache.set(key, data, ttl=3600)
return data
|
확률적 조기 만료: 캐시 항목의 실제 만료 시간보다 조금 일찍 재생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import random
def get_with_early_refresh(key, ttl=3600):
data = cache.get(key)
# 데이터가 있고, 만료 임계값(80%)을 넘은 경우 확률적으로 갱신
if data and data.get('created_at') < (current_time() - ttl * 0.8):
# 10% 확률로 백그라운드 갱신 트리거
if random.random() < 0.1:
threading.Thread(target=refresh_cache, args=(key,)).start()
# 데이터가 없으면 동기적으로 로드
if data is None:
data = load_and_cache(key, ttl)
return data.get('value')
|
캐시 계층화 전략#
다양한 데이터 유형과 접근 패턴에 맞게 여러 캐시 계층을 활용하는 방법:
1
2
3
4
5
6
7
8
9
10
11
12
13
| 사용자 요청
│
▼
로컬 캐시 (메모리 내 캐시, 매우 빠름, 짧은 TTL)
│
▼
분산 캐시 (Redis/Memcached, 중간 속도, 중간 TTL)
│
▼
데이터베이스 캐시 (쿼리 캐시, 느림, 긴 TTL)
│
▼
DB
|
캐싱 성능 모니터링 및 측정#
효과적인 캐싱 전략을 유지하기 위해서는 지속적인 모니터링이 필요하다.
주요 캐싱 성능 지표#
- 캐시 적중률(Hit Rate): 전체 요청 중 캐시에서 처리된 비율
- 캐시 미스 비율(Miss Rate): 전체 요청 중 캐시에 없어 원본에서 가져온 비율
- 캐시 지연 시간(Latency): 캐시 요청부터 응답까지의 시간
- 캐시 메모리 사용량: 현재 사용 중인 캐시 메모리 양
- 캐시 제거(Eviction) 비율: 공간 부족으로 캐시에서 제거된 항목의 비율
모니터링 구현 예시#
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
| class MonitoredCache:
def __init__(self, cache_client, metrics_client):
self.cache = cache_client
self.metrics = metrics_client
def get(self, key):
start_time = time.time()
data = self.cache.get(key)
# 지연 시간 측정
latency = time.time() - start_time
self.metrics.record("cache.latency", latency)
if data is None:
# 캐시 미스 기록
self.metrics.increment("cache.miss")
return None
else:
# 캐시 적중 기록
self.metrics.increment("cache.hit")
return data
def set(self, key, value, ttl=None):
self.cache.set(key, value, ttl)
# 캐시 크기 측정 (항목 수)
cache_size = self.cache.info().get("curr_items", 0)
self.metrics.gauge("cache.size", cache_size)
|
용어 정리#
참고 및 출처#
데이터베이스 캐싱 (Database Caching) 데이터베이스 캐싱은 자주 사용되는 데이터를 빠르게 접근할 수 있는 메모리에 임시로 저장하는 기술.
정의와 목적 자주 액세스하는 데이터를 고속 메모리에 저장하여 빠른 검색 가능 데이터베이스 서버의 부하 감소 및 응답 시간 단축 주요 장점 성능 향상: 데이터 검색 속도 개선 서버 부하 감소: 반복적인 쿼리 처리 최소화 비용 절감: 데이터베이스 리소스 사용 효율화 사용자 경험 개선: 빠른 응답 시간 제공 작동 원리 캐시 히트: 요청 데이터가 캐시에 있어 즉시 반환 캐시 미스: 데이터가 캐시에 없어 원본 데이터베이스에서 조회 캐싱 전략 인-메모리 캐싱: RAM에 데이터 저장 (예: Redis, Memcached) 쿼리 결과 캐싱: 자주 실행되는 쿼리 결과 저장 객체 캐싱: 애플리케이션 레벨에서 객체 단위로 캐싱 주의사항 데이터 일관성 유지: 캐시와 원본 데이터 간 불일치 방지 적절한 캐시 갱신 정책 수립 필요 참고 및 출처
Cache Strategy 캐싱은 API 설계에서 성능을 최적화하는 핵심 전략이다. 자주 요청되는 데이터를 임시 저장소에 저장함으로써 반복적인 계산, 데이터베이스 쿼리, 네트워크 요청을 줄이고 응답 시간을 대폭 향상시킬 수 있다.
API 캐싱은 현대 웹 애플리케이션과 분산 시스템의 성능, 확장성, 사용자 경험을 개선하는 필수적인 전략이다.
적절히 구현된 캐싱 전략은 다음과 같은 여러 이점을 제공한다:
응답 시간 단축: 사용자에게 더 빠른 API 응답 제공 백엔드 부하 감소: 서버와 데이터베이스의 리소스 사용 최적화 비용 효율성: 인프라 요구 사항 및 대역폭 비용 절감 확장성 향상: 기존 리소스로 더 많은 사용자와 요청 처리 안정성 개선: 일시적인 장애 상황에서도 서비스 가용성 유지 그러나 API 캐싱은 단순히 기술적 구현 이상의 것이다. 효과적인 캐싱 전략은 데이터의 특성, 사용자 요구사항, 비즈니스 우선순위를 철저히 이해하고 이를 기반으로 설계되어야 한다. 데이터의 신선도, 캐시 적중률, 리소스 사용, 구현 복잡성 간의 균형을 맞추는 것이 중요하다.
...