본문 바로가기
Back-end/Spring

[Spring] Spring 캐시 사용하기

by 며루치꽃 2024. 2. 18.

0. 서론

각 비즈니스 로직에서 콘텐츠를 더 빠르게 제공하기 위해 캐싱을 이용하게 되는데요. 이번 글에서는 Spring 캐시 관련 기능을 살펴보고 스프링이 캐시를 AOP로 추상화하여 편리하게 개발할 수 있도록 지원하는지 살펴보겠습니다.

1. Spring 캐시와 Web 캐시

우선 캐싱에 대해 간단히 살펴보겠습니다. 

캐싱(Caching)

캐싱은 데이터를 빠르게 읽고 처리하기 위해 속도가 빠른 메모리를 활용하여 임시로 데이터를 저장하는 기술입니다. 계산된 값을 임시로 저장해 동일한 계산이나 요청이 필요할 때 다시 계산하지 않고 빠르게 결과값을 사용하기 위해 사용됩니다.

캐시(Cache): 캐싱 기술을 사용할 때 사용되는 임시저장소 입니다.

 

저희가 이번 글에서 살펴볼 캐싱은 Spring 캐싱입니다.

Spring Cache와 Web Cache는 서로 다른 맥락에서 캐싱과 관련된 서로 다른 두 가지 개념입니다. 각각을 개별적으로 이해해야합니다.

 

웹 캐싱(Web Caching)

웹 캐싱의 기본 목표는 원본 소스에서 동일한 리소스를 반복적으로 가져올 필요가 없도록 하여 콘텐츠를 더 빠르게 제공하는 것입니다. 이는 서버 로드를 줄이고, 응답 시간을 개선하며, 대역폭 사용량을 최적화하는 데 도움이 됩니다. 매번 서버에서 가져오는 대신 캐시된 콘텐츠를 제공하여 네트워크 트래픽을 줄이고 성능을 향상시키는 것을 목표로 합니다. 웹 캐싱은 대표적으로 웹브라우저 캐싱, CDN 등이 있습니다.

 

웹브라우저 캐싱

웹브라우저에서 어떤 페이지의 데이터를 로딩했을 때 보통의 웹페이지는 HTML, CSS, 이미지 등이 자주 바뀌지 않습니다. 그래서 웹페이지를 방문할 때마다 해당 데이터를 새로 읽지 않고 캐싱하였다가 재사용하는 것이 도움이 됩니다. 웹브라우저는 해당 데이터를 사용자의 로컬 저장소에 저장해두고 해당 페이지 재방문시에 사용하게 됩니다.

 

CDN

이미지나 동영상 같은 큰 파일들은 cdn이라는 곳에 캐싱됩니다. 용량이 큰 파일을 요청했을 때 사용자와 원본 서버의 물리적인 거리가 멀다면 해당 파일은 네트워크를 따라 이동하게 되면서 파일 전송에 많은 시간이 소요됩니다. 이때 CDN을 이용하면 POP 서버라는 곳에 미리 해당 파일들을 옮겨 놓고 사용자가 파일을 요청했을 경우 가장 가까운 POP 서버에서 파일을 응답해 줌으로써 네트워크 지연 시간을 줄일수 있습니다.

 

Spring 캐싱

Spring Cache는 Spring에서 AOP로 제공하는 캐싱 추상화입니다. 메소드 레벨에서 작동하며, @Cacheable 주석으로 메소드에 주석을 달아 결과를 캐시해야 함을 나타낼 수 있습니다. 메서드를 처음 호출하면 결과가 계산되어 캐시에 저장됩니다. 그런 다음 동일한 입력 매개변수를 사용하는 후속 호출을 가로챌 수 있으며 메서드를 다시 실행하는 대신 캐시된 결과가 반환됩니다.

 

주의할 점은 위에도 언급하였지만, 동일한 인수로 동일한 메소드가 호출될 때 캐시된 결과를 반환함으로써 메소드의 성능을 향상을 목표로 하기에 입력이 같으면 결과가 같을때 적용이 가능합니다.

 

2. Spring 캐시 설정

Spring 캐시는 AOP로 구현되어있기에 캐싱 관련 코드와 비즈니스 로직을 분리할 수 있고, 스프링은 캐시 구현 기술에 종속되지 않도록 추상화된 서비스를 제공합니다.

캐시를 이용하기 위해 종속성을 추가합니다.

 

implementation("org.springframework.boot:spring-boot-starter-cache")

 

@EnableCaching 추가

 

Spring에서 @Cacheable과 같은 어노테이션 기반의 캐시 기능을 사용하기 위해서는 먼저 별도의 선언이 필요합니다. 그렇기 때문에 @EnableCaching 어노테이션을 설정 클래스에 추가해주어야 합니다.

 

@EnableCaching
@Configuration 
public class CacheConfig {

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

 

Spring 캐싱 추상화의 핵심 인터페이스는 'CacheManager' 인터페이스입니다. 주요 역할은 캐시된 데이터를 저장하는 컨테이너인 캐시를 생성하고 관리해줍니다. 이를 위해 캐시 매니저를 빈으로 선언해주는데 캐시 매니저는 캐시를 관리하고 캐싱 동작을 조정하는 역할을 담당합니다.

 

CacheManager Bean 등록 시 Spring에서 제공되는 캐시 매니저는 ConcurrentMapCacheManager, SimpleCacheManager, EhcacheCacheManager 등이 있습니다. 다른 캐시 매니저를 확인하시려면 아래 링크를 확인해주세요

 

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

 

Configuring the Cache Storage :: Spring Framework

Sometimes, when switching environments or doing testing, you might have cache declarations without having an actual backing cache configured. As this is an invalid configuration, an exception is thrown at runtime, since the caching infrastructure is unable

docs.spring.io

 

3. 캐싱 어노테이션 사용하기

캐싱 어노테이션에는 크게 @Cacheable, @CachePut, @CacheEvict 이 있습니다.

@Cacheable

캐시를 적용할 메소드에 @Cacheable 어노테이션을 붙여주면 캐시에 데이터가 없을 경우에는 기존의 로직을 실 행한 후에 캐시에 데이터를 추가하고, 캐시에 데이터가 있으면 캐시의 데이터를 반환합니다.

 

// 캐시 저장
@Cacheable("musicCacheStore")
public Music getMusicRanking(int ranking) {
  System.out.println("cacheable 실행 / ranking: " + ranking);
  ...
  return music;
}

public static void main(String[] args) {
    MusicService musicService = new MusicService();

    musicService.getMusicRanking(1);
    musicService.getMusicRanking(1);
    musicService.getMusicRanking(2);
}

input으로 랭킹에 해당하는 음악을 가져오기 위해 getMusicRanking() 을 선언해주고 위에 @Cacheable 을 붙여줬습니다. musicCacheStore는 위에 Config에서 설정한 캐시 저장소입니다.

 

실행결과

"cacheable 실행 / ranking: 1"
// "cacheable 실행 / ranking: 1" (출력 X)
"cacheable 실행 / ranking: 2"

musicService.getMusicRanking(1) 을 두번 호출하였는데도 "cacheable 실행 / ranking: 1" 출력 결과는 1번만 나오는 것을 알 수 있는데 과정을 살펴봅니다.

 

  1. getMusicRanking(1) 첫번째 호출 
    1. musicCacheStore 캐시(저장소)에 getMusicRanking(1)에 값이 있는지 확인
    2. musicCacheStore 값이 없음. 해당 로직을 실행
    3. 로직의 결과를 value로 저장
  2. getMusicRanking(1) 두번째 호출
    1. musicCacheStore 캐시(저장소)에 getMusicRanking(1)에 값이 있는지 확인
    2. musicCacheStore 값이 있음. 로직을 실행하지 않음. 캐시에 조회한 값을 그대로 반환
  3. getMusicRanking(2) 첫번째 호출
    1. 인자가 1 → 2로 변경
    2. musicCacheStore 캐시(저장소)에 getMusicRanking(2)에 값이 있는지 확인
    3. musicCacheStore 값이 없음. 해당 로직을 실행
    4. 로직의 결과를 value로 저장
// 캐시 저장 (Key 지정)
@Cacheable(value = "musicCacheStore", key = "#music.title")
public Music getMusicRanking(Music music) {
  System.out.println("getMusicRanking 키로 저장 실행");
  ...
  return music;
}

캐시 저장소에 키 값을 지정하고 싶을 경우가 있습니다. 이떄는 Key 값을 별도로 지정해주면 됩니다. Kev값의 지정에는 SpEL 문법이 사용됩니다. 그렇기 때문에 만약 파라미터가 객체라면 #{객체.필드} 로 접근하면 됩니다.

 

  1. getMusicRanking(music) 첫번째 호출 이번에는 music에 title이 “Hello” 라고 가정해보겠습니다.
    1. musicCacheStore 캐시(저장소)에 getMusicRanking()의 tilte에 “Hello” 값이 있는지 확인
    2. musicCacheStore 값이 없음. 해당 로직을 실행
    3. 로직의 결과를 value로 저장
  2. getMusicRanking(music) 두번째 호출
    1. musicCacheStore 캐시(저장소)에 getMusicRanking()의 tilte에 “Hello” 값이 있는지 확인
    2. musicCacheStore 값이 있음. 로직을 실행하지 않음. 캐시에 조회한 값을 그대로 반환
  3. getMusicRanking(music) 첫번째 호출
    1. musicCacheStore 캐시(저장소)에 getMusicRanking()의 tilte에 “Hi” 값이 있는지 확인
    2. musicCacheStore 값이 없음. 해당 로직을 실행
    3. 로직의 결과를 value로 저장
// 조건부 캐시 저장
@Cacheable(value = "musicCacheStore", key = "#music.title", condition = "#music.rank <= 10")
public Music getMusicRankingTop10(Music music) {
  System.out.println("getMusicRanking 실행");
  ...
  return music;
}

 

인자에 따라 메서드를 캐싱하도록 할 수 있으며, condition 속성을 통해 적용할 수 있습니다.

예를 들어 Music rank의 Top 10에 포함하는지 확인하기 위해 condition 속성을 적용하였습니다. 조건이 true로 평가되면 메서드 결과가 캐시되고 그렇지 않으면 캐시되지 않습니다.

 

@CachePut

두 번째 @CachePut 어노테이션은 저장을 위한 기능입니다.

@Cacheable과 유사하게 실행 결과를 캐시에 저장하지만, 조회 시에 저장된 캐시 내용을 사용하지는 않고 항상 메서드의 로직을 실행한다는 점에서 다릅니다.

메서드 실행에 영향을 주지 않고 캐시를 갱신해야 하는 경우에 사용됩니다.

 

@CacheEvict

캐싱된 데이터는 적절한 시점에 제거되어야 합니다. 만약 값이 업데이트가 되었는데 기존 캐싱된 데이터를 보여주면 메서드가 실행이 되지 않아 의도하지 않는 데이터를 보여줄 수 있습니다. 그래서 값이 업데이트가 되면, 캐시를 제거해줘야합니다

캐시를 제거해야하는 시점은 다음과 같습니다

  • 값이 변경되었을 때
    • 예를 들어, 고객이 장바구니에 상품을 담았는데 기존 캐싱된 데이터가 계속 남아있다면 잘못된 주문으로 이뤄질 수 있어 주문 상품을 변경을 해야합니다
  • 일정한 기간 주기로 값이 변경될 때
    • 매일 특정 시간동안 배치가 돌면서 변경되는 데이터일 때 사용한다. 예를 들어, 오늘의 음악을 보여줄 때 매일 자정마다 변경을 해주면 됩니다.

이 때 @CacheEvict는 저장된 캐시를 제거할 때 사용됩니다. @CacheEvict에 지워줄 캐시 이름을 넣어주면 해당 어노테이션이 있는 메소드가 실행될 때 캐시의 내용이 제거됩니다.

 

@CacheEvict (value = "musicCacheStore")
public void clearMusicCacheStore() { 
	System.out.println("clearMusicCacheStore 실행");
	...
}

musicCacheStore 로 저장된 캐시를 삭제합니다.

 

@CacheEvict (value = "musicCacheStore", key = "#music.title")
public void clearMusicCacheStore(Music music) {
	System.out.println("clearMusicCacheStore 실행");
}

@CacheEvict는 기본적으로 메소드의 키에 해당하는 캐시만 제거합니다. 만약 다음과 같은 메소드에 @CacheEvict를 적용하면 title와 같은 키값을 가진 캐시를 제거합니다.

 

@CacheEvict (value = "musicCacheStore", allEntires = true)
public void clearMusicCacheStore(Music music) {
	System.out.println("clearMusicCacheStore 실행");
}

 

캐시에 저장된 값을 모두 제거할 필요가 있다면 allEntries 속성을 true로 지정해주면 됩니다.

 

4. 정리

이번 글에서는 Spring Cache를 살펴보았습니다. 메소드의 성능을 향상을 목표로 캐싱을 적절하게 이용한다면 서비스 개선에 도움이 될 수 있지 않을 까 생각합니다.
감사합니다.

댓글