1. 문제 상황
- 사용 기술 스택 : Spring Boot, Spring Data JPA, Redis, Spring Cache
- 현상 : 특정 API에서 페이징된 엔티티 목록을 스프링 캐시에 저장했더니, 1차로 조회할 때는 DB에서 가져오므로 성공했지만 이후 캐시에서 데이터를 가져오면서InvalidDefinitionException 또는 Cannot construct instance of PageImpl 오류가 발생했습니다.
2025-02-02T23:28:37.189+09:00 ERROR 38011 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON:Invalid type definition for type com.example.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: ...
- 영향 : 해당 캐시된 API를 호출할 때마다 오류가 발생하며 애플리케이션이 정상 응답을 주지 못했습니다.
2. 원인 분석
Spring Data JPA의 페이징 결과는 실제로 PageImpl<T>라는 구현체입니다. Jackson이 Redis에서 JSON을 역직렬화할 때, PageImpl에 기본 생성자 혹은 @JsonCreator가 없어 객체를 만들 수 없으므로 직렬화는 되지만 역직렬화가 실패하여 오류가 발생했습니다.
간단히 말해 Spring Data JPA에서 Page<T>를 반환할 때, 내부적으로는 PageImpl<T> 클래스를 사용합니다. Jackson은 객체 -> JSON(직렬화)은 어떻게든 하겠지만 JSON -> 객체(역직렬화) 시 생성자를 찾지 못해 에러를 내게 됩니다.
캐시를 안 쓰고 Spring Data JPA만 썼을 때는 페이징을 해도 직렬화/역직렬화 신경 안 써도 되던데, 왜 그러지?
캐시를 안 쓸 때는 보통 Page<T>를 DB에서 조회하고 바로 컨트롤러에서 HTTP 응답(JSON)으로 내보냅니다. 이때 Jackson은 객체 -> JSON(직렬화)만 수행하고, JSON -> 객체(역직렬화)를 할 필요가 없습니다.
캐시를 쓸 때는 Page<T> 객체를 객체 -> JSON으로 Redis에 저장(직렬화)하고 나중에 JSON -> 객체(역직렬화)로 다시 꺼내 서비스 내부에서 재활용 해야 합니다. 이 과정에서 PageImpl<T> 생성자를 못 찾아 Jackson이 "객체를 어떻게 만들지?" 몰라하면서 에러가 납니다.
쉽게 결론을 말하면 내 메서드 반환 타입이 Page이면 캐시에서도 Page로 받아와야 메서드 호출부에서 동일하게 다룰 수 있습니다. 단순히 역직렬화를 하기 싫어서 JSON으로만 리턴하면, 메서드가 사용되는 곳(다른 서비스 로직, 컨트롤러)에서 JSON 파싱을 따로 해야합니다.
3. 해결
Page<T> 대신 DTO(PageResponse<T>) 사용하여 Page<T>를 직접 캐시에 저장하지 않고 필요한 필드만 담은 DTO를 만들어 저장했습니다.
서비스 로직에서 Page<T>를 조회한 뒤 DTO로 변환하여 캐시에 저장하니 Jackson은 DTO를 문제 없이 직렬화/역직렬화 할 수 있었습니다.
왜냐하면 DTO에는 기본 생성자나 @JsonProperty 등을 자유롭게 추가해줄 수 있기 때문입니다.
그럼 여기까지 글을 읽으셨으면 궁금증이 하나 더 생길 수 있다고 생각합니다.
근데 왜 Page<T>는 안되고 DTO는 되는거야? 왜 오류가 나는지는 이해했는데 어떤 식으로 역직렬화가 되는거지??
핵심은 Jackson이 JSON을 다시 객체로 만들 때 생성자를 찾거나 @JsonCreator가 필요 하다는 사실입니다.
1. PageImpl<T>는 라이브러리 클래스라 기본 생성자나 @JsonCreator가 없습니다
- 직렬화(객체 -> JSON)는 getter로 필드를 뽑아내는 거니까 문제가 없지만
- 역직렬화(JSON -> 객체) 할 때는 "객체를 어떻게 new 할지" Jackson이 알 수 없습니다.
- 그래서 캐시에 JSON이 들어가도 다시 객체로 만들기가 불가능 하므로 오류를 발생 시킵니다.
2. DTO(PageResponse<T>) 는 직접 만든 클래스 이므로 기본 생성자를 추가하고 @JsonProperty를 통해 "Json에서 필드를 어떻게 매핑"할지 명시할 수 있습니다. 그래서 Jackson이 "아! PageResponse는 이렇게 생성해서 필드를 넣으면 되겠구나" 하고 쉽게 역직렬화를 할 수 있습니다.(@JsonProperty와 Setter가 없어도 Jackson이 기본 생성자가 있는 DTO를 reflection 방식으로 필드에 값을 채워 줍니다)
즉 PageImpl<T>는 역직렬화 로직이 닫혀 있고, PageResponse<T>는 역직렬화 로직을 열어둘 수 있기 때문에 직접 만든 DTO는 "객체 -> JSON -> 객체” 양방향이 자유로울 수 있습니다.


4. 느낀 점
- 직렬화 대상이 되는 클래스는 기본 생성자, @JsonCreator, @JsonProperty 등 Jackson 호환성을 고려해야 함
- Spring PageImpl처럼 라이브러리 클래스는 내부 구조가 역직렬화에 친화적이지 않을 수 있음
- 가능하면 DTO를 통해 필요한 데이터만 캐시에 저장하는 게 더 안정적이고, 구조적 호환성 문제도 줄어든다
5. 결과

1차 조회 때는 캐시미스 상태라 레디스에 갔다가 DB에서 갖고 오느라 1Page에 30개의 데이터를 가지고 오는데 381ms가 걸렸습니다.

2차 조회 때는 캐시 히트 상태라 바로 캐시에서 가져오므로 약 19배 정도 속도가 향상되었습니다.
'Back-End > Redis' 카테고리의 다른 글
| JMeter를 활용하여 Redis 캐시 테스트 분석 (0) | 2025.02.05 |
|---|---|
| Redis 캐싱 전략(CacheAside, Write Around)과 한계 (0) | 2025.01.29 |
| Redis란?(feat. 기본 명령어와 Key 네이밍 컨벤션) (0) | 2025.01.29 |