-
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 방식을 쓰고 타입따라 인코딩과 디코딩을 따로 해주는 코드를 작성해야한다. 관련 내용으로 다른 분이 블로그에 작성해놓은 것이 있어 링크를 첨부한다.
'Project' 카테고리의 다른 글
토큰 인증과 권한체크를 Spring Cloud Gateway로 분리하기 (3) 2024.09.07 팀 프로젝트 시작하기 | 01. 프로젝트 문서 작성하기 (4) 2024.09.06 프로젝트 실행 시 환경변수 안전하게 주입하기 | AWS Secrets Manager로 환경변수 관리하기 (0) 2024.08.02 Github Actions에서 발생하는 detached HEAD 문제 해결하기 (0) 2023.12.09 [배포자동화구축] 2-2. Github Repository와 배포서버, 운영서버를 연동하기 (0) 2023.11.15