SpringBoot기반 Redis Cache 활용법

Yorath Jang
Jul 13, 2019 · 13 min read

우리가 서비스를 개발할 때 백앤드 영역에서 Cache를 적극적으로 사용하게 되면 생각했던것 보다 더 드라마틱한 서비스 성능 개선을 가져올 수 있다(고 생각한다). 반대로 용도에 맞는 않는 정보나 서비스요청에 캐시를 남용하게 되면 서비스 신뢰도에 큰 문제가 생길 수 있는 위험성도 내포하고 있다.

이번글은 SpringBoot 기반에서 Cache서버를 얼마나 쉽게 구성할 수 있는지에 대한 간단한 개요정도의 글이다.

Cache? Cache를 왜 사용하는가?

Cache?

  • 한번 읽은(처리한) 데이타를 임시로 저장하고 필요에 따라 전송,갱신,삭제하는 기술로
    보통은 데이타의 보관장소로 서버의 메모리를 사용하는 경우가 많다
  • 그렇기 때문에 디스크에서 정보를 얻어오는 것보다 훨씬 빠른 I/O성능을 얻을 수 있으나 서버가 다운되거나 재부팅되는 경우 사라지는 성격의 휘발성을 가지고 있어
  • 영속적으로 보관할 수 없는, 말 그대로 임시적으로 보관하고 빠르게 그 정보에 접근하기 위한 용도로 사용해야 한다.
  • 물론 정보의 성격에 따라 별도의 디스크백업 및 TTL등의 설정으로 영구보관이나 오랜기간 유지가 가능하다. 단 이런 설정들이 꼭 필요하다면 Cache를 적용하는게 맞는지 한 번도 타당성을 검토해 보는게 좋겠다.

Cache를 쓰는 목적은 단순하다.

  • 서버간 불필요한 트래픽을 줄일 수 있고,
  • 그로 인해 웹어플리케이션 서버의 부하 감소시키고
  • 어플리케이션의 빠른 처리성능(조회)을 확보해서 궁극적으로 어플리케이션을 사용하는 고객에게 쾌적한 서비스경험을 제공한는 것

Cache의 대상이 되는 정보들

  1. 단순한, 또는 단순한 구조의 정보를 -> 정보의 단순성
  2. 반복적으로 동일하게 제공해야 하거나 -> 빈번한 동일요청의 반복
  3. 정보의 변경주기가 빈번하지 않고, 단위처리 시간이 오래걸리는 정보이고 -> 높은 단위처리비용
  4. 정보의 최신화가 반드시 실시간으로 이뤄지지 않아도 서비스 품질에 영향을 거의 주지 않는 정보

더 많은 조건들이 있겠으나,
저 조건들 중 2개이상 포함되는 성격의 서비스와 정보라면
Cache를 적용하는 것을 적극적으로 고려해 보아도 큰 무리가 없을 것 같다.

어떤 정보들을 Cache로 사용하나?

  • 포탈의 검색어
  • 쇼핑몰의 핫딜상품, 베스트셀러, 추천상품등
  • 상품의 카테고리와 카테고리별 등록상품 수
  • 방문자수, 조회수, 추천수
  • 1회성 인증정보 (SMS 본인인증정보, IP정보등)
  • 공지사항, Q&A

따로 설명을 하지 않더라도 Cache의 대상이 되는 정보들의 내용을 대입해보면 최소 1가지 이상은 포함되는 정보들이다.
우리가 사용하는 웹서비스들은 저 정보 뿐만이 아니라 훨씬 더 많은 정보들을 Cache를 적용해서 제공하고 있다고 보면 된다.

단적으로, 우리가 웹사이트에서 보는 대부분의 이미지는 다 캐싱된 이미지이다.(CDN의 가장 중요한 기능)


Cache를 사용할때 주의해야 할 것

  • 캐싱할 정보의 선택 -> 제일 중요하겠다.
  • 캐싱할 정보의 유효기간 (TTL — Time To Live ) 설정
  • 캐싱한 정보의 갱신시점

서비스를 설계할 때, 특히 백앤드의 경우 API 서비스의 기능설계단계에서 부터 Cache정책을 수립하는게 좋다
어떤 정보를 Cache로 적용할까를 먼저 따져보고 그 정보들을 어떤 시점에 어떤 주기로 갱신, 삭제를 할 지에 대한 최소한의 ‘캐싱전략’을 세우는 것도 어플리케이션 설계에서 중요한 영역중의 하나이다.

Java(로 쓰고 Spring으로 읽고, Springboot를 쓴다)에서 Redis를 사용하는 이유

  • 추상화된 API와 어노테이션을 제공
  • 어노테이션 사용만으로 일반 Service 메서드를 캐시 함수로 사용 가능
  • SpringBoot의 Auto Configuration 적용으로 Cache서버 설정이 간결
  • Springboot Starter Kit을 기본으로 제공함
  • spring-boot-starter-data-redis

스프링부트에서 공식지원(추상화API 제공)하는 Third-Party Cache 라이브러리

  • Redis
  • Caffeine
  • EhCache
  • Hazelcast
  • Infinispan
  • Guava -> springboot 2.0에서 삭제

위에서 얘기한 추상화된 API는 이 라이브러리들 모두에 동일하게 사용가능하다.
이 글의 주제는 Redis기반 설정이니 Redis기준으로 설명한다. 사실 그 외의 라이브러들은 EhCache와 Hazelcast정도 이름만 들어봤고 나머지는 생소한 것들이라 잘 알지 못한다.

설정하는 방법은 많이 복잡하거나 까다로울게 없다.

  1. 스프링부트 의존성 라이브러리 추가
  2. Redis 커넥션 정보 설정
  3. SpringBoot 메인클래스에 ‘나 캐시 사용할래’ 알려주기
  4. 사용할 Method ( controller 메서드 or service 메서드)에 어노테이션 달아주기 -> 캐시전략에서 정해진 ‘서비스&정보’
  5. 사용

이 정도의 작업만 해주면 Springboot 어플리케이션에서 Redis를 기본적인 Cache 서버로 사용할 수 있다.


Redis CacheServer설정

의존성 라이브러리 추가 (Maven기준) — pom.xml에 추가

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Redis 서버설정 — application.yml or application.properties 파일에 추가

spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379

SpringBoot에 캐시사용하겠다고 알려주기 — Springboot Main Application Class에 @EnableCaching 어노테이션 추가

@EnableCaching
@SpringBootApplication
public class RedisCacheApplication {
public static void main(String[] args) {
SpringApplication.run(RedisCacheApplication.class, args);
}
}

사용할 서비스 메서드에 어노테이션 달아주기

주요 어노테이션

  • @Cacheable @CachePut -> 캐시등록
  • @Cacheble은 캐시가 있으면 캐시의 정보를 가져오고, 없으면 등록한다.
  • @CacehPut은 무조건 캐시에 저장한다.
  • @CacheEvict -> 캐시삭제

이렇게 해 주면 우리가 Cache가 필요한 API나 서비스 메서드에 어노테이션을 붙여서 바로 사용할 수 있다.

  • 일반 메서드에 어노테이션만 붙여서 바로 사용, 별도의 Cache 메서드를 정의할 필요가 없음
  • @Controller 메서드에 적용하면 파라메터를 Redis의 키값으로 자동지정됨.
  • condition, unless 어노테이션 옵션으로 특정 조건에 따른 캐시적용여부 설정가능

캐시등록 및 조회 — @Cacheable, @CachePut

캐시 어노테이션의 key, condition, unless의 옵션인자들은 ‘SpEL’(Spring Expression Language)을 지원한다. 즉 캐시의 키 값과 조건들을 메서드의 인자객체와 반환객체의 항목들로 동적으로 구성할 수 있다는 의미이다.

https://www.baeldung.com/spring-expression-language

유명한(?) 밸덩 아저씨가 SpEL에 대해서 예제를 통해 잘 설명해 놓은 글이니 참고하면 좋을 듯 하다.

아래의 간단한 예제는 특정 ID의 포스트글을 조회하는 API인데 캐시를 적용하되,
반환된 Post객체에 담긴 실제 값 중 `shares`의 값이 500보다 작지 않을 경우에만 캐시를 적용하도록 설정해 놓은 코드이다. 500보다 작으면 굳이 캐시에 저장하지 않고 서비스를 직접 호출해서 값을 반환하겠다는 의미이다.

@Cacheable(value = "post-single", key = "#id", unless = "#result.shares < 500")
@GetMapping("/{id}")
public Post getPostByID(@PathVariable String id) throws PostNotFoundException {
log.info("get post with id {}", id);
return postService.getPostByID(id);
}

캐시삭제 — @CacheEvict

@CacheEvict(value = "post-single", key = "#id")
@DeleteMapping("/delete/{id}")
public void deletePostByID(@PathVariable String id) throws PostNotFoundException {
log.info("delete post with id {}", id);
postService.deletePost(id);
}

이렇게 @Controller 메세드에 직접 @CacheEvict 어노테이션을 직접 적용하게 되면 특정 ID의 포스트글을 지우는 요청하나로 서비스에서 실제 데이타를 지우고 동시에 Key로 지정된 — 즉, API의 요청 파라메터 — ID로 저장되어 있는 Redis키를 삭제할 수 있게 된다.

스프링부트에서 기본적인 Springboot-date-cache 설정을 통해 캐시를 쉽게 적용할 수 있는 방법을 간단히 살펴보았는데, 조금 더 디테일하게 Cache를 사용하고 싶다면, 별도의 cacheManager를 오버라이딩해서 사용하면 된다.

  • Redis key에 데이타를 저장할때와 꺼낼때 처리할 Serializer의 지정 ( 일반적으로 string or json serializer를 사용)
  • Key의 TTL(Time to Live)의 설정
  • Key의 Prefix 설정

이런 설정들을 임의로 하고 싶다면 별도의 Configuation Class를 만들어 사용하도록 하자.


Redis Cache 사용자 구성

  • 캐시의 TTL 적용 및 임의시점의 캐시등록/삭제 처리 등의 기능을 사용하고 싶으면 별도의 Configuration 설정필요
  • Springboot에서 제공하는 CachingConfigure 인터페이스를 구현해 놓은 거의 추상클래스인 CachingConfigureSupport클래스를 상속받아 별도의 Configuration Class 파일 추가
  • cacheManager() 을 오버라이딩 -> caheManager에 필요한 속성값 설정 (Serializer, TTL, keyPrefix 등)
  • @Cacheabe,CachePut,CacheEvice 어노테이션을 사용할때 cacheManager를 지정 또는, 필요한 클래스에 cacheManager를 주입받아 인라인 방식으로 직접 사용하면 된다.

RedisCache Configuration Class

@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {
...
@Bean
@Override
public CacheManager cacheManager() {
RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory());
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.prefixKeysWith("imhere:")
.entryTtl(Duration.ofHours(5L));
builder.cacheDefaults(configuration);
return builder.build();
}

캐시 어노테이션에 등록한 cacheManager 지정

@Cacheable(value = "post-single", key = "#id", unless = "#result.shares < 500", cacheManager="cacheManager")
@GetMapping("/{id}")
public Post getPostByID(@PathVariable String id) throws PostNotFoundException {
log.info("get post with id {}", id);
return postService.getPostByID(id);
}

메서드내에 필요한 곳에 인라인으로 캐시 사용

@Slf4j
@RequestMapping(value = "${api.path.default}")
@RestController
public class ClassesController extends BaseController {
@Autowired
CacheManager cacheManager;

@PostMapping("/academies/{academyId}/classes")
@Transactional
public ResponseEntity<ApiResponseDto> addClassInfo(@PathVariable Long academyId,
@Valid @RequestBody ClassesDto.ClassesInfo classesInfo) {

...
...

// 캐시 초기화
cacheManager.getCache("common-classes").evict("academies:" + academyId + ":classes");

...
...

}
}

개요정도의 수준으로 SpringBoot Redis Cache설정 및 활용법에 대해서 살펴 보았는데, 라이트한 목적으로 사용하기에는 어느정도 도움이 될 꺼라고 생각한다.
기회가 되면 cacheManager와 각각의 어노테이션에 대해서 좀 더 깊은 내용으로 알아볼까 한다.


참고자료

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade