애플리케이션을 개발하며 쓰기 동작보다 읽기 동작이 많은 데이터가 있다면 캐시 도입을 고민할 수 있다.
예를 들어, 상품의 카테고리 목록은 자주 바뀌지 않는 데이터들인데 쇼핑몰 내에서 페이지를 이동 할 때마다 전체 카테고리들을 DB에서 매번 Query해서 모두 불러오는 것은 비효율적이기에 이런 경우에도 캐시를 적용하면 좋은 케이스다.
일반적으로 캐시는 메모리에 데이터를 미리 적재하고 이를 빠르게 읽어 응답하는 구조다. 그래서 읽기 동작이 많은 서비스에 캐시를 사용하면 서비스 응답 속도를 향상할 수 있고, 시스템 리소스도 효율적으로 사용할 수 있다.
Spring은 다양한 저장소에 데이터를 캐시할 수 있는 기능을 제공한다. 또한 저장소에 독립적이고 추상화된 캐시 메커니즘을 제공한다. AOP 기반의 애너테이션을 제공하여 간편하게 캐시 기능을 운영 중인 애플리케이션에 도입할 수 있다.
레디스와 Spring Cache의 사용법을 간단하게 살펴보자.
레디스의 특징을 나열하자면 크게 다음과 같다.
메모리 특성상 저장된 데이터는 사라질 가능성이 있다. 이를 보완하고자 레디스는 관리하고 있는 데이터에 영속성을 제공한다. 즉, 메모리에 있는 데이터를 디스크에 백업하는 기능을 제공하며 그 방법들은 다음과 같다.
이 두 가지 기능은 각각 사용해도 되지만, 함께 설정하여 상호 보완 기능으로 사용해도 된다.
레디스는 사용자들이 실행한 명령어들을 이벤트 루프 방식으로 처리한다.
이벤트 루프 방식은 클라이언트가 실행한 명령어들을 이벤트 큐에 적재하고 싱글 스레드로 하나씩 처리한다. 멀티 스레드 환경에서 발생할 수 있는 컨텍스트 스위칭이 없으므로 효율적으로 시스템 리소스를 사용할 수 있는 장점이 있다.
ZRANGE
, ZREVRANGE
, ZRANGEBYSCORE
ZREVRANGEBYSCORE
는 ZSet(Sorted Set) 자료 구조를 사용한다. 이런 자료구조로 쉽고 빠르게 순위를 계산할 수 있다.Spring은 일부 데이터를 미리 메모리 저장소에 저장하고 저장된 데이터를 다시 읽어 사용하는 캐시 기능을 제공한다. 트랜잭션과 마찬가지로 AOP를 사용하여 캐시 기능을 구현하였고, 캐시 애너테이션을 사용하면 쉽게 구현할 수 있다. Spring에서 캐시 데이터를 관리하는 기능은 별도의 캐시 프레임워크에 위임한다.
Java의 인터페이스 기능은 Spring Framework 가 제공하고, 구현체 기능은 별도의 캐시 프레임워크에 위임하는 것에 비유할 수 있다.
캐시 저장소를 구성하는 방식은 두 가지로 구분된다.
로컬 캐시 방식으로 아키텍처를 설계하면 애플리케이션은 각각의 캐시 시스템을 가지며, 1:1 방식으로 사용한다. 그러므로 로컬 캐시들은 데이터를 서로 공유할 수 없다. 같은 이름의 데이터라도 각 서버마다 관리하고 있는 캐시 데이터는 다르다.
원격 캐시 아키텍처는 외부에 독립적인 데이터 저장소를 사용한다. 따라서 데이터를 캐시하거나 사용하면 I/O가 발생한다. 로컬 캐시 방식보다 I/O 시간만큼 서버 리소스와 시간이 더 소요된다. 네트워크를 사용하므로 외부 환경으로 캐시 성능이 영향을 받는다. 하지만 어떤 서버라도 모두 같은 데이터를 사용할 수 있고 일관된 방식으로 데이터를 읽고 쓸 수 있는 장점이 있다.
캐시 추상화에서는 캐시 기술을 지원하는 캐시 매니저를 Bean으로 등록해야 한다.
ConcurrentMapCacheManager
: JRE에서 제공하는 ConcurrentHashMap
을 캐시 저장소로 사용할 수 있는 구현체다. 캐시 정보를 Map
타입으로 메모리에 저장해두기 때문에 빠르고 별다른 설정이 필요 없다는 장점이 있지만, 실제 서비스에서 사용하기엔 기능이 빈약하다.SimpleCacheManager
: 기본적으로 제공하는 캐시가 없다. 사용할 캐시를 직접 등록하여 사용하기 위한 캐시 매니저 구현체다.EhCacheCacheManager
: Java에서 유명한 캐시 프레임워크 중 하나인 EhCache를 지원하는 캐시 매니저 구현체다.CaffeineCacheManager
: Java 8로 Guava 캐시를 재작성한 Caffeine 캐시 저장소를 사용할 수 있는 구현체다. EhCache와 함께 인기 있는 매니저인데, 이보다 좋은 성능을 갖는다고 한다.JCacheCacheManager
: JSR-107 표준을 따르는 JCache 캐시 저장소를 사용할 수 있는 구현체다.RedisCacheManager
: Redis를 캐시 저장소로 사용할 수 있는 구현체다.CompositeCacheManager
: 한 개 이상의 캐시 매니저를 사용할 수 있는 혼합 캐시 매니저다.<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>
@Cacheable
과 같은 애노테이션 기반의 캐시 기능을 사용하기 위해서는 먼저 별도의 선언이 필요하다.
@EnableCaching
@Configuration
public class CacheConfig {
@Bean
public RedisConnectionFactory basicCacheRedisConnectionFactory() {
// ...
}
@Bean
public CacheManager cacheManager() {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer(Object.class)));
Map<String, RedisCacheConfiguration> configurations = new HashMap<>();
configurations.put("hotelCache", defaultConfig.entryTtl(Duration.ofMinutes(30)));
configurations.put("hotelAddressCache", defaultConfig.entryTtl(Duration.ofDays(1)));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(basicCacheRedisConnectionFactory())
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configurations)
.build();
}
}
캐시 저장소에 캐시 데이터를 저장하거나 조회하는 기능을 사용할 수 있다. 애노테이션이 정의된 메서드를 실행하면 데이터 저장소에 캐시 데이터 유무를 확인한다. 적용된 메서드의 리턴 값을 기준으로 캐시에 값을 저장한다. 캐시에 데이터가 있다면 메서드를 실행하지 않고 바로 데이터를 리턴한다. 만약 예외가 발생하면 캐시 데이터는 저장하지 않는다.
속성 | 설명 | Type |
---|---|---|
cacheName | 캐시 이름(설정 메서드 리턴값이 저장되는) | String[] |
value | cacheName 의 alias | String[] |
key | 동적인 키 값을 사용하는 SpEL 표현식. 동일한 cache name을 사용하지만 구분될 필요가 있을 때 사용되는 값. | String |
condition | SpEL 표현식이 참일 경우에만 캐싱 적용. or, and 등 조건식 및 논리연산 가능 | String |
unless | 캐싱을 막기 위해 사용되는 SpEL 표현식. condition과 반대로 참일 경우에만 캐싱이 적용되지 않음 | String |
cacheManager | 사용 할 CacheManager 지정 | String |
sync | 여러 스레드가 동일한 키에 대한 값을 로드하려고 할 경우, 기본 메서드의 호출을 동기화함. 캐시 구현체가 Thread safe 하지 않는 경우, 캐시에 동기화를 걸 수 있는 속성 | boolean |
@Cacheable(value="hotelCache")
public HotelResponse getHotelById(Long hotelId) {
// ...
}
Long hotelId
메서드 인자의 toString()
메서드를 사용하여 캐시 키를 설정한다. 인자가 여러 개일 경우 모두 조합하여 캐시 키를 생성한다.
CacheManager
스프링 빈에 설정된 StringRedisSerializer
와 Jackson2JsonRedisSerializer
로 캐시 키는 문자열로 변경되어 저장되고, 캐시 데이터는 JSON 형식으로 변경되어 저장된다.
@Cacheable
애노테이션을 사용할 때는 메서드의 인자와 리턴 타입 변경에 유의해야 한다. 운영 중인 시스템의 인자를 추가한다면 캐시 키 값이 변경될 수 있다. 그러면 데이터 저장소에 저장된 데이터를 활용할 수 없게 된다. 메서드의 리턴 타입을 다른 클래스로 변경한다면, 데이터 저장소에 저장된 데이터가 언마셜링되는 과정 중 에러가 발생할 수 있다.
캐시 데이터를 캐시에서 제거하는 목적으로 사용된다. 원본 데이터를 변경하거나 삭제하는 메서드에 해당 애노테이션을 적용하면 된다. 원본 데이터가 변경되면 캐시에서 삭제하고 @Cacheable
애노테이션이 적용된 메서드가 실행되면 다시 변경된 데이터가 저장되기 때문이다.
속성 | 설명 | Type |
---|---|---|
cacheName | 제거할 캐시 이름 | String[] |
value | cacheName 의 alias | String[] |
key | 동적인 키 값을 사용하는 SpEL 표현식. 동일한 cache name을 사용하지만 구분될 필요가 있을 때 사용되는 값. | String |
allEntries | 캐시 내의 모든 리소스를 삭제할 지의 여부 | boolean |
condition | SpEL 표현식이 참일 경우에만 캐싱 적용. or, and 등 조건식 및 논리연산 가능 | String |
cacheManager | 사용 할 CacheManager 지정 | String |
beforeInvocation | true 일 경우 메서드 수행 이전 캐시 리소스 삭제, false 일 경우 메서드 수행 후 캐시 리소스 삭제 | boolean |
@CacheEvict(key = "testKey", condition="#caching")
public Object removeSome(boolean caching) {
// ...
}
캐시를 생성하는 기능만 제공하는 애노테이션이다.
@Cachable
과 유사하게 실행 결과를 캐시에 저장하지만, 조회 시에 저장된 캐시의 내용을 사용하지는 않고 항상 메소드의 로직을 실행한다.
속성 | 설명 | Type |
---|---|---|
cacheName | 캐시 이름(설정 메서드 리턴값이 저장되는) | String[] |
value | cacheName 의 alias | String[] |
key | 동적인 키 값을 사용하는 SpEL 표현식. 동일한 cache name을 사용하지만 구분될 필요가 있을 때 사용되는 값. | String |
condition | SpEL 표현식이 참일 경우에만 캐싱 적용. or, and 등 조건식 및 논리연산 가능 | String |
unless | 캐싱을 막기 위해 사용되는 SpEL 표현식. condition과 반대로 참일 경우에만 캐싱이 적용되지 않음 | String |
@CachePut(key = "testKey", condition="#caching")
public Object modifySome(boolean caching) {
// ...
}
두 개 이상의 캐시 애노테이션을 조합하여 사용한다.
속성 | 설명 | Type |
---|---|---|
cacheable | 적용 될 @Cacheable array를 등록 | Cacheable[] |
evict | 적용 될 @CacheEvict array를 등록 | CacheEvict[] |
put | 적용 될 @Cacheput array를 등록 | CachePut[] |
@Caching(cacheable = {
@Cacheable(value="primaryHotelCache", keyGenerator="hotelKeyGenerator"),
@Cacheable(value="secondaryHotelCache", keyGenerator="hotelKeyGenerator")
})
public HotelResponse getHotel(HotelRequest hotelRequest) {
// ...
}
클래스 단위로 캐시 설정을 동일하게 하고 싶을 때 사용한다.
속성 | 설명 | Type |
---|---|---|
cacheNames | 해당 클래스 내 정의된 캐시 작업에서의 default 캐시 이름 | String[] |
cacheManager | 사용 할 CacheManager 지정 | String |
@CacheConfig(cacheNames={"addresses"})
public class CustomerDataService {
// ...
@Cacheable
public String getAddress(Customer customer) {
// ...
}
}