스프링부트 Caching 도입하기(Redis, Ehcache)

Belluga·2021년 7월 23일
12

✨ Caching

가져오는데 비용이 드는 데이터를 한번 가져온 뒤 복사본을 속도가 빠른 임시 공간에 저장해둠으로써 애플리케이션 처리속도를 높이는 방식입니다.

그렇다면 캐시는 언제 어디서 사용될까요?
캐시는 애플리케이션 전반에 사용될 수 있으며 원본 데이터에 접근하는 시간이 오래 걸리는 경우, 값을 다시 계산하는 시간을 절약하고 싶은 경우 사용될 수 있습니다.

기본 동작

1. 데이터를 요청합니다.
2. 캐시에 있는지 확인합니다.
3. 캐시에 있다면 캐시에서 데이터를 가지고 오고 없다면 실제 저장공간에서 데이터를 가지고 옵니다.
4. 실 저장공간에서 데이터를 가지고 왔다면 해당 데이터를 캐시에 저장합니다.

cache hit : 참조하려는 데이터가 캐시에 존재할 때 cache hit라 합니다.
cache miss : 참조하려는 데이터가 캐시에 존재 하지 않을 때 cache miss라 합니다.
cache hit ratio(캐시 히트율) : (cache hit 횟수)/(전체참조횟수) = (cache hit 횟수)/(cache hit 횟수 + cache miss 횟수)

cache miss가 발생하는 경우 실제 저장공간에서 데이터를 가져와야하기 때문에 비효율적이라고 할 수 있습니다. 캐싱의 활용도를 높이기 위해서는 캐시 히트율을 높이는것이 중요합니다.

캐시 히트율을 높이려면?
자주 참조되며 수정이 잘 발생하지 않는 데이터들로 구성되어야 합니다.
데이터의 수정이 잦은 경우 데이터베이스 접근 및 캐시 데이터 일관성 처리 과정이 필요합니다.
1000번을 같은 데이터를 참조한다고 하더라고 매번 데이터베이스와 캐시에 접근해야합니다.

스프링의 캐시 추상화

스프링의 캐시 추상화는 캐시 기술에 종속되지 않으며 AOP를 통해 적용되어
애플리케이션 코드를 수정하지 않고 캐시 부가기능을 추가할 수 있습니다.

캐시 추상화에서는 캐시 기술을 지원하는 캐시 매니저를 빈으로 등록해야 합니다.

캐시 데이터를 ConcurrentHashMap에 저장하는 ConcurrentMapCacheManager
EhCache를 지원하는 EhCacheCacheManager, RedisCacheManager 등 다양한 캐시 매니저가 존재하며 캐싱 전략에 따라 적절한 캐시 매니저를 사용할 수 있습니다.

Local Caching vs Global Caching

Local Cache는 로컬 서버 내부 저장소에 데이터를 보관합니다.
서버에서 바로 데이터를 서비스할 수 있기 때문에 속도가 빠르다는 장점이 있지만
다중 서버 환경에서는 각 서버에 중복된 데이터를 보관해야 하며 서버간 데이터 일관성이 깨질 수 있습니다.
캐싱할 데이터의 양이 많을 경우 더욱 부하가 커지게 됩니다.

Global Cache는 서버와 분리된 별도 저장소에 데이터를 보관합니다.
네트워크 I/O가 발생하여 Local Cache보다 속도가 느리다는 단점이 있지만
중복 데이터 및 데이터 일관성 문제가 없다는 장점이 있습니다.

저는 Global Caching 용도로 인메모리 데이터 저장소 Redis를 사용하였습니다. 데이터를 메모리에 저장하기 때문에 디스크 기반의 데이터베이스보다 빠르며 이는 캐시 관점에서 매우 적합합니다.

해당 프로젝트에서는 자주 조회되고 수정이 자주 발생하지 않는 상품 정보카테고리 정보를 캐싱하도록 하겠습니다.

카테고리 정보 조회 기능에 대해서는 Local Caching 방식을 선택하였습니다. 카테고리 정보는 데이터의 양이 많지 않아 중복으로 데이터에 저장이 되더라도 부하가 적으며 데이터의 일관성이 깨지더라도 비즈니스에 큰 영향이 없기 때문입니다.

반면 상품 정보 조회 기능을 개발할때는 Global Caching 방식이 유리할 것이라고 생각되었습니다. 상품 정보는 상대적으로 데이터의 양이 많아 중복으로 데이터를 저장하게 되는 경우 자원의 낭비가 심하며 비즈니스 관점에서 일관성이 중요한 데이터이기 때문입니다.

먼저 Global Caching을 위한 RedisCacheManager부터 살펴보도록 하겠습니다.

Redis

라이브러리 추가

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.3.9.RELEASE'

build.gradle 파일에 spring-boot-starter-data-redis 의존 라이브러리를 추가해줍니다.

Configuration

@Configuration
@EnableCaching
public class RedisCacheConfig {

    @Bean
    public CacheManager userCacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues()
                .entryTtl(Duration.ofHours(5L));
        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory).cacheDefaults(redisCacheConfiguration).build();
    }
}

@EnableCaching 어노테이션을 추가함으로써 캐싱 기능을 활성화할 수 습니다.
이 때 cacheManager를 등록해야 하는데 스프링부트에서 스타터 패키지와 EnableCaching 어노테이션이 있다면 ConcurrentMapCacheManager를 기본으로 등록합니다.

우리는 별도의 Redis Cache Configuration 설정을 통해 RedisCacheManager를 등록하겠습니다.

RedisCacheConfiguration 오브젝트를 통해 TTL(Time To Live), disableCachingNullValues, key&value 직렬화 등 캐싱 정책을 설정할 수 있습니다.

@Cachable, @CacheEvict

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductMapper productMapper;

    public void createProduct(CreateProductRequestDto createProductRequestDto, long sellerId) {
        productMapper.create(createProductRequestDto.toEntity(sellerId));
    }

    @Cacheable(value="product", key = "#id", unless="#result == null")
    public Product getById(long id) {
        return productMapper.getById(id).orElseThrow(NotFoundProductException::new);
    }

    @CacheEvict(value="product", key = "#id")
    public void modifyProduct(long id, ModifyProductRequestDto modifyProductRequestDto, AuthMember authMember) {
       Product product = getById(id);
       if((authMember.getRole() != Role.ADMIN) && !authMember.getId().equals(product.getSellerId())) {
           throw new ForbiddenException();
       }
       product.modifyProduct(modifyProductRequestDto);
       productMapper.modifyProduct(product);
    }
}

서비스 레이어 method에 어노테이션을 선언합니다.

@Cacheable
Redis에 캐싱된 데이터가 있으면 메서드를 실행없이 데이터를 반환하고, 없으면 DB에서 조회한 다음 메서드 return값을 Redis에 캐시합니다.

@Cacheable은 value와 key를 같이 사용하여 캐시의 키값으로 사용합니다.

Spring @Cacheable은 내부적으로 Spring AOP를 이용하기 때문에 @Async, @Transactional 등과 마찬가지로 같은 객체내의 method끼리 호출시에는 @Cacheable이 설정되어있어도 캐싱되지 않습니다.

@Cacheable 어노테이션에 지정할 수 있는 속성은 위와 같습니다.

@CacheEvict
지정된 키에 해당하는 캐시를 삭제합니다. 데이터를 수정 시 데이터베이스의 데이터는 수정되지만 캐시 저장소의 데이터는 수정 전의 상태로 남아있게 되면 데이터 조회시 무효화된 데이터를 조회하게 되기 때문에 이때 캐시가 만료되기 전에 수정된 데이터를 조회하면 데이터 기존 캐시 데이터를 제거해야 합니다.

Ehcache

라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.ehcache:ehcache:3.8.0'
implementation 'javax.cache:cache-api:1.1.0'

build.gradle 파일에 의존 라이브러리를 추가해줍니다.

스프링에 우리가 Ehcache 3를 사용함을 알리기 위해 JSR-107를 구현하는 cache-api 라이브러리를 추가해줍니다.


<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">

    <cache alias="categoryCache"> <!-- cache 요소는 CachceManager에 의해 작성되고 관리될 Cache 인스턴스를 나타낸다. Cache<k,v> 형태로 인스턴스가 생성된다. alias에는 캐시의 이름을 지정한다. -->
        <key-type>java.lang.Integer</key-type> <!-- key-type 요소는 Cache 인스턴스에 저장될 캐시의 키의 FQCN을 지정한다. 즉, 키의 타입을 명시해주면 된다. 기본 값은 java.lang.Object 이다. -->
        <value-type>com.flab.demo.category.domain.Category</value-type> <!-- value-type 요소는 Cache 인스턴스에 저장된 값의 FQCN을 지정한다. 기본 값은 java.lang.Object 이다. -->
        <expiry> <!-- expiry는 캐시 만료기간에 대해 설정하는 요소이다. -->
            <ttl unit="minutes">5</ttl> <!-- ttl에는 캐시 만료 시간을 지정하며 unit에는 단위를 지정한다. -->
        </expiry>
        <resources> <!-- resources는 캐시 데이터의 저장 공간과 용량을 지정한다. 만약 힙 메모리만 사용한다면 <heap> 요소만으로 대체할 수 있다.  -->
            <heap unit="kB">10</heap> <!-- heap은 JVM 힙 메모리에 캐시를 저장하도록 세팅하는 요소이다. -->
        </resources>
    </cache>
</config>

resources/ehcache.xml
캐시에 대해 어떻게 처리할 것인지 정의하는 ehcache.xml을 작성합니다.

spring.cache.jcache.config=classpath:ehcache.xml

application.properties
작성한 ehcache.xml 파일을 Spring이 알도록 하기 위해 application.properties을 작성합니다.

@Configuration
@EnableCaching
public class CacheConfig {

}

Spring Boot에서 캐싱 지원을 활성화하기위해 Configuration 클래스에 @EnableCaching 을 붙여줍니다.

애플리케이션에서 주입받는 CacheManager를 출력해보면 JCacheCacheManager가 주입됨을 확인할 수 있습니다.

References

http://dveamer.github.io/backend/SpringCacheable.html

https://chagokx2.tistory.com/98

https://coding-start.tistory.com/271

https://onecellboy.tistory.com/260

https://medium.com/finda-tech/spring-%EB%A1%9C%EC%BB%AC-%EC%BA%90%EC%8B%9C-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-ehcache-4b5cba8697e0

0개의 댓글