본문 바로가기

Spring

Spring 캐시 추상화 (Cache Abstraction) 간단하게 적용해보기

728x90

개인 프로젝트를 진행하면서 페이지의 공통 부분에 필요한 데이터를 aop로 적용한 부분이 있다. 때문에 자주 바뀌지 않는 데이터지만 지속적으로 실행되는 메서드가 존재했는데 결과 데이터를 캐시에 저장하면 좋을 것 같아서 찾다보니 캐시 추상화(Cache Abstraction)를 지원하는것을 알게 되었다. 그리고 기존에 redis를 세션 클러스터링으로만 사용하고 있었는데 아깝다고 생각하던 찰나였기 때문에 마침 잘된 것같다.

 

Spring Framework에서는 3.1버전부터 투명하게 캐싱기능을 적용하는 캐싱 추상화를 제공했다고 한다. 이는 트랜잭션 처리 처럼 쉽게 적용이 가능하고 4.1 버전에서 크게 확장되었다고 한다.

@transactional로 편리하게 트랜잭션 처리를 하는 것 처럼, 캐싱 관련 어노테이션으로 쉽게 캐싱 기능을 적용하고 개발자는 로직에만 집중할 수 있도록 하는게 캐싱 추상화라고 생각한다.

 

참고 문서

https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache

 

Integration

As a lightweight container, Spring is often considered an EJB replacement. We do believe that for many, if not most, applications and use cases, Spring, as a container, combined with its rich supporting functionality in the area of transactions, ORM and JD

docs.spring.io

 

CacheManager 설정

캐시 추상화를 적용하기 위해서는 트랜잭션의 TransactionManager 빈 설정처럼 CacheManager 빈 설정을 해야한다.

CacheManager 종류는 다양하게 있는데, 내 경우 redis를 사용하고 있었기 때문에 RedisCacheManager로 적용했다.

 

 

CacheConfig

import java.time.Duration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class CacheConfig {
	private LettuceConnectionFactory redisConnectionFactory;

	// 10분
	private final int EXPIRE_SECONDS = 600;	
	private final int EXPIRE_MINUTES = 10;
	
	@Autowired
	public CacheConfig(LettuceConnectionFactory redisConnectionFactory) {
    	//redisConnectionFactory 의존성 주입
		this.redisConnectionFactory = redisConnectionFactory;
	}
	
	@Bean
	public RedisCacheManager redisCacheManager() {
		RedisCacheConfiguration redisCacheConfiguration = 
				RedisCacheConfiguration.defaultCacheConfig()
				.serializeKeysWith(RedisSerializationContext
							.SerializationPair
							.fromSerializer(new StringRedisSerializer()))
				.serializeValuesWith(RedisSerializationContext
							.SerializationPair
							.fromSerializer(new GenericJackson2JsonRedisSerializer()))
				.entryTtl(Duration.ofMinutes(EXPIRE_MINUTES));
		
		return RedisCacheManager
				.RedisCacheManagerBuilder
				.fromConnectionFactory(redisConnectionFactory)
				.cacheDefaults(redisCacheConfiguration)
				.build();
	}
}

사실 나는 Serializer들에 대해서 잘 모른다. 캐시는 메모리에 key-value 형식으로 저장을 하기때문에 key와 value값에 대한 직렬화 설정이 필요하다.

 

serializeKeyWith는 key값에 대한 직렬화 설정

serializeValuesWith는 value값에 대한 직렬화 설정

entryTtl은 선택사항으로 저장된 캐시 데이터의 만료시간을 의미한다. 나는 10분으로 설정했다.

 

이렇게 CacheManager를 bean등록 해주면 캐시 어노테이션 사용할 준비는 끝났다.

 

Cache Annotation 사용

스프링 문서를 참고해보면 캐싱에 관련된 어노테이션은 5가지가 있다.

  • @Cacheable : 데이터를 캐싱하거나 조회
  • @CacheEvict : 캐싱된 데이터를 삭제
  • @CachePut : 데이터를 캐싱
  • @Caching : 여러 캐싱 어노테이션을 적용할때 사용
  • @CacheConfig : 클래스 단계에서 공통적으로 글로벌하게 설정을 적용할때 사용

개인적으로 @CachePut은 무조건 데이터를 캐싱할때 사용하는 기능이라고 생각해 사용하지 않았고

@Caching은 @CacheEvict를 여러개 적용하게 된다면 사용할듯 하다.

내 경우는 @Cacheable, @CacheEvict만 적용해봤다.

 

캐싱을 사용하면서 주의할 점이 있다.

호출횟수와 상관없이 동일한 입력에 대해서 동일한 출력이 나오도록 보장된 메서드에만 적용해야한다는점이다.

 

@cacheable

@Cacheable(value = "work", key = "#seq")
	public Work getWorkOne(int seq) {
		return workRepository.findById(seq).get();
	}

@Cacheable(value = "groupList", 
			   key = "#workGroup.work_seq.toString().concat('|').concat(#workGroup.member_seq.toString())", 
			   condition = "#workGroup.search == null or #workGroup.search == ''")
	public List<WorkGroup> getWorkGroupList(WorkGroup workGroup) {
		return workMapper.getWorkGroupList(workGroup);
	}

코드를 보면 getWorkOne과 getWorkGroupList 메서드에 다른 형식으로 설정이 되어있다.

설정시 속성에 대해서는 SpEL 문법을 사용한다고 한다.

  • value : cacheNames와 같은 속성으로 데이터가 저장되는 이름이다.
  • key : 데이터 저장될 key값이다. getWorkGroupList처럼 커스텀해서 정의하거나, KeyGenerator를 따로 선언해서 사용할 수 있다. 내 경우에 #로 매개변수값을 활용해 key를 설정했다.
  • condition : 캐싱이 될 조건을 설정한다. 

추가로 unless라는 속성도 있는데 condition과 반대로 캐싱이 안될 조건을 설정하는 속성이다.

 

@cacheEvict

@CacheEvict(value = "categoryList", 
		    	key = "#work_seq.toString().concat('|').concat(#member_seq.toString())")
	public void clearCategoryListCache(int work_seq, int member_seq) {
		
	}

@CacheEvict(value = "categoryList", 
		    key = "#workCategory.work_seq.toString().concat('|').concat(#workCategory.member_seq.toString())")
	public int insertWorkCategory(WorkCategory workCategory) {
		workCategory.setDefaultyn("N");
		workCategoryRepository.save(workCategory);
		return 1;
	}

캐시를 삭제하는 @cacheEvict이다. 캐시 슈팅률이 떨어지게 되면 성능이 오히려 저하되기 때문에 적절한때에 캐시를 삭제하는 cacheEvict를 잘 사용해야한다.

 

insertWorkCategory메서드의 경우 새 데이터를 저장하는 메서드기 때문에 cacheEvict 처리를 해주었다.

하지만 나는 매개변수가 일정하지않고 복잡한 경우가 있는 메서드도 많기 때문에 그것들에 대해 캐시 삭제를 위한

clearCategoryListCache용 메서드를 추가했다.

아무 로직 없이 캐시만 지우는 용도로 사용할때 호출할 생각이다.

 

 

다른 기능들에 대해서는 추후에 적용해보면서 내용을 추가할 예정이다.

 

캐시 데이터 확인

이미지를 보면 위에서 설정한 cacheNames와 key값이 설정된 직렬방식에 의해 저장되어있고, get했을때 값또한 직렬화 되어있는것을 확인할 수 있었다.

 

또한 select 메서드 동작시간을 확인해봤는데 데이터가 캐싱되어 있는 경우에 select 메서드가 동작하지 않고 캐시에서 가져오는걸 확인할 수 있었다.

728x90