Spring @Cacheable Hateoas 관련 오류 해결
개발 환경
-
Spring boot 2.6.6
-
Redis
-
hateoas 2.6.4
구현 코드 및 오류
Redis Cache 설정
@Configuration
@EnableCaching
public class RedisConfig {
private final RedisProperties redisProperties;
@Bean(name = "connectionFactory")
@Profile({"local", "test"})
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
@Bean(name = "cacheManager")
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager.RedisCacheManagerBuilder builder= RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(connectionFactory);
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.entryTtl(Duration.ofDays(30))
.computePrefixWith(CacheKeyPrefix.simple())
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
return builder.cacheDefaults(configuration).build();
}
}
1. Hateoas 관련 오류
아래와 같이 org.springframework.hateoas의 PagedModel 또는 RepresentationModel를 상속받은 객체를 cache하게 되면 읽을 때 deserialize 오류가 발생하게 된다.
- 서비스 코드
@Cacheable(cacheNames = "CACHE_DELIVERY_NOTICE",
cacheManager = "cacheManager",
key = "T(String).valueOf(#params.btnSearch)+#params.viewType+#pageable.pageNumber",
condition="T(org.springframework.util.StringUtils).hasText(#params.searchText) == false",
unless="#result == null")
@Transactional(readOnly = true)
public PagedModel<NoticeModel> findAllByParams(NoticeSearchParams params, Pageable pageable) {
return this.noticeModelAssembler.toPageModel(noticeRepository.findAllByParams(params, pageable));
}
- 오류 메시지
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Could not resolve type id 'org.springframework.hateoas.Links' as a subtype of `java.util.List<org.springframework.hateoas.Link>`: Not a subtype
...
at [Source: (byte[])"{"@class":"org.springframework.hateoas.PagedModel","links":["org.springframework.hateoas.Links",[{"@class":"org.springframework.hateoas.Link","rel":["org.springframework.hateoas.StringLinkRelation","self"],"href"
...
at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:156)
-
원인
GenericJackson2JsonRedisSerializer가 org.springframework.hateoas.Links type을 deserialize 하지 못하여 발생하는 오류.
-
해결
서비스 코드에서 hateoas 관련 Type을 제거하고 일반 객체로 리턴한 후 Controller에서 Convert하여 Response하도록 변경.
(최종 해결 소스는 마지막에..)
2. PageImpl 오류
Hateoas 관련 오류를 해결하기 위해 아래와 같이 Page<T> 객체로 리턴하도록 만들었지만, PageImpl 관련 오류가 또 발생하였습니다.
- 서비스 코드
@Cacheable(cacheNames = "CACHE_DELIVERY_NOTICE",
cacheManager = "cacheManager",
key = "T(String).valueOf(#params.btnSearch)+#params.viewType+#pageable.pageNumber",
condition="T(org.springframework.util.StringUtils).hasText(#params.searchText) == false",
unless="#result == null")
@Transactional(readOnly = true)
public Page<NoticeResponse> findAllByParams(NoticeSearchParams params, Pageable pageable) {
return noticeRepository.findAllByParams(params, pageable);
}
- 오류 메시지
nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.data.domain.PageImpl`
- 원인
org.springframework.data.domain.PageImpl에 기본생성자가 존재하지 않아 발생.
- 해결
PageImpl를 Wrapper 객체를 만들어 사용.
@JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"})
public class RestPage<T> extends PageImpl<T> {
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public RestPage(@JsonProperty("content") List<T> content,
@JsonProperty("number") int page,
@JsonProperty("size") int size,
@JsonProperty("totalElements") long total) {
super(content, PageRequest.of(page, size), total);
}
public RestPage(Page<T> page) {
super(page.getContent(), page.getPageable(), page.getTotalElements());
}
}
@Cacheable(cacheNames = "CACHE_DELIVERY_NOTICE",
cacheManager = "cacheManager",
key = "T(String).valueOf(#params.btnSearch)+#params.viewType+#pageable.pageNumber",
condition="T(org.springframework.util.StringUtils).hasText(#params.searchText) == false",
unless="#result == null")
@Transactional(readOnly = true)
public Page<NoticeResponse> findAllByParams(NoticeSearchParams params, Pageable pageable) {
return new RestPage<>(noticeRepository.findAllByParams(params, pageable));
}
최종 해결 소스
- 서비스
@Cacheable(cacheNames = "CACHE_DELIVERY_NOTICE",
cacheManager = "cacheManager",
key = "T(String).valueOf(#params.btnSearch)+#params.viewType+#pageable.pageNumber",
condition="T(org.springframework.util.StringUtils).hasText(#params.searchText) == false",
unless="#result == null")
@Transactional(readOnly = true)
public Page<NoticeResponse> findAllByParams(NoticeSearchParams params, Pageable pageable) {
return new RestPage<>(noticeRepository.findAllByParams(params, pageable));
}
- 컨트롤러
@GetMapping("/query")
public ResponseEntity<PagedModel<NoticeModel>> findAllByParams(@Valid NoticeSearchParams params,
Pageable pageable, Errors errors) {
if (errors.hasErrors()) {
throw new InvalidParameterException("입력 값이 올바르지 않습니다.", errors);
}
return ResponseEntity.ok()
.body(this.noticeModelAssembler.toPageModel(this.noticeService.findAllByParams(params, pageable)));
}
- Page Wrapper
@JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"})
public class RestPage<T> extends PageImpl<T> {
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public RestPage(@JsonProperty("content") List<T> content,
@JsonProperty("number") int page,
@JsonProperty("size") int size,
@JsonProperty("totalElements") long total) {
super(content, PageRequest.of(page, size), total);
}
public RestPage(Page<T> page) {
super(page.getContent(), page.getPageable(), page.getTotalElements());
}
}
- ModelAssembler
@Component
@RequiredArgsConstructor
public class NoticeModelAssembler implements RepresentationModelAssembler<NoticeModel, NoticeModel> {
private final PagedResourcesAssembler<NoticeModel> pagedResourcesAssembler;
@Override
public NoticeModel toModel(NoticeModel model) {
return model
.add(getSelfLink(model.getNoticeSeq()))
.add(getUpdateLink(entity.getNoticeSeq()))
.add(getDeleteLink(entity.getNoticeSeq()));
}
@Override
public CollectionModel<NoticeModel> toCollectionModel(Iterable<? extends NoticeModel> entities) {
return RepresentationModelAssembler.super.toCollectionModel(entities);
}
public PagedModel<NoticeModel> toPageModel(Page<NoticeResponse> entities) {
Page<NoticeModel> models = entities
.map(NoticeResponse::toModel);
return pagedResourcesAssembler.toModel(models, this);
}
...
}
댓글남기기