사전 준비
starter를 사용하여 프로젝트를 생성할 때 Spring Cache Abstraction을 추가해준다.
build.gradle에 아래 의존성이 추가된다.
implementation 'org.springframework.boot:spring-boot-starter-cache'
사용 방법
사용 방법은 굉장히 간편하다.
캐시를 활성화하기 위해 @EnableCaching 어노테이션을 추가한 후, 캐시를 사용하고싶은 메서드 위에 @Cacheable 어노테이션을 추가해주면 된다.
@EnableCaching
@Configuration
public class CacheConfig {
}
@RequiredArgsConstructor
@Service
public class BookService {
private final BookRepository bookRepository;
@Cacheable("books")
public List<Book> getAll() throws InterruptedException {
Thread.sleep(3000);
return bookRepository.findAll();
}
}
캐시가 사용되는지 직접 확인하기 위해 sleep을 걸었다.
결과를 확인해보자.
@Test
void test() throws InterruptedException {
Book book = new Book("title");
bookRepository.save(book);
List<Book> books1 = bookService.getAll(); // (1)
List<Book> books2 = bookService.getAll(); // (2)
System.out.println(books1);
System.out.println(books2);
}
1번에서 getAll() 메서드가 호출되고, 캐시에 결과가 저장된다. 2번에서는 캐시에 원하는 값이 있기 때문에 실제 메서드를 호출하지 않고 프록시에서 반환한다. 이런 이유로 1번에서는 응답을 받는데 3초가 걸리고, 2번에서는 즉시 응답을 받는다.
스프링의 캐시는 프록시를 통해 구현된다. 여기선 간단히 캐시를 조회하는 흐름만 살펴보고 프록시는 별도의 글에서 설명할 에정이다.)
1번이 이루어지는 과정은 아래와 같은 구조이다.
캐시에 원하는 데이터가 없는 상태를 cache miss라고 표현한다.
2번이 이루어지는 과정은 아래와 같은 구조이다. 캐시에 원하는 데이터가 있는 상태를 cache hit라고 표현한다.
조회시에 어떤 메서드가 어떤 캐시 이름을 사용하는지 구분해야 한다. 캐시 이름이 같다면 같은 값을 사용한다.
@Cacheable("books")
public List<Book> getAll() throws InterruptedException {
log.info("get all books");
Thread.sleep(3000);
return bookRepository.findAll();
}
@Cacheable("books")
public List<Book> getNewAll() throws InterruptedException {
log.info("get all books22");
Thread.sleep(5000);
return bookRepository.findAll();
}
getAll과 getNewAll은 같은 캐시 키를 갖는다. 직전에 설명한 것 처럼 캐시 hit상황에서 내부 로직은 수행되지 않는다. 따라서 실제 내부에서 수행하는 로직이 다르더라도 캐시 키가 같기에 같은 값을 반환한다. 동일한 값을 반환하기를 원치 않는 경우 캐시 키를 분리해야 한다.
조금 더 정확하게는 cacheNames와 key가 동일한 경우 동일한 값을 반환한다.
파라미터가 있는 메서드를 살펴보자.
@Cacheable("books")
public List<Book> getAllByTitle(String title) throws InterruptedException {
log.info("title");
Thread.sleep(5000);
return bookRepository.getAllByTitle(title);
}
파라미터가 기본으로 캐시 키로 사용된다. 위에 사용된 어노테이션을 풀어 쓰면 아래 형태가 된다.
@Cacheable(cacheNames = "books", key = "#title")
public List<Book> getAllByTitle(String title) throws InterruptedException {
log.info("title");
Thread.sleep(5000);
return bookRepository.getAllByTitle(title);
}
캐시 네임과 키가 동일한 경우 같은 캐시를 반환한다. 이름과 키가 겹치지 않게 주의해야 한다.
캐시 삭제
캐시 삭제는 @CacheEvict 어노테이션을 메서드에 추가하면 된다. 해당 메서드 호출시 지정한 키가 삭제된다.
@CacheEvict(cacheNames = "books", allEntries = true)
public void remove() {
log.info("remove all");
}
@CacheEvict(cacheNames = "books", key = "#title")
public void remove2(String title) {
log.info("remove");
}
캐시 업데이트
@CachePut을 사용해 캐시를 업데이트 할 수 있다.
@CachePut(value = "books", key = "#title")
public List<Book> getAll(String title) throws InterruptedException {
log.info("title");
Thread.sleep(5000);
return bookRepository.getAllByTitle(title);
}
@Cacheable과의 차이점은 @CachePut의 경우 캐시 hit 상황에서도 메서드가 실행된다. 캐시에서 데이터를 조회하는게 목적이 아닌, 쓰기가 목적인 경우 사용한다.
이외에도 조건부로 캐시를 적용하는 방법도 있다.
@CachePut(value="books", condition="#title=='hello'") // 매개변수를 보고 저장할지 결정
public String getBook(String title) {
// ...
}
@CachePut(value="books", unless="#result.length()<64") // 응답(return)을 보고 저장할지를 결정
public String getBook(String title) {
// ...
}