캐시(Cache)는 왜 쓸까?
데이터베이스는 거대한 창고와 같다.
원하는 정보를 찾으려면 그 넓은 창고에서 일일이 찾아야 한다.
당연히 시간도 많이 소모되고, 그에 대한 비용도 소모될 것이다.
반면 캐시는 책상 위에 놓인 메모지이다.
자주 찾는 정보는 메모지에 적어두고, 필요할 때마다 펼쳐서 보면
당연히 창고까지 일일이 갈 필요 없이 바로 답을 얻을 수 있기에
속도가 압도적으로 빠르다.
Spring Cache의 어노테이션들은 "메모지를 관리하는 규칙들"이다.
@Cacheable: "메모지에 있으면 그거 주고, 없으면 창고 가서 적어와"
가장 많이 쓰이는 조회 규칙
동작 흐름
사용자가 "1번 책 어딨어?"라고 물으면, 사서는 먼저
책상 위 메모지(@Cacheable)를 본다.
케이스 A (Cache Hit): 메모지에 "1번 책은 A 구역에 있음"이라고,
적혀 있으면 -> 창고에 안 가고, 바로 필요한 값을 준다. (메서드 실행 X)
케이스 B (Cache Miss): 메모지에 내용이 없다면? 사서가 직접
창고(DB)에 가서 데이터를 가져오고, 다음에 또 물어볼 것에 대비하여
메모지에 그 내용을 적어둔다. (메서드 실행 O)
똑같은 질문을 여러 번 할 때, 두 번째부터는
창고에 가는 수고(연산/조회 비용)를 사실상 0으로 만든다.
@CachePut: "창고 작업은 무조건 하고, 메모지도 최신으로 고쳐 써"
이것은 수정 규칙이다.
@Cacheable과 가장 큰 차이는 "일단 창고에 가느냐 마느냐"이다.
동작 흐름
사서는 메모지를 보지 않고, 무조건 창고(DB)로 달려가서 정보를 수정한다.
(메서드 무조건 실행)
수정이 끝났으면, 그 결과물을 메모지에 새로 덮어쓴다.
왜 @Cacheable을 안 쓰고 이걸 쓰나?
@CachePut을 사용한다.@CacheEvict: "데이터가 바뀌었으니 낡은 메모지는 버려"
삭제 or 초기화 규칙이다.
동작 흐름
삭제: 사서가 창고에서 책을 폐기하거나 아주 크게 정보를 바꾼다.
파기: 이때 책상 위에 있던 낡은 메모지를 찢어서 버린다.
왜 버려야 하나?
데이터 수정의 2가지 경우
방법 A (@CachePut): "DB도 고치고, 메모지(캐시)도 새 내용으로 고쳐 쓰자."
장점: 다음 조회 시 즉시 캐시에서 데이터를 가져올 수 있어 매우 빠름.
단점: 수정하는 과정에서 메모지에 적을
데이터를 만드는 연산이 복잡하거나 비용이 크다면 비효율적임.
방법 B (@CacheEvict): "DB는 고치고, 메모지는 그냥 찢어서 버리자."
장점: 메모지를 새로 적는 수고를 덜 수 있음.
어차피 나중에 누군가 요청할 때 다시 창고(DB)에서
최신 데이터를 가져와서 적으면 됨.
논리: "낡은 데이터(수정 전)를 보여주느니,
차라리 메모지를 없애서(삭제) 다음 사람에게 정확한 정보를
새로 가져와서 전달해주겠다."는 전략이다.
코드 예시
DTO (캐시에 저장할 데이터는 불변의 record 사용)
// record를 사용해 불변 객체로 정의
public record Book(Long id, String title, String author) {}
서비스 로직
@Service
public class BookService {
// 조회: 메모지에 있으면 주고, 없으면 창고(DB) 가서 가져와서 적어둠
// books는 캐시의 저장소 이름을 지정한 것
// key = "#id"는 메모지들 사이에서 특정 데이터 찾기 위한 이름표
@Cacheable(value = "books", key = "#id")
public Book getBook(Long id) {
simulateSlowService(); // DB 조회를 시뮬레이션하는 무거운 로직
return new Book(id, "Spring 공부법", "Junyoung");
}
// 수정: 창고(DB) 내용을 고치고, 메모지(캐시) 내용도 최신으로 업데이트함
@CachePut(value = "books", key = "#book.id()")
public Book updateBook(Book book) {
// 실제로는 repository.save(book) 등이 호출됨
return book;
}
// 삭제: 창고(DB)에서 지우고, 메모지(캐시)도 찢어서 버림
@CacheEvict(value = "books", key = "#id")
public void deleteBook(Long id) {
// 실제로는 repository.deleteById(id) 등이 호출됨
System.out.println(id + "번 책이 삭제되었습니다.");
}
// 성능 차이를 확인하기 위한 가상 지연 메서드
private void simulateSlowService() {
try {
Thread.sleep(3000); // 3초 대기
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}