🙏내용에 대한 피드백은 언제나 환영입니다!!🙏
공지사항의 경우, 데이터의 변화가 적기 때문에 캐싱을 해두고 불러오는 과정을 적용하려 했다. 그러나 게시글을 페이징 처리하여 다루기 때문에, org.springframework.data.domain.PageImpl
을 Redis에 캐싱할 때 JSON 역직렬화 오류가 발생하였다.
오류 로그는 다음과 같이 나타났다.
Error occurred : Could not read JSON:Cannot construct
instance of org.springframework.data.domain.PageImpl
(no Creators, like default constructor, exist): cannot
deserialize from Object value
(no delegate- or property-based Creator)
이 오류는 PageImpl 클래스에 기본 생성자가 없어 Jackson이 역직렬화하지 못할 때 발생한다는 것이다. Page 객체는 기본 생성자가 없어서 역직렬화 시 Jackson이 올바르게 처리할 수 없게 되는 것이다.
Jackson은 리플렉션(Reflection)으로 동작하기 때문에, 역직렬화 할 시 기본 생성자가 필요하다. - 리플렉션과 관련된 내용과 왜 기본 생성자가 필요한지는 다음 포스팅에서 다뤄볼 예정이다. (링크 - DB에서 객체 할당 시, 기본 생성자 필요한 이유)
Redis에 Page 객체를 캐싱할 때, PageImpl 클래스가 역직렬화되지 않는다는 문제이다.
해결 방법은 커스텀 Page 클래스 RestPage를 만들어 JSON 역직렬화가 가능하도록 작성하는 것이다.
먼저, 커스텀 하기 위해서 알아둬야 할 개념이 있다.
@JsonCreator
: 역직렬화 하는 과정에서 해당 어노테이션이 있다면, 해당 어노테이션이 달린 생성자를 이용하여 역직렬화를 실행한다.
@JsonProperty
: JSON 필드를 생성자의 매개변수에 매핑하여 JSON 데이터를 해당 생성자에 전달한다.
RestPage는 PageImpl을 상속하고, Jackson에서 사용할 수 있도록 @JsonCreator를 추가하여 역직렬화가 가능하도록 했다.
@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 totalElements) {
super(content, PageRequest.of(page, size), totalElements);
}
public RestPage(Page<T> page) {
super(page.getContent(), page.getPageable(), page.getTotalElements());
}
public RestPage(List<T> content, Pageable pageable, Long total) {
super(content, pageable, total);
}
}
@JsonCreator
와 @JsonProperty
를 통해 기본 생성자가 필요한 Jackson이 해당 어노테이션이 달린 생성자를 이용하도록 설정하였다.
또한, @JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"})
어노테이션을 달아 pageble에 대한 JSON 역직렬화는 무시하도록 하였다. (pageble에 담기는 내용은 정렬 조건, offset 등 클라이언트에서 필요하지 않는 내용들이 담겨 있어 무시하였다.)
기존의 Page 반환 타입을 RestPage로 변경하여 Redis 캐싱 시 커스텀 페이지 클래스를 사용하도록 했다.
@Transactional(readOnly = true)
@Cacheable(value = "noticePosts", key = "#categoryName + ':' + #pageable.pageNumber", condition = "#categoryName.equals('notice')")
public RestPage<PostInfoDto> readPostsByCategory(String categoryName, Pageable pageable) {
Category findCategory = categoryRepository.findByName(categoryName)
.orElseThrow(() -> new CustomException(ErrorCode.CATEGORY_NOT_FOUND));
Page<Post> posts = postRepository.findByCategory(findCategory, pageable);
return new RestPage<>(posts.map(PostInfoDto::new));
}
Jackson에서는 리플랙션이 사용되기 때문에, 기본 생성자가 필요한 것은 당연하다. Restful API를 진행하는 과정에서, JSON을 역직렬화 하는 과정에서도 해당 DTO에 기본생성자가 없다면, 값을 제대로 입력받지 못한다.
이 외에도 예를 들어보자면, 로그인 과정에서 loadUserByUsername
캐싱을 하기 위해서는 SimpleGrantedAuthority
클래스에도 기본 생성자가 없기 때문에 Jackson이 이를 JSON으로부터 역직렬화를 위한 커스텀 클래스가 필요하다.
이를 통해서, 이미 구현되어 있는 라이브러리나 프레임워크의 코드를 깊이 읽어보는 것이 얼마나 중요한지 다시 한번 깨달았다.
단순히 기능을 사용하는 것에서 그치는 것이 아니라, 내부 동작 방식을 이해하고 필요한 부분을 커스터마이징할 수 있어야 문제를 빠르게 해결할 수 있다.
라이브러리의 기본 구조와 원리를 파악해 두면 예상치 못한 상황에서도 대응력이 높아진다는 점에서 코드 분석의 중요성을 다시 한번 느꼈다.