본문 바로가기

Spring Boot/Cache

캐시의 기본 개념과 필요성 : 캐시의 핵심 원리

캐시 2

 

 - 캐시의 핵심 원리

 - 시간적 지역성과 공간적 지역성의 개념

 - 캐시 히트(Cache Hit), 캐시 미스(cache Miss)의 차이

 - 캐시 적중률(Cache Ratio)의 계산 방식과 의미

 - 캐시 성능이 시스템에 얼마나 효율적인가?

 


1. 캐시의 핵심 원리 개요

캐시가 왜 이렇게 빠른 속도를 낼 수 있는가?? - 내부 원리 알아보기

 

캐시의 성능을 결정하는 핵심 개념, 4가지

 - 시간적 지역성(Temporal Locality)

 - 공간적 지역성(Spatial Locality)

 - 캐시 히트(Cache Hit), 캐시 미스(Cache Miss)

 - 캐시 적중률(Cache Ratio)

 

이 네 가지 원리는 캐시가 데이터를 효율적으로 저장하고 재사용하는 근본적 이유를 설명한다.

 


2. 시간적 지역성(Temporal Locality)

1) 개념

시간적 지역성이란, 최근에 사용한 데이터는 가까운 미래에도 다시 사용될 가능성이 높다 - 라는 원리임

사용자가 쇼핑몰에서 노트북을 검색했다면, 곧이어 같은 상품을 여러 번 다시 조회하거나 관련 정보를 볼 확률이 높다.

 

즉, 최근에 접근된 데이터를 캐시에 보관한다면 다시 요청할 때 즉시 응답할 수 있다.

시간적 지역성의 원리

 

시간적 지역성의 경우 대부분의 캐시 시스템이 TTL(Time To Live, 유효 기간) 개념을 사용하는 이유가 되기도 한다.

최근 접근된 데이터는 일정 시간 유지되어 반복 요청에 대해 빠르게 대응할 수 있다.

 

 

2) 예시

import java.util.HashMap;
import java.util.Map;

public class TemporalLocalityWithTTLExample {
	static class CacheItem {
		String value;
		long expireAt; // 만료시각, 밀리초 단위라 long
		
		CacheItem(String value, long ttlMillies) {
			this.value = value;
			this.expireAt = System.currentTimeMillis() + ttlMillies;
		}
		
		boolean expired() { // 만료됨
			// 현재 시간이 expiredAt보다 크면, 만료되었음을 의미함, 지정한 ttlMilles를 지났기 때문
			return System.currentTimeMillis() > expireAt;
		}
	}
	
	// 캐시 필드
	private static final Map<String, CacheItem> cache = new HashMap<>();
	
	// 캐시에서 데이터를 조회하는 메서드
	public static String getData(String key) {
		CacheItem item = cache.get(key); // key값 찾고
		
		// 캐시에 item이 존재하며, 만료되지 않았다면
		if(item != null && !item.expired()) {
			System.out.println("[Cache Hit] 캐시에서 데이터를 반환합니다.");
			return item.value; // 캐시에서 value 리턴
		}
		
		// 필터링에 걸리지 않았다면 캐시에 데이터가 없다는 것을 의미함, DB에서 조회
		System.out.println("[Cache Miss] 원본 데이터를 조회합니다.");
		String data = key + "상품 정보"; // 이게 원본 데이터라고 가정함
		cache.put(key, new CacheItem(data, 3000)); // 캐시에 key와 item을 적재함
		return data;
	}
	
	public static void main(String[] args) {
		try {
			System.out.println(getData("노트북")); // 첫번째 요청
			Thread.sleep(1000);
			System.out.println(getData("노트북")); // 두번째 요청, 1초 후
			Thread.sleep(4000);
			System.out.println(getData("노트북")); // 세번째 요청, 4초 후
		} catch(InterruptedException e) {
			System.out.printf("예기치 못한 무언가가 발생, {}", e);
		}
	}
}

 

결과

[Cache Miss] 원본 데이터를 조회합니다.
노트북상품 정보
[Cache Hit] 캐시에서 데이터를 반환합니다.
노트북상품 정보
[Cache Miss] 원본 데이터를 조회합니다.
노트북상품 정보

 

* 같은 키워드를 짧은 시간 내에 반복 호출하면 시간적 지역성이 작동하여 빠르게 동작한다.

 


3. 공간적 지역성(Spatial Locality)

1) 개념 설명

공간적 지역성은, 어떤 데이터가 사용되면 그 주변(인접) 데이터도 곧 사용될 가능성이 높다 - 라는 원리임

사용자가 '노트북' 정보를 조회했다면, 비슷한 시점에 '노트북 케이스', '마우스' 와 같은 관련 상품을 볼 가능성이 높다.

이러한 원리를 이용하면 캐시가 하나의 데이터뿐만 아니라 인접한 데이터까지 함께 저장하여, 이후 요청시 빠르게 제공할 수 있다.

공간적 지역성, Spatial Locality

 

 

 

2) 예시

이거 좀 이해하기 어려웠음

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class SpatialLocalityImprovedExample {
	
	private static final Map<String, List<String>> database = new HashMap<>(); // 여기가 DB라고 가정
	private static final Map<String, List<String>> cache = new HashMap<>(); // 여기가 Cache라고 가정
	
	static {
		database.put("노트북", Arrays.asList("노트북", "마우스", "키보드", "노트북 가방"));
		database.put("마우스", Arrays.asList("마우스", "마우스 패드", "키보드"));
	}
	
	public static List<String> getRelatedProducts(String keyword) {
		// cache에 key가 있다면, DB 조회 과정을 스킵한다
		if(cache.containsKey(keyword)) {
			System.out.println("[Cache Hit] 캐시에서 연관 데이터를 반환합니다.");
			return cache.get(keyword); // 리턴을 한번 함
		}
		
		// 이후는 cache에 key가 없는 상태임
		// DB를 조회하는 과정
		System.out.println("[Cache Miss] DB에서 연관 데이터를 조회합니다.");
		List<String> data = database.getOrDefault(keyword, Collections.emptyList());
		
		// 핵심 포인트 : 인접 데이터까지 함께 캐싱함
		for(int i=0; i<data.size(); i++) {
			String related = data.get(i); // 키워드 가져옴
			
			// cache에 data key가 없고, DB에 key가 있다면
			if(!cache.containsKey(related) && database.containsKey(related)) {
				cache.put(related, database.get(related));
			}
		}
		
		// for문을 빠져나와 cache에 put된 상태
		// cache에 key를 넣음
		cache.put(keyword, data);
		return data;
	}
	
	public static void main(String[] args) {
		System.out.println(getRelatedProducts("노트북")); // miss 발생 - > 주변 데이터도 캐싱함
		System.out.println(getRelatedProducts("마우스")); // 이미 인접 캐싱되여 hit함

	}
}

 

결과

[Cache Miss] DB에서 연관 데이터를 조회합니다.
[노트북, 마우스, 키보드, 노트북 가방]
[Cache Hit] 캐시에서 연관 데이터를 반환합니다.
[마우스, 마우스 패드, 키보드]

 

* 인접한 데이터들을 함께 캐싱하여, 사용자의 다음 행동을 예측한 빠른 응답이 가능함

 


4. 캐시 히트(Cache Hit), 캐시 미스(Cache Miss)

1) 개념 설명

 - 캐시 히트 : 요청한 데이터가 캐시에 존재하여 즉시 응답이 가능한 경우

 - 캐시 미스 : 요청한 데이터가 캐시에 없어 원본 데이터 소스(DB 등..)를 다시 조회해야 하는 경우

 

캐시 히트는 빠른 응답을 제공하지만, 캐시 미스는 추가 연산(조회, 저장 등 로직)을 필요로 함

캐시 히트, 캐시 미스

 

2) 시나리오

 - 첫번째 요청 - > 캐시 미스 발생 - > DB에서 조회 후 캐시에 저장함

 - 두번째 요청 - > 캐시 히트 발생 - > 캐시에서 즉시 응답

 

결과적으로 캐시 히트가 많을수록 시스템 성능은 향상된다.

 


5. 캐시 적중률(Cache Hit Ratio)

개념

캐시 적중률은 전체 요청 중 캐시 히트가 발생한 비율임

Cache Hit Ratio = (Cache Hit 횟수 / 전체 요청 횟수) * 100%

 

전체 요청 수가 100, 캐시 히트 수가 80, 캐시 미스 수가 20이면 캐시 적중률은 80 / 100 * 100, 80%이다

 

* 캐시 적중률이 높을수록 시스템은 더 빠르고 효율적으로 동작함

 


* 적중률 80 ~ 90% 이상을 유지하는 것이 일반적이고, 이상적이다.

* 적중률이 낮다면 TTL이 너무 짧거나, 캐시할 데이터 선정이 잘못되었을 가능성이 높음

* 데이터 변경이 잦은 경우, 오히려 캐시 오버헤드가 생길 수 있으므로 주의해야함

* 실제 시스템에서는 Prometheus, Grafana, Elastic Stack 등을 활용해서 캐시 적중률을 모니터링 한다고 함

 


정리

핵심 개념 설명
시간적 지역성 최근 사용한 데이터는 곧 다시 사용될 가능성이 높음
공간적 지역성 인접한 데이터도 함께 사용될 가능성이 높음
캐시 히트 요청한 데이터가 캐시에 존재하여, 즉시 응답함
캐시 미스 캐시에 없어서 DB 등에서 새로 조회해야 하는 상황임
캐시 적중률 전체 요청 중 캐시 히트의 비율(성능 지표)