spring / / 2023. 9. 27. 13:46

Spring Boot cache에서 ConcurrentMapCache를 값으로 저장

Spring boot의 기본 캐쉬 구현체인 ConcurrentMapCache를 사용할 때 캐쉬의 값이 임의로 변경되는 경우가 있다.

예를 들면 특정 객체를 캐쉬에서 조회한 다음 값을 setter를 통해 변경을 했는데, 캐쉬의 값이 변경되는 문제이다.

아래의 예를 한번 보자.

예제 코드

서비스 코드에서 아래의 로직이 있다.

@Cacheable(value = "user", key = "#userId")
public User find(int userId) {
    return userMap.get(userId);
}

여기서 사용자를 조회한 다음, 이름을 setter를 통해 변경을 하고 다시 조회를 하는 테스트 코드를 작성해보자.

@Test
void storeByValueTest() {
    // user 조회
    User user = userService.find(1);
    System.out.println("user = " + user);
    user.setName("홍길동_수정"); // 이름 변경

    // 객체 변경 후 user 조회
    User user2 = userService.find(1);
    System.out.println("user2 = " + user2);
}

실행해보면 아래와 같은 결과가 표시된다.

user = User(userId=1, name=홍길동)
user2 = User(userId=1, name=홍길동_수정)

캐쉬의 값을 변경하지 않았는데, 2번째 조회 시 캐쉬의 값이 변경되는 현상이 있다.

찾아보니 ConcurrentMapCache은 기본적으로 캐쉬 저장 시 실제 객체가 아닌 참조값을 저장하는 특성이 있다.
그래서 캐쉬에 객체를 저장할 때 객체를 직렬화(serializable)를 하지 않아도 된다.

해결방법은 객체를 수정하기 전에 새로운 객체(new)로 생성 후 변경을 해도 되지만, 더 좋은 방법은 캐쉬 저장 시 참조값이 아닌 실제 값을 저장하는 방법으로 바꾸는 방법이다.

cacheManager에 보면 setStoreByValue라는 것이 있는데 기본적으로 false이여서 참조값이 저장되게 되고, 이를 true로 변경하면 값이 저장된다.

ConcurrentMapCacheManager#setStoreByValue 소스코드 내에 아래 내용이 적혀있다.

Specify whether this cache manager stores a copy of each entry (true or the reference (false for all of its caches.
Default is "false" so that the value itself is stored and no serializable contract is required on cached values.
Note: A change of the store-by-value setting will reset all existing caches, if any, to reconfigure them with the new store-by-value requirement.

만일 setStoreValue를 true로 사용할 경우 User 객체를 직렬화(Serializable) 해줘야 한다. (implements Serializable)

cacheManager의 Bean을 새로 생성하여 구현해보자.

@Configuration
@EnableCaching
public class CachingConfig implements BeanClassLoaderAware {

    private ClassLoader classLoader;

    @Bean
    public CacheManager cacheManager() {
        ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
        cacheManager.setStoreByValue(true);
        cacheManager.setBeanClassLoader(classLoader);

        return new TransactionAwareCacheManagerProxy(cacheManager);
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }
}

위에서 TransactionAwareCacheManagerProxy는 캐쉬를 트랜잭션 내에서 처리되도록 하기 위한 것이다.

다시 실행해보면 정상적으로 잘 실행되는 것을 확인할 수 있다.

user = User(userId=1, name=홍길동)
user2 = User(userId=1, name=홍길동)
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유