ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • MSA환경에서 캐시 역직렬화 문제 해결하기 | Jackson2JsonRedisSerializer 직렬화 방식 사용하기
    Project 2024. 9. 7. 13:42

    1. 이전의 직렬화 방식 

     

     기존 모놀리식의 시스템을 MSA로 전환하면서 역직렬화 문제가 발생하게 되었다. 이전에 설정했던 캐시 직렬화 방식은 디폴트 값인 JdkSerializationRedisSerializer였다. 객체를 바이트 스트림으로 변환할때 자바 객체 데이터와 해당 클래스와 관련된 메타데이터들까지 한번에 저장하기 때문에 패키지 구조가 변경되거나(분리된 환경이거나) 클래스 안의 타입들이 자주 변경된다면 지양해야한다. 메타데이터들도 함께 저장되기 때문에 용량이 크다는 것 또한 염두해 둬야한다. 

     

    현재 유저의 unique인 email을 키로 권한정보가 들어있는 객체를 밸류로 캐싱하고 있었는데, MSA 적용으로 기존의 시스템에서 유저가 분리되게 되었다. 따라서 분리된 유저 서버에서 해당 캐시를 읽어오면서 역직렬화할때 패키지의 구조가 달라졌기 때문에 다음과 같은 오류가 발생하게 되었다. 

     

    org.springframework.data.redis.serializer.SerializationException: Cannot deserialize
    	at org.springframework.data.redis.serializer.JdkSerializationRedisSerializer.deserialize(JdkSerializationRedisSerializer.java:108) ~[spring-data-redis-3.3.2.jar:3.3.2]

     

     

    2. json 타입의 직렬화 방식 사용하기 

     

     현재 key는 단순 문자열이고 value는 객체라서 json타입으로 저장하고 싶었다. 따라서 json타입으로 저장하는 GenericJackson2JsonRedisSerializer를 적용하려 했지만 해당 방식은 객체의 class 정보를 저장한다. serialVersionUID를 명시적으로 정의하는 등으로 역직렬화 문제를 피하는 방식은 있었으나 클래스 정보를 저장해서 매핑한다는 방식이 MSA환경에서는 적절하지 않다고 생각했다.

    아래처럼 클래스 타입과 일치하는 경로에서 찾지 못해 실패한다.

    org.springframework.data.redis.serializer.SerializationException: Could not read JSON:Failed to parse type 'com.baedalping.delivery.domain.user.dto.response.UserAuthorityResponseDto' (remaining: ''): Cannot locate class 'com.baedalping.delivery.domain.user.dto.response.UserAuthorityResponseDto', problem: com.baedalping.delivery.domain.user.dto.response.UserAuthorityResponseDto at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:307) ~[spring-data-redis-3.3.2.jar:3.3.2]

     

     

     

    3.  메타데이터를 저장하지 않는 Jackson2JsonRedisSerializer 직렬화 방식 사용하기

     

    따라서 객체 데이터만 json 방식으로 저장되게끔 캐시매니저의 value 직렬화 방식을 Jackson2JsonRedisSerializer로 설정해주었다. 

    package com.baedalping.delivery.global.config;
    
    import com.baedalping.delivery.domain.user.dto.response.UserAuthorityResponseDto;
    import java.time.Duration;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.cache.CacheKeyPrefix;
    import org.springframework.data.redis.cache.RedisCacheConfiguration;
    import org.springframework.data.redis.cache.RedisCacheManager;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.GenericToStringSerializer;
    import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializationContext;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    @Configuration
    @EnableCaching// 캐시 사용 설정 
    public class RedisConfig {
    
      @Bean
      public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        Jackson2JsonRedisSerializer<UserAuthorityResponseDto> jsonRedisSerializer =
            new Jackson2JsonRedisSerializer(UserAuthorityResponseDto.class);// 명시적으로 저장할 객체 타입을 지정
        
        RedisCacheConfiguration configuration =
            RedisCacheConfiguration.defaultCacheConfig()
                .disableCachingNullValues()
                .entryTtl(Duration.ofHours(1))// 설정한 TTL 시간에 따라 변경하세요
                .computePrefixWith(CacheKeyPrefix.simple())
                .serializeValuesWith(// value의 직렬화 방식을 지정
                    RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer));
            ;
    
        return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(configuration).build();
      }
    }

     

     

    다음과 같이 저장시 key는 roleCache::이메일, 값은 json형식으로 저장되는 것을 확인할 수 있다. 

    key의 직렬화 방식은 따로 지정하지 않아도 RedisCacheManager에서는 디폴트로 StringRedisSerializer를 사용하여 키를 string으로 직렬화 한다. 

     

    오더 서버에 있는 캐시 저장 메소드와 해당 캐시를 사용하는 게이트웨이 서버의 파싱 메소드를 둘다 적어놓을테니 참고하시기 바란다.

     

    @Component
    @Slf4j(topic = "RedisService")
    public class RedisComponent {
      @Cacheable(cacheNames = "roleCache", key = "args[0]")// 매개변수의 첫번째 값인 email을 키로 캐시 이름을 roleCache로 지정
      public UserAuthorityResponseDto setUserAuthorityInRedis(String email, String role) {
        return new UserAuthorityResponseDto(email, role);
      }
    }

     

    @Component
    @Slf4j(topic = "RedisService")
    public class RedisComponent {
    
      private final CacheManager cacheManager;
    
      public RedisComponent(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
      }
    
      public Optional<UserAuthorityResponseDto> getUserAuthorityFromRedis(String email) {
        Cache cache = cacheManager.getCache("roleCache");
        if (cache != null) {
          Cache.ValueWrapper valueWrapper = cache.get(email);
          if (valueWrapper != null) {
            UserAuthorityResponseDto cachedValue = (UserAuthorityResponseDto) valueWrapper.get();// 해당 타입으로 형변환
            log.info("redis role: {}", cachedValue.getRole());
            return Optional.of(cachedValue);
          }
        }
        return Optional.empty();
      }
    }

     

     

    중요한 점은 내가 작업한 프로젝트의 정책은 서버별로 캐시를 따로 쓴다는 것이다. 해당 캐시는 인증 관련 용도로만 사용하는 캐시고, 유저의 권한만 저장하는 것이기 때문에 특정 클래스를 명시하는 Jackson2JsonRedisSerializer 직렬화 방식을 사용했다.

     

    만약 다른 타입의 객체도 함께 저장하는 경우라면 value에도 문자열로 저장하는 StringRedisSerializer 방식을 쓰고 타입따라 인코딩과 디코딩을 따로 해주는 코드를 작성해야한다. 관련 내용으로 다른 분이 블로그에 작성해놓은 것이 있어 링크를 첨부한다.  

Designed by Tistory.