본문 바로가기
Java/Spring

스프링에서 어떻게 캐싱이 이루어질까? - 1편 스프링에서 캐싱 사용하기

by yongckim 2024. 8. 4.
728x90
반응형

캐싱을 사용하면 자주 읽는 데이터에 대해 I/O 작업을 최소화하여 성능을 크게 개선할 수 있습니다.

스프링에서는 캐싱을 적용하기 위해 스프링 3.1 버전부터 캐시 추상화를 지원합니다.

캐시 추상화를 통해 애플리케이션에 캐싱을 적용하기 위해 캐싱의 세부 구현 사항에 대해 신경쓰지 않아도 됩니다.

💡 공식 문서에서는 이에 대해 캐싱을 투명하게 제공한다라고 표현하고 있는데 투명하다는 의미는 개발자가 애플리케이션에서 사용하기 위해 세부 구현을 알 필요없이 간단한 선언만으로 사용할 수 있게 해줌을 의미합니다.

 

 

이를 코드로 살펴보면 다음과 같이 @EnableCaching, @Cacheable 어노테이션을 추가한것만으로 캐싱이 적용되게 됩니다.

@Configuration
@EnableCaching
class CacheConfig
@Service
@Transactional
class PostService(
    private val postRepository: PostRepository
) {
    @Transactional(readOnly = true)
    @Cacheable("posts")
    fun findAll(pageable: Pageable): Page<Post> = postRepository.findAll(pageable)
}

어노테이션을 사용하여 캐싱을 설정하고, 실제로 어떤 캐시를 사용할지에 대한 결정은 설정 파일이나 구성 클래스에서 이루어지기 때문에 캐시 전략을 변경할 때 코드 수정 없이 설정만 변경하면 되도록 할 수 있습니다.

캐싱 추상화 알아보기

캐싱에 대해 알아보기 전에 캐싱과 버퍼의 차이점에 대해 알아보기

  • 버퍼
    • 통신 중에 데이터의 처리 속도가 차이나는 경우 전송 속도 차이를 조정하여 효율적인 데이터 처리를 위해 사용됨
    • 한쪽이 다른 쪽을 기다려서 데이터를 전송해야 하기 때문에 데이터 블록을 청크 단위가 아닌 한번에 보내줍니다.
  • 캐싱
    • 자주 사용되는 데이터를 임시로 저장하여 데이터의 접근 속도를 빠르게 하는 목적으로 사용됨
    • 캐싱을 통해 자주 읽는 데이터에 대해 처리 성능을 개선할 수 있습니다.

위의 내용을 통해 버퍼와 캐싱은 유사하지만 목적에서 다르다는 점을 알 수 있습니다.

버퍼는 데이터 전송에 대한 속도 차이를 조정하여 효율적으로 처리하기 위해 사용되고 캐싱은 자주 읽는 데이터에 대해 임시로 저장하여 접근 속도를 빠르게 하여 성능을 개선하는 것이 목적입니다.

스프링 캐싱 추상화의 핵심

캐시 추상화의 핵심은 Java 메서드에 캐싱을 적용하여 캐시에서 사용 가능한 정보를 기반으로 실행 횟수를 줄이는 것입니다.

캐싱이 적용된 메서드가 호출될 때마다 캐싱 추상화는 해당 메서드가 주어진 인수에 대해 이미 호출되었는지 확인하는 캐싱을 적용합니다.

이때, 호출된 경우 실제 메서드를 호출하지 않고 캐시된 결과가 반환됩니다.

이런 방식을 통해 CPU 또는 IO 작업이 많이 이루어지는 작업에 대해 여러번 반복 작업을 거치지 않아 성능 향상을 기대할 수 있습니다.

 

💡 주어진 입력에 대해 호출 횟수에 관계 없이 동일한 출력을 반환하는 것이 보장되는 메서드에만 적용해야 합니다.
호출될 때마다 다른 값이 조회되는 랜덤 값 반환 같은 경우 랜덤 값이 아닌 계속 같은 값이 반환될 위험이 있습니다.

 

스프링 캐싱 추상화의 구현

스프링 캐싱은 인터페이스로 추상화된 객체이며, 결국 실제 캐싱을 하기 위해서는 캐싱 데이터를 저장할 수 있는 공간이 필요합니다.

즉, 캐싱 추상화는 캐싱 관련 로직을 작성할 필요가 없도록 해주지만 실제 데이터 저장소는 제공하지 않습니다.

이러한 추상화는 org.springframework.cache.Cache org.springframework.cache.cacheManager 인터페이스를 통해 구현됩니다.

public interface Cache {
    String getName();

    Object getNativeCache();

    @Nullable
    ValueWrapper get(Object key);

    @Nullable
    <T> T get(Object key, @Nullable Class<T> type);

    @Nullable
    <T> T get(Object key, Callable<T> valueLoader);

    void put(Object key, @Nullable Object value);

    void evict(Object key);

    void clear();
    
    ...
    ...
}

public interface CacheManager {
    @Nullable
    Cache getCache(String name);

    Collection<String> getCacheNames();
}

Spring은 이러한 추상화된 인터페이스의 구현체를 기본적으로 제공해줍니다.

만약 관련된 저장소 설정을 하지 않는다면 ConcurrentHashMap 을 사용하는 캐싱 저장소가 생성됩니다.

캐싱 추상화 사용하기

스프링 공식문서에서는 캐시 추상화를 사용하기 위해서는 다음과 같은 작업을 해야한다고 설명합니다.

  • 캐싱 선언 (Caching declation)
    • 캐시해야 할 메서드와 해당 정책을 식별합니다.
    • 아래 코드에서 @Cacheable이 이에 해당합니다.
    @Service
    @Transactional
    class PostService(
        private val postRepository: PostRepository
    ) {
        @Transactional(readOnly = true)
        @Cacheable("posts")
        fun findAll(pageable: Pageable): Page<Post> = postRepository.findAll(pageable)
    }
    
  • 캐싱 구성 (Cache configuration)
    • 데이터가 저장되고 읽혀지는 저장소를 정의합니다.
    • 아래 코드에서 @EnableCaching 가에 이에 해당됩니다. (아무 설정도 없으므로 지금은 기본값인 ConcurrentHashMap 를 사용합니다.)
    @Configuration
    @EnableCaching
    class CacheConfig
    

설정 클래스와 캐싱을 사용할 메서드를 지정하는 것만으로 간단하게 캐싱을 사용할 수 있습니다.

참고

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

https://docs.spring.io/spring-boot/reference/io/caching.html#io.caching.provider.simple

반응형