우리가 서비스를 개발할 때 백앤드 영역에서 Cache를 적극적으로 사용하게 되면 생각했던것 보다 더 드라마틱한 서비스 성능 개선을 가져올 수 있다(고 생각한다). 반대로 용도에 맞는 않는 정보나 서비스요청에 캐시를 남용하게 되면 서비스 신뢰도에 큰 문제가 생길 수 있는 위험성도 내포하고 있다.
이번글은 SpringBoot 기반에서 Cache서버를 얼마나 쉽게 구성할 수 있는지에 대한 간단한 개요정도의 글이다.
Cache? Cache를 왜 사용하는가?
- Cache?
- 한번 읽은(처리한) 데이타를 임시로 저장하고 필요에 따라 전송,갱신,삭제하는 기술로
보통은 데이타의 보관장소로 서버의 메모리를 사용하는 경우가 많다 - 그렇기 때문에 디스크에서 정보를 얻어오는 것보다 훨씬 빠른 I/O성능을 얻을 수 있으나 서버가 다운되거나 재부팅되는 경우 사라지는 성격의 휘발성을 가지고 있어
- 영속적으로 보관할 수 없는, 말 그대로 임시적으로 보관하고 빠르게 그 정보에 접근하기 위한 용도로 사용해야 한다.
(물론 정보의 성격에 따라 별도의 디스크백업 및 TTL등의 설정으로 영구보관이나 오랜기간 유지가 가능하다. 단 이런 설정들이 꼭 필요하다면 Cache를 적용하는게 맞는지 한 번도 타당성을 검토해 보는게 좋겠다)
- 한번 읽은(처리한) 데이타를 임시로 저장하고 필요에 따라 전송,갱신,삭제하는 기술로
- Cache를 쓰는 목적은 단순하다.
- 서버간 불필요한 트래픽을 줄일 수 있고,
- 그로 인해 웹어플리케이션 서버의 부하 감소시키고,
- 어플리케이션의 빠른 처리성능(조회)을 확보해서 궁극적으로 어플리케이션를 사용하는 고객에게 쾌적한 서비스경험을 제공하는 것이다.
Cache의 대상이 되는 정보들
- 단순한, 또는 단순한 구조의 정보를 -> 정보의 단순성
- 반복적으로 동일하게 제공해야 하거나 -> 빈번한 동일요청의 반복
- 정보의 변경주기가 빈번하지 않고, 단위처리 시간이 오래걸리는 정보이고 -> 높은 단위처리비용
- 정보의 최신화가 반드시 실시간으로 이뤄지지 않아도 서비스 품질에 영향을 거의 주지 않는 정보
더 많은 조건들이 있겠으나,
저 조건들 중 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
- Springboot Starter Kit을 기본으로 제공함
스프링부트에서 공식지원(추상화API 제공)하는 Third-Party Cache 라이브러리
- Redis
- Caffeine
- EhCache
- Hazelcate
- Infinispan
Guava-> springboot 2.0에서 삭제
위에서 얘기한 추상화된 API는 이 라이브러리들 모두에 동일하게 사용가능하다.
이 글의 주제는 Redis기반 설정이니 Redis기준으로 설명한다. 사실 그 외의 라이브러들은 EhCache와 Hazelcate정도 이름만 들어봤고 나머지는 생소한 것들이라 잘 알지 못한다.
설정하는 방법은 많이 복잡하거나 까다로울게 없다.
- 스프링부트 의존성 라이브러리 추가
- Redis 커넥션 정보 설정
- SpringBoot 메인클래스에 '나 캐시 사용할래' 알려주기
- 사용할 Method ( controller 메서드 or service 메서드)에 어노테이션 달아주기 -> 캐시전략에서 정해진 '서비스&정보'
- 사용
이 정도의 작업만 해주면 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와 각각의 어노테이션에 대해서 좀 더 깊은 내용으로 알아볼까 한다.
참고자료
- [springboot-data-redis]
- [Redis 공식홈페이지]
https://redis.io/ - [springboot-date-cache 공식 레퍼런스 문서]
https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache
https://spring.io/projects/spring-data-redis
최근댓글