두둥.. 에러가 발생했습니다. 발생한 이유는 org.springframework.data.domain.PageImpl 객체를 JSON으로 직렬화하거나 역직렬화하는 과정에서 발생하는 문제였습니다. 특히, Spring에서 캐싱된 Page 객체를 다시 사용할 때 나타나는 문제로, PageImpl 클래스가 기본 생성자나 적절한 생성자를 제공하지 않아 Jackson이 객체를 역직렬화할 수 없기 때문이었습니다.
Page 인터페이스를 구현한 PageImpl 클래스는 기본 생성자가 없으며, Jackson이나 다른 JSON 라이브러리가 객체를 역직렬화할 때 기본 생성자가 필요했고, 이 경우 캐시에서 꺼낸 데이터가 역직렬화되지 않으면서 예외가 발생한 것이었습니다. PageImpl의 기본 생성자도 신경써줘야 하다니 !! 문제를 해결하기 위해서, 두 가지 방법을 찾았습니다. 하나는 Page 객체가 아닌 DTO로 변환하여 캐시 데이터를 저장하는 것. 나머지는 PageImpl 객체를 JSON으로 직렬화/역직렬화할 수 있도록 커스텀 설정을 추가하는 방법이 있었고, 기존에 제공하고 있는 API를 변경할 수가 없었기 때문에 후자를 선택했습니다.
@JsonIgnoreProperties 어노테이션은 com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "empty” 와 같은 에러를 해결하기 위해 작성하였습니다.
자세한 내용은 아래 글을 확인해보세요!
CustomPageImpl
@JsonIgnoreProperties(ignoreUnknown = true)
public class CustomPageImpl<T> extends PageImpl<T> {
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public CustomPageImpl(@JsonProperty("content") List<T> content, @JsonProperty("number") int number, @JsonProperty("size") int size,
@JsonProperty("totalElements") Long totalElements, @JsonProperty("pageable") JsonNode pageable, @JsonProperty("last") boolean last,
@JsonProperty("totalPages") int totalPages, @JsonProperty("sort") JsonNode sort, @JsonProperty("first") boolean first,
@JsonProperty("numberOfElements") int numberOfElements) {
super(content, PageRequest.of(number, size), totalElements);
}
public CustomPageImpl(List<T> content, Pageable pageable, Long total) {
super(content, pageable, total);
}
public CustomPageImpl(List<T> content) {
super(content);
}
public CustomPageImpl() {
super(new ArrayList<T>());
}
}
SearchRepositoryImpl
public class SearchRepositoryImpl implements SearchRepositoryCustom {
private final JPAQueryFactory queryFactory;
public SearchRepositoryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
@Override
public Page<Instance> search(Progress progressCond, String titleCond, Pageable pageable) {
BooleanBuilder builder = new BooleanBuilder();
if (progressCond != null) {
builder.and(instance.progress.eq(progressCond));
}
if (titleCond != null) {
builder.and(instance.title.contains(titleCond));
}
List<Instance> content = queryFactory
.selectFrom(instance)
.where(builder)
.orderBy(instance.startedDate.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
JPAQuery<Long> countQuery = queryFactory
.select(instance.count())
.from(instance)
.where(builder);
return new CustomPageImpl<>(content, pageable, countQuery.fetchOne());
}
}
새로 적용한 CustomPageImpl에서 직렬화, 역직렬화가 정상적으로 작동되는 지 아래와 같이 테스트해보았습니다.
public class CustomPageImplTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
public void testCustomPageImplSerialization() throws JsonProcessingException {
List<String> data = List.of("item1", "item2", "item3");
PageRequest pageRequest = PageRequest.of(0, 10);
CustomPageImpl<String> customPage = new CustomPageImpl<>(data, pageRequest, 3L);
// CustomPageImpl 객체를 JSON으로 직렬화
String json = objectMapper.writeValueAsString(customPage);
System.out.println("Serialized JSON: " + json);
// JSON 문자열을 다시 CustomPageImpl 객체로 역직렬화
CustomPageImpl deserializedPage = objectMapper.readValue(json, CustomPageImpl.class);
assertNotNull(deserializedPage);
assertEquals(customPage.getContent(), deserializedPage.getContent());
assertEquals(customPage.getTotalElements(), deserializedPage.getTotalElements());
assertEquals(customPage.getPageable().getPageNumber(), deserializedPage.getPageable().getPageNumber());
}
}
성공 ! 레디스 캐시를 적용하면서 많은 이슈들을 마주하고 있는데요. 참 재밌습니다 하하하하 !
참고 자료
- https://stackoverflow.com/questions/55965523/error-during-deserialization-of-pageimpl-cannot-construct-instance-of-org-spr
- https://stackoverflow.com/questions/52490399/spring-boot-page-deserialization-pageimpl-no-constructor