쿠폰 미션에서 성능을 위해 캐시를 사용하라는 요구사항을 받았다.
캐시를 사용할 때 적용할 수 있는 전략에 대해 알아보고자 작성한다.
읽기 전략
Look Aside (Lazy Loading) 전략
조회 시 캐시를 먼저 확인하고 없다면 DB에서 조회한다.
그 후 조회된 데이터를 캐시에 저장하는 전략이다.
public Product getProduct(Long id) {
// 1. 캐시 먼저 확인
Coupon coupon = cacheRepository.get(id);
if (coupon != null) {
return coupon;
}
// 2. 캐시에 없으면 DB에서 조회
coupon = dbRepository.findById(id);
// 3. DB에서 조회한 데이터를 캐시에 저장
if (coupon != null) {
cacheRepository.set(id, coupon);
}
return coupon;
}
- 장점
- 조회가 된 내역이 있는 데이터만 캐시에 저장하기 때문에 필요한 데이터만 캐시에 저장한다고 볼 수 있다.
- 따라서 효율적인 메모리 사용이 가능하다.
- 단점
- Cache Miss 시 DB에서 조회해야 하기 때문에 지연이 발생한다.
- 따라서 최초 접근 시 성능이 낮다.
Read Through 전략
캐시가 직접 DB와 통신하여 데이터 관리하는 방식이다.
캐시에 데이터가 없는 경우 캐시가 직접 누락된 데이터를 DB에서 조회해 와서 데이터를 넣는다.
결과적으로 애플리케이션은 캐시하고만 통신하면 된다.
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return dbRepository.findById(id);
}
- 장점:
- 구현이 단순
- 캐시 로직이 추상화됨
- 단점:
- 마찬가지로 캐시 미스 시에 지연이 발생할 수 있다.
- 또 캐시에 의존성이 매우 높기 때문에 장애 발생 시 전체 장애로 이어진다.
Refresh Ahead 전략
데이터 만료 전에 미리 캐시 갱신
예측 알고리즘으로 자주 사용될 데이터 예측
@Scheduled(fixedRate = 60000) // 1분마다 실행
public void refreshCache() {
List<Product> hotProducts = dbRepository.findHotProducts();
for (Product product : hotProducts) {
cacheRepository.set(product.getId(), product);
}
}
- 장점:
- 캐시 히트율이 높다.
- 응답 지연이 다른 방식에 비해 최소화된다.
- 단점:
- 백그라운드 갱신 작업 때문에 추가 리소스 사용이 필요하다.
- 사용되지 않을 수 있는 데이터도 미리 캐시하여 불필요한 데이터도 캐시될 수 있다.
읽기 전략 선택 기준
위 3가지 방식 중 어떤 방식을 선택해야 할까?
보통 아래의 기준을 통해 적절한 읽기 전략을 선택한다.
- 데이터 접근 빈도
- 허용 가능한 지연 시간
- 메모리 제약사항
쓰기 전략
Write Through
데이터베이스에 저장한 후 캐시에 즉시 업데이트를 하는 방식이다.
public Product saveProduct(Product product) {
// 1. DB 저장
Product savedProduct = dbRepository.save(product);
// 2. 캐시 즉시 업데이트
cacheRepository.set(product.getId(), savedProduct);
return savedProduct;
}
- 장점
- DB에 저장한 후 바로 동일한 데이터를 저장하기 때문에 데이터 일관성 보장
- 즉시 캐시에 업데이트 하기 때문에 읽기 성능 좋음
- 단점
- 쓰기 지연 발생
- 모든 쓰기 작업이 두 번 발생
Write Behind (= Write Back)
캐시에 즉시 업데이트를 한 후 백그라운드에서 DB에 저장하는 방식이다.
보통 캐시 저장 후 큐에 넣어두고 배치로 DB에 저장을 처리한다.
public class WriteBackCache {
private final Queue<CacheEntry> writeQueue = new ConcurrentLinkedQueue<>();
public void saveProduct(Product product) {
// 1. 캐시 즉시 업데이트
cacheRepository.set(product.getId(), product);
// 2. 쓰기 큐에 추가
writeQueue.offer(new CacheEntry(product));
}
@Scheduled(fixedRate = 5000) // 5초마다 실행
public void processWriteQueue() {
List<CacheEntry> batch = new ArrayList<>();
CacheEntry entry;
while ((entry = writeQueue.poll()) != null) {
batch.add(entry);
}
if (!batch.isEmpty()) {
dbRepository.saveAll(batch);
}
}
}
- 장점
- 쓰기 성능 향상
- 배치 처리로 인해 DB 접근이 적기 때문에 부하 감소
- 단점
- 중간에 큐에 저장한 데이터가 유실될 가능성이 있다.
- 구현 복잡도 증가
Write Around
DB에만 저장하고 캐시에는 저장하지 않는 방식이다.
쓰기 작업이 많은 경우에 사용한다고 하는데 흠 잘 모르겠다.
// Write Around 구현 예시
public class WriteAroundCache {
private final Cache cache;
private final Repository repository;
public void save(String key, Data data) {
// DB에만 저장
repository.save(data);
// 캐시는 무시 (invalidate만 수행)
cache.invalidate(key);
}
public Data get(String key) {
Data data = cache.get(key);
if (data == null) {
data = repository.findById(key);
cache.put(key, data);
}
return data;
}
}
- 장점
- 쓰기 성능 우수
- 캐시 리소스 효율적 사용
- 단점
- 읽기 시 최초 지연이 발생하며
- 캐시 미스가 발생될 확률이 높다.
쓰기 전략 선택 기준
위 3가지 방식 중 어떤 방식을 선택해야 할까?
보통 아래의 기준을 통해 적절한 읽기 전략을 선택한다.
- 데이터 일관성 요구사항
- 쓰기 빈도
- DB 부하 상황
- 데이터 유실 허용도
캐시 전략 조합 사용 가이드
각 조합을 언제 사용하면 좋을지 claude 선생님께 물어보았는데 다음과 같은 결과를 확인할 수 있었다.
읽기 전략 | 쓰기 전략 | 적합한 사용 사례 | 예시 시나리오 |
Look Aside | Write Through | - 데이터 일관성이 중요한 경우 - 읽기/쓰기 비율이 비슷한 경우 - 시스템 리소스가 충분한 경우 |
- 사용자 프로필 관리 - 장바구니 시스템 - 주문 관리 시스템 |
Look Aside | Write Behind | - 높은 처리량이 필요한 경우 - 일시적 데이터 불일치 허용 - 빈번한 데이터 수정이 있는 경우 |
- 로그 수집 시스템 -실시간 분석 시스템 - 소셜 미디어 포스팅 |
Look Aside | Write Around | - 쓰기가 자주 발생하지 않는 경우 - 읽기가 간헐적인 경우 - 캐시 리소스 절약이 필요한 경우 |
- 보고서 생성 시스템 - 로그 조회 시스템 - 아카이브 데이터 관리 |
Read Through | Write Through | - 강한 데이터 일관성 필요 - 캐시 관리를 추상화하고 싶은 경우 - 단순한 구현이 필요한 경우 |
- 금융 거래 시스템 - 재고 관리 시스템 - 예약 시스템 |
Read Through | Write Behind | - 높은 성능이 필요한 경우 - 복잡한 캐시 로직 허용 - 배치 처리가 효율적인 경우 |
- 메시징 시스템 - 게임 점수 시스템 - 사용자 활동 추적 |
Read Through | Write Around | - 읽기 성능 최적화가 필요한 경우 - 쓰기는 직접 DB에 하고 싶은 경우 - 캐시 오버헤드 최소화가 필요한 경우 |
- CMS 시스템 - 제품 카탈로그 - 설정 관리 시스템 |
Refresh Ahead | Write Through | - 매우 낮은 읽기 지연시간 필요 - 데이터 일관성도 중요한 경우 - 예측 가능한 데이터 접근 패턴 |
- 실시간 대시보드 - 가격 정보 시스템 - 실시간 모니터링 |
Refresh Ahead | Write Behind | - 초고성능이 필요한 경우 - 복잡한 시스템 구현 가능한 경우 - 리소스가 충분한 경우 |
- 실시간 분석 플랫폼 - 게임 리더보드 - IoT 데이터 처리 |
Refresh Ahead | Write Around | - 읽기 최적화가 매우 중요한 경우 - 쓰기는 드물게 발생하는 경우 - 예측 가능한 데이터셋 |
- 정적 컨텐츠 제공 - 참조 데이터 관리 |
결론적으로 나는 쿠폰이라는 도메인에 대해 읽기 작업이 더 월등할 것이라고 예상했다.
조회가 매우 빈번하고 동일한 쿠폰에 대해 반복적으로 조회할 일이 많다고 판단했다.
또한 하나의 쿠폰은 발행 가능할 경우 최소 한번 이상은 조회될 가능성이 높으며, 이미 발생한 가능한 쿠폰에 대해 수정될 일은 현저히 낮다고 판단했다.
하지만 쓰기 작업은 읽기에 비해 상대적으로 낮은 빈도이며 동시 사용이 불가능해야 하기 때문에 높은 정합성 필요했다.
따라서 Read-Through + Write-Through 조합을 사용하기로 했다.
'우아한테크코스 6기 > 4단계' 카테고리의 다른 글
우아한테크코스 6기 백엔드 과정을 마치며 적는 회고 (8) | 2024.12.08 |
---|---|
@DynamicUpdate 의 장단점 알아보기 (1) | 2024.11.04 |
Grafana, Prometheus로 TPS 측정 및 시각화하기 (0) | 2024.10.14 |
Connection Pool과 HikariCP에 대해 알아보자 (0) | 2024.10.09 |
고가용성과 SPOF (0) | 2024.10.01 |