프로그래밍 일기 — Spring에서의 캐싱(Caching)

배우는 자(Learner Of Life)
17 min readOct 3, 2023

--

캐쉬(Cache)와 친해져야한다.

캐시(cash)하고 헷갈리면 안되지만, 사실 컴퓨터에서 캐쉬(cache)는 돈과 같다(1).

캐쉬(Cache)라는 단어를 프로그래밍 시작하면서 어렴풋이 듣기 시작했고, Spring을 배우면서는 안듣는 날을 찾기 어려울 정도로 자주듣는 단어 중 하나다. 그만큼 이것을 이해하고 활용하는 것이 개발자에게 매우 중요하다고 밖에 이해할 수 없다. 그도 그럴 것이 이 캐쉬라는 저장 공간이 특정 데이터를 임시적으로 저장해 언제든지 꺼내 쓸 수 있게하는 역할을 하기 때문이다. 특정 데이터를 저장하고 있기 때문에 자주 접근하는 데이터일 수록 캐시를 통해 쉽고 빠르게 읽어오는 것이 가능하고, 메인 스토리지에서 읽어와야할 필요성을 줄여줘 데이터 읽기에 대한 성능을 높히는데 큰 힘이된다.

그렇다면 어플리케이션이 커지고 복잡해질수록, 저장하고 기억해야할 데이터는 커지기 마련이고, 그에 따라서 캐쉬가 저장해야할 것들도 많아질 수 있다. 그러나 캐쉬는 빠르게 데이터를 읽어올 수 있다는 장점때문에 사용하는 것이기에 메인 메모리처럼 많은 데이터를 저장할 수는 없다. 결국, 캐쉬에 어떠한 데이터를 저장해 읽어오는지가 얼마나 효율적으로 캐쉬를 사용하는지를 결정할 것이며, 동시에 어플리케이션의 사용성과 성능을 검증하는 지표가 될 수 있다. 이 캐쉬를 얼마나 잘 사용하느냐가 실제 내 서비스나 어플리케이션에 대한 캐시(cash) 즉, 돈을 얼마나 잘 벌어올 수 있느냐를 판가름할 수도 있다는 의미다.

이렇듯 제한적이지만 자주 사용해야하는 데이터를 빠르게 읽어 올 수 있게한다는 점에서 캐쉬는 분명 매력적이다. 그렇기에 이 유용한 기능을 사용하지 않을 이유가 없다. 이번 프로젝트에서도 Cache개념을 적용했는데, 이 부분을 내가 구현한게 아니어서 제대로 이해하지 못하고 넘어갔다는 아쉬움이 있었다. 따라서 이번 글에서는 Spring에서 Caching을 어떻게 활용할 수 있는지에 대해 기록해보려한다.

Cache 추상화(2)

이 글에서는 Caching 추상화(abstraction)를 어떻게 활용하는지에 대해 다룰 것이며, 이 것이 어떻게 시스템의 성능을 증가시키는지에 대해 알아볼 것이다. 예제를 통해 현실에서 활용되는 메서드에 어떻게 caching을 활성화시키고, 이러한 메서드를 호출할 때 스마트한 cache 관리를 활용해 어떻게 성능을 증가시킬 수 있는지 배워본다. 즉, Spring에서의 Caching방법들이 어떤 것들이 있는지 알아보고, Annotation을 활용해 어떻게 Caching을 추상화할 수 있는지를 예제를 통해 알아본다.

1. Maven Dependency

먼저 spring-context 모듈에 있는 코어 caching 추상화 기능이 필요하다. 혹은 spring-context-support 라는 모듈도 존재한다. 이는 spring-context 를 기반으로 만들어진 것으로써, EhCache(3)및 Caffeine(4)에서 제공하는 CacheManager 클래스들을 제공한다. 만약 이들을 cache 저장소로 활용하고 싶다면, spring-context-support 를 사용할 수 있다. spring-context-supportspring-context 가 지원하는 것들을 포함하기 때문에, 이를 활용한다면 spring-context 를 굳이 불러올 필요는 없다.

또한 Spring Boot를 활용한다면 Spring Boot가 지원하는 caching기능의 활용을 위해 spring-boot-starter-cache 모듈을 불러올 수 있다. 이는 caching관련 dependency들을 쉽게 추가할 수 있게 해준다. 사실 이 모듈이 spring-context-support 모듈을 불러온다.

// spring-context
implementation 'org.springframework:spring-context:6.0.12'
// spring-context-support
implementation 'org.springframework:spring-context-support:6.0.12'
// spring-boot-starter-cache
implementation 'org.springframework.boot:spring-boot-starter-cache'

2. Caching 활성화

Caching을 활성화하는데 있어 Spring은 몇 가지 좋은 Annotation을 제공한다. 프레임워크에서 다른 설정 단계의 기능을 활성화하는 것과 다름없이 Annotation을 씌워주면 된다. caching 기능은 @EnableCaching Annotation을 설정 클래스에 씌워주는 것으로 활성가능하다.

@Configuration
@EnableCaching
public class CachingConfig {

@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("addresses");
}
}

혹은 XML 설정에서 아래와 같이 cache 관리를 활성화할 수 있다. Caching 활성화 이후 최소한의 설정을 하기 위해서는 cacheManager 를 반드시 등록해야한다.

<beans>
<cache:annotation-driven />

<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
name="addresses"/>
</set>
</property>
</bean>
</beans>

Spring Boot 사용시

Spring Boot사용시에는 @EnableCaching Annotation과 함께 classpath에 starter package가 있다는 것 만으로 같은 ConcurrentMapCacheManager 클래스를 등록할 수 있다. 따라서 별도의 Bean 선언이 필요없다.

또한, 하나 이상의 CacheManagerCustomizer<T> bean을 통해 자동으로 설정된 CacheManager 를 커스터마이징할 수 있다. CacheAutoConfiguration 은 초기화하기전 이러한 커스터마이저들을 자동으로 가져와 현재 CacheManager 에 적용한다.

@Component
public class SimpleCacheCustomizer
implements CacheManagerCustomizer<ConcurrentMapCacheManager> {

@Override
public void customize(ConcurrentMapCacheManager cacheManager) {
cacheManager.setCacheNames(asList("users", "transactions"));
}
}

3. Annotation으로 Caching 사용하기

Caching을 활성화했다면, 다음과정은 Caching 행위를 Annotation과 함께 메서드에 연결하는 것이다. Spring에서는 Caching관련 Annotation 여러가지를 제공한다.

@Cacheable

Caching 행위를 메서드에 활성화시키는 가장 쉬운 방법은 @Cacheable Annotation을을 씌우는 것이다. 이후에 결과가 저장될 Cache의 이름으로 변수화(parameterize)한다. getAddress() 메서드는 호출되기전, 먼저 cache의 주소( address ) 들을 체크한 후, 결과를 caching한다.

비록 대부분의 상황에서 cache는 하나로도 충분하지만, Spring 프레임워크는 다수의 cache들을 매개변수들로 전달하는 것을 지원한다. 아래와 같이, 만약 특정 cache가 필요한 결과를 이미 저장하고 있다면, 결과값이 리턴되면서 메서드는 호출되지 않을 것이다.

// 하나의 Cache를 활용할 때
@Cacheable("addresses")
public String getAddress(Customer customer) {...}

// 두 개 이상의 Cache를 활용할 때
@Cacheable({"addresses", "directory"})
public String getAddress(Customer customer) {...}

@CacheEvict

만약 이전과 같이 모든 메서드에 @Cacheable Annotation을 활용한다면, 우리가 자주 사용하지 않는 데이터값까지 Cache에 계속 저장하게된다는 문제가 발생할 수 있다. Cache는 쉽게 용량이 빨리 찰 수 있고, 이렇게되면 불필요한 데이터도 저장하게 될 수 있다.

이러한 단점을 피하기 위해 @CacheEvict 라는 Annotation을 활용할 수 있다. 이 주석은 하나 이상의 값을 제거할 수 있어 cache에 새로운 값이 저장될 수 있도록 한다. 아래 예제에서는 allEntries 라는 매개변수를 활용해 비워낼 cache의 값을 지정해준다. 이렇게하면 addresses 라는 cache에 있는 모든 엔트리를 제거하고 새로운 데이터를 저장할 수 있게한다.

@CacheEvict(value="addresses", allEntries=true)
public String getAddress(Customer customer) {...}

@CachePut

@CacheEvict 가 대용량의 Cache에서 필요없는 데이터를 제거해 데이터 조회의 필요성을 줄여주는 방법이라면, 또한 데이터를 Cache에서 너무 많이 제거하는 것도 좋지 않을 수 있을 것이다. 따라서 선택적으로 데이터를 업데이트할 필요가 있는데, @CachePut Annotation을 활용하면 메서드의 실행을 방해하지 않으면서 Cache의 내용을 업데이트할 수 있다. 즉, 메서드는 항상 실행되며 결과가 Cache에 저장된다는 것이다. @Cacheable@CachePut 의 차이점은 @Cacheable 은 항상 메서드의 실행을 skip한다는 것이다. 이와 대조적으로@CachePut 은 메서드를 실행하고 그 결과를 Cache에 저장한다.

@CachePut(value="addresses")
public String getAddress(Customer customer) {...}

@Caching

만약 메서드를 Caching하는데 있어 같은 타입의 Annotation을 여러개 사용하고 싶다면, @Caching 을 통해 여러 Caching Annotation을 하나로 묶을 수 있다. 이를 활용해 자신만의 커스터마이징된 Caching 로직을 구현할 수 있다. 이 방법을 잘못된 예제와 함께 비교해보면 조금 더 명백해질 것이다. @CacheEvict 는 Java에서 여러개가 한꺼번에 선언하도록 허용하지 않으므로 아래와 같은 예제는 컴파일 단계에서 에러가 날 것이다. 이를 구현하려면 @Caching Annotation을 통해 여러 @CacheEvict 를 묶어주어야한다.

// @CacheEvict를 여러개 사용한 잘못된 예제 (Compile 에러 발생)
@CacheEvict("addresses")
@CacheEvict(value="directory", key=customer.name)
public String getAddress(Customer customer) {...}

// @Caching을 활용해 여러개의 @CacheEvict를 묶은 모습
@Caching(evict = {
@CacheEvict("addresses"),
@CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) {...}

@CacheConfig

@CacheConfig Annotation을 활용하면 일부 Cache 설정을 하나의 클래스 레벨로 streamline(정리)할 수 있다. 이렇게해서 같은 것을 여러번 선언해야할 필요를 막아준다.

@CacheConfig(cacheNames={"addresses"})
public class CustomerDataService {

@Cacheable
public String getAddress(Customer customer) {...}

}

4. 조건부 Caching

가끔은 모든 상황에서 모든 메서드에대해 Caching을 적용하기 어려울 수 있다. 이전에 언급한 @CachePut Annotation을 활용하여 매번 메서드를 실행하고 결과값을 Cache에 저장할 수 있다.

@CachePut(value="addresses")
public String getAddress(Customer customer) {...}

조건부 매개변수(Conditional Parameter)

Annotation이 활성화될 때 더 많은 제어권을 얻고 싶다면, @CachePut 을 SpEL 표현을 받아 표현을 검증(evaluate)하는 방식으로 결과값을 cache에 저장하도록 보장하는 조건부 매개변수와 함께 매개변수화(parameterize)할 수 있다. 즉, caching을 할 조건을 명기하는 방법이다.

@CachePut(value="addresses", condition="#customer.name=='Tom'")
public String getAddress(Customer customer) {...}

Unless 매개변수

unless 매개변수를 통해 메서드의 입력값이 아닌 출력값을 기반으로 Caching을 제어할 수 있다. 이 아래 Annotation은 addresses 가 64문자보다 적지 않는한 이를 caching 한다. 주의할 것은 conditionunless 매개변수들이 caching annotation과 함께 같이 활용될 수 있다는 것이다.

이러한 조건부 Caching은 결과값의 크기가 매우 클때 상당히 유용하다. 또한 입력값에 기반해 행위를 커스터마이징하기 때문에, 모든 동작에 대해 일률적인(generic) 행위를 적용하는 것보다 상황에 따라 더 유용할 수 있다.

@CachePut(value="addresses", unless="#result.length()<64")
public String getAddress(Customer customer) {...}

5. XML 기반 Caching

만약 어플리케이션의 소스코드에 접근할 수 없다면, 외부에서 Caching 행위를 주입하는 방식으로 구현을 해야할 것이다. 이에 가장 유용한 방법이 XML기반 Caching이다.

<!-- 1. Caching할 서비스 정의 -->
<bean id="customerDataService"
class="com.your.app.namespace.service.CustomerDataService"/>

<bean id="cacheManager"
class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
name="directory"/>
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
name="addresses"/>
</set>
</property>
</bean>
<!-- 2. Caching행위 정의 -->
<cache:advice id="cachingBehavior" cache-manager="cacheManager">
<cache:caching cache="addresses">
<cache:cacheable method="getAddress" key="#customer.name"/>
</cache:caching>
</cache:advice>

<!-- 3. 정의한 행위를 CustomerDataService 인터페이스의 모든 구현체에 적용한다. -->
<aop:config>
<aop:advisor advice-ref="cachingBehavior"
pointcut="execution(* com.your.app.namespace.service.CustomerDataService.*(..))"/>
</aop:config>

6. Java 기반 Caching

Java @Configuration에서 Caching을 할 수도 있다. 아래와 같은 예제이다. 이렇게 Java 기반 설정을 잡아놓으면 @Component 상에서도 활용이 가능하다. 아래는 CustomerDataService 라는 클래스 컴포넌트에 초기 Java 의 기본설정 Annotation인@Configuration 를 통해 정의한 Caching 설정을 적용한 예제이다.

// Java Configuration을 통해 Caching 설정
@Configuration
@EnableCaching
public class CachingConfig {

@Bean
public CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
new ConcurrentMapCache("directory"),
new ConcurrentMapCache("addresses")));
return cacheManager;
}
}

// 위 설정한 것을 활용해 CustomerDataService 컴포넌트에 Caching 적용
@Component
public class CustomerDataService {

@Cacheable(value = "addresses", key = "#customer.name")
public String getAddress(Customer customer) {
return customer.getAddress();
}
}

이 글에서 참조한 모든 코드는 참조(5)에서 확인가능하다.

참조:

(1) https://pixabay.com/illustrations/money-financial-business-cach-1425581/

(2) https://www.baeldung.com/spring-cache-tutorial

(3) https://www.baeldung.com/spring-boot-ehcache

(4) https://www.baeldung.com/java-caching-caffeine

(5) https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-caching

--

--

배우는 자(Learner Of Life)
배우는 자(Learner Of Life)

Written by 배우는 자(Learner Of Life)

배움은 죽을 때까지 끝이 없다. 어쩌면 그게 우리가 살아있다는 증거일지도 모른다. 배움을 멈추는 순간, 혹은 배움의 기회가 더 이상 존재하지 않는 순간, 우리의 삶은 어쩌면 거기서 끝나는 것은 아닐까? 나는 배운다 그러므로 나는 존재한다. 배울 수 있음에, 그래서 살아 있음에 감사한다.

No responses yet