컴퓨터 구조에서 볼 수 있는 메모리 계층 구조는 프로세스 처리를 담당하는 cpu에 전달해야 하는 메모리를 빠르게 전달할 수 있는 목적을 두고 있어 빠른 메모리는 cpu에서 가깝게 위치하고 느린 메모리는 멀리 위치한 형태를 띄고 있다. 빠를 수록 비싸기 때문에 상위 계층의 메모리에는 실제 cpu에서의 사용 빈도가 높은 값들로만 저장되어 있고 사용되지 않는 대다수의 값들은 저렴한 하위 계층의 메모리에 저장하고 있다. cpu에서 사용해야 되는 값이 상위 계층의 메모리에서 저장되지 않아 탐색에 실패하면(cache miss) 하위 계층으로 내려가면서 메모리를 찾게 되기 때문에 상위 계층에서 탐색이 자주 이뤄질수록(cache hit) cpu가 빠르게 값을 불러와서 프로세스에 대한 처리를 진행할 수 있게 되는 만큼 캐싱은 성능 개선에 큰 역할을 기여할 수 있다.
상위 계층에 저장해야 되는 데이터(자주 참조가 되는 데이터)가 보이는 특징은 다음과 같다고 알려져 있기 때문에 이러한 지역성을 이용한 캐싱을 통해 사용자는 컴퓨터가 빠른 데이터를 가지고도 많은 데이터를 저장할 수 있는 것과 같은 효과를 보일 수 있다.
조금 전에 참조된 데이터는 다시 참조할 수 있는 확률이 높다. (ex. 변수 데이터)
사용한 데이터에 가까이 있는 데이터는 곧 참조될 확률이 높다. (ex. 메모리 상에 연속적으로 저장되는 배열 데이터)
캐싱은 컴퓨터 구조 뿐만 아니라 웹어플리케이션에서도 사용하게 되는데 매번 데이터베이스 서버에서 동일한 값을 읽어서 응답하는 대신 캐시 서버에 재사용되는 응답을 저장하고 사용자 요청에 대한 응답이 캐시 서버에 저장된 경우에는 바로 응답을 반환할 수 있는 구조를 통해 데이터베이스 부하를 줄이고 조회 성능을 개선할 수 있다.
스프링은 캐시 기능에 대한 추상화를 제공하고 사용자는 원하는 캐시 저장소를 빈으로 등록해서 사용할 수 있다. 기본적으로 등록된 저장소가 없는 경우 concurrentHashMap을 사용한 로컬 캐시 저장소를 사용하게 된다. 외부 캐시 저장소로는 Redis가 주로 사용된다.
키-값 형태의 데이터를 저장하는 NoSQL으로 문자열, 리스트, 해시, 셋과 같이 다양한 자료구조를 지원하고 있고 영속성을 지원하기 위해 사용된 데이터를 디스크에도 저장할 수 있는 기능이 지원된다.
기본적인 메소드에 캐싱을 적용하는 방법은 아래와 같이 어떤 인덱스에 키와 값을 저장할 지를 명시하기만 한다면 자동으로 메소드 객체를 키로 메소드 반환값을 값으로 저장하게 된다. 캐싱이 적용된 메소드의 경우 먼저 메소드 매개변수로 들어온 값으로 이미 저장된 값이 있는지 캐시 저장소에서 탐색을 하게 되고 존재하는 경우에는 내부의 메소드 동작 없이 결과를 바로 반환하고 없는 경우에는 메소드 동작 후 반환되는 결과를 캐시 저장소에 기록하는 방식으로 동작하게 된다.
@Cacheable("addresses")
public String getAddress(Customer customer) {...}
매개변수가 아닌 내부 필드를 키로 지정하고 싶은 경우에는 다음과 같이 설정하면 된다.
@Cacheable(value ="addresses", key = "#customer.name")
public String getAddress(Customer customer) {...}
매개변수가 여러개인 경우 각 매개변수의 조합을 통해 키로 지정하는 것도 가능하다.
@Cacheable(value ="addresses", key = {"#customer.id", "#address.zipcode"})
public String getAddress(Customer customer, Address address) {...}
캐시는 DB의 탐색 과정 없이 빠르게 응답을 반환할 수 있어 조회 성능 개선에 큰 기여를 하는 것은 자명하다. 하지만 실제 DB에서 변경된 값이 캐시에 반영되지 않고 과거의 값을 계속 저장하고 있다면 캐시가 지속되는 동안 사용자에게는 잘못된 응답을 계속 반환하기 때문에 오래된 캐시를 삭제하고 DB와 캐시 간의 동기화 작업은 중요한데 스프링 캐싱은 이러한 기능을 수행할 수 있는 아노테이션을 지원하고 있다.
키에 해당하는 값을 캐시 저장소에서 없애주는 기능을 제공해 오래된 캐시값을 비워주는 기능을 제공한다. allEntries 옵션을 true로 하게 되면 키가 일치하지 않아도 동일한 모든 값을 캐시 저장소에서 없애줄 수 있다.
@CacheEvict(value="addresses", allEntries=true)
public String changeAddress(Customer customer) {...}
메소드가 동작될 때 메소드 내부가 항상 동작되고 결과가 저장되서 캐시 저장소에 값을 업데이트 할 때 사용된다.
@CachePut(value="addresses", key="#customer.name")
public String changeAddress(Customer customer) {...}
하나의 메소드에 대한 결과를 여러 개의 캐시 장소에 저장하는 경우에는 다음과 같이 사용할 수 있다.
@Caching(evict = {
@CacheEvict("addresses"),
@CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) {...}
메소드에 대한 모든 결과를 캐싱하는 것이 아닌 특정 조건에 대해서만 저장할 때는 condtion을 통해 저장되는 조건을 지정할 수 있다.
@CachePut(value="addresses", condition="#customer.name=='Tom'")
public String getAddress(Customer customer) {...}