이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › 구조와 운영
캐시는 무엇이고, 쓰면 무엇을 얻고 무엇을 잃는가
캐시는 오래 걸려 얻은 결과를 가까운 곳에 적어 두었다가, 같은 것을 다시 달라고 하면 적어 둔 것을 주는 장치입니다. 이 실습의 코드에서는 cache라는 Map이 적어 두는 곳이고, find 메서드 안의 cache 줄들이 "적어 둔 것이 있으면 그것을 주고, 없을 때만 느린 조회(slowLoad)를 한 뒤 적어 둔다"는 일을 합니다. slowLoad는 원본(source)에서 값을 읽어 오는 느린 일이고, 부른 횟수를 slowCount로 셉니다. 실제 시간은 재지 않고 slowLoad가 불린 횟수로 "얼마나 느린 일을 했는가"를 보입니다. 이 실습은 cache 줄을 지워 보고, 두 번 찾는 사이에 원본의 값을 바꾸거나 cache.remove(1) 줄로 캐시의 값을 지워 가며 같은 코드를 컴파일해 실행하고, 느린 조회가 몇 번 일어나는지와 두 번째로 받은 값이 원본과 같은지를 봅니다. (static은 main이 객체를 만들지 않고 바로 부를 수 있게 붙인 것으로 보고, 이 실습에서는 다루지 않습니다.)
실행으로 대조 · JDK 21에서 다섯 가지 조합을 javax.tools로 컴파일하고 실행해 대조
import java.util.*;
public class Main {
// 원본: 값이 원래 들어 있는 곳
static Map<Integer, String> source = new HashMap<>();
static int slowCount = 0;
// 느린 조회: 원본에서 읽어 온다. 부른 횟수를 센다
static String slowLoad(int id) {
slowCount++;
return source.get(id);
}
// 캐시: 읽어 온 값을 적어 두는 곳
static Map<Integer, String> cache = new HashMap<>();
static String find(int id) {
// 적어 둔 값이 있으면 주고, 없으면 읽어 와서 적어 둔다
if (cache.containsKey(id)) return cache.get(id);
String v = slowLoad(id);
cache.put(id, v);
return v;
}
public static void main(String[] args) {
source.put(1, "키보드");
// 1번 값을 처음 찾는다
find(1);
// 1번 값을 다시 찾는다
String second = find(1);
// 느린 조회 횟수 / 두 번째로 받은 값 / 원본의 값
System.out.println(
slowCount + " / " + second + " / " + source.get(1));
}
}
컴파일러와 실행 환경은 JDK 21.0.12.1(javac 21)입니다. 다섯 가지 조합(캐시를 두는가 두 가지와 두 번 찾는 사이에 하는 일 세 가지에서, 캐시가 없으면 cache.remove(1) 줄을 쓸 수 없어 빠지는 한 가지를 뺀 것)을 모두, 화면의 코드와 같은 소스(줄 단위로 같은 글, import 줄까지 포함)를 javax.tools로 컴파일하고, 클래스 파일을 새 클래스 로더로 읽어 Main.main을 실행해 표준 출력을 잡은 결과와 대조했습니다. 대조한 것은 컴파일 성공 여부, 출력된 세 값(slowLoad가 불린 횟수, 두 번째 find가 돌려준 값, 원본의 값)과 마지막 단계 문장이 보이는 출력 글입니다. 단계 문장이 기대는 "처음 find(1) 호출을 한 번 한 뒤 slowLoad는 1번 불렸고 받은 값은 키보드"라는 사실은, 처음 find의 결과를 출력하도록 고친 별도의 소스(캐시가 있는 것과 없는 것)를 같은 방식으로 실행해 확인했습니다. 화면의 코드는 실제로 컴파일되고 실행되는 프로그램이며 줄인 것은 없습니다. slowLoad는 실제로 오래 걸리지 않고 시간도 재지 않았습니다. "느리다"는 slowLoad가 불린 횟수로만 나타냈고, 실제 DB나 네트워크의 시간은 측정하지 않았습니다. 이 캐시는 Map 하나이고, 적어 둔 값을 언제 버릴지 정하는 규칙(유효 시간, 크기 제한)이 없어서 cache.remove(1) 줄로 지우기 전까지 값이 남습니다. 문서는 직접 열어 확인했습니다. Spring Framework Reference의 Cache Abstraction 도입부(https://docs.spring.io/spring-framework/reference/integration/cache.html)는 캐싱을 기존 애플리케이션에 투명하게 더하는 지원이라는 설명까지만 열렸고, 같은 문서의 하위 쪽(https://docs.spring.io/spring-framework/reference/integration/cache/strategies.html)에서 같은 인자로 이미 호출했으면 메서드를 실행하지 않고 캐시된 결과를 돌려주며 아니면 실행해 결과를 캐시에 넣는다는 것, 캐시의 내용을 갱신하거나 항목을 지우는 기능이 있다는 것을 확인했습니다. RFC 9111(HTTP Caching)은 1절(캐시가 응답을 저장해 응답 시간과 네트워크 사용을 줄인다), 4절(저장한 응답을 다시 쓰는 조건), 4.2절과 4.2.4절(유효 기간을 넘긴 저장 응답을 stale이라 부른다)을 확인했고, 문서의 끝 7천 자 가량은 읽지 못했습니다. 적중과 미스라는 낱말을 그 문서들이 쓰는지는 확인하지 못했고, 설명집에는 이 낱말을 일반적인 용어로만 적었습니다. 이 실습의 Map 캐시는 문서가 말하는 방식의 한 예일 뿐 스프링이나 HTTP 캐시의 동작을 실행해 대조한 것은 아닙니다. 이 밖에 여러 스레드가 동시에 쓰는 캐시, 캐시가 가득 찼을 때 무엇을 버릴지, 유효 시간(TTL), 인기 있는 값이 한꺼번에 사라질 때 원본에 요청이 몰리는 일, 스프링의 @Cacheable, 브라우저와 서버 사이의 HTTP 캐시(ETag), Redis는 다루지 않았습니다. 이들은 "세부 동작 실습: 캐시와 TTL, 캐시 추상화, ETag, Redis"에서 다룹니다.
줄을 그은 곳은 처음 코드에서 지운 것이고, 색을 칠한 곳은 새로 쓴 것입니다.