[Spring] PageImpl 역직렬화를 위한 Wrapper 클래스, PageImpl Redis 캐싱

2026. 4. 17. 21:20·Backend/Spring

1. 배경 및 목표

1) PageImpl Wrapper 클래스의 필요성

지금까지 사내 프로젝트에서 PageImpl을 상속받은 RestPageImpl 이라는 클래스를 사용해 페이징을 처리해왔습니다.

@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public RestPageImpl(@JsonProperty("content") List<T> content, ...)

 

개인 프로젝트때는 PageImpl로도 페이징을 처리했었는데 굳이 이런 클래스를 만들어 사용하는 이유가 궁금해져 알아보았습니다.

 

PageImpl은 직렬화(객체 -> JSON)은 가능하지만 역직렬화(JSON -> 객체)가 불가능합니다.

이를 해결하기 위해 Jackson이 역직렬화에 사용할 수 있는 생성자를 추가한 Wrapper클래스입니다.


결론적으로 
PageImpl의 Wrapper 클래스인 RestPageImpl은 페이징 객체를 역직렬화하기 위해 사용됩니다.

직렬화(Serialization): 메모리상의 객체를 바이트 스트림(연속적인 데이터, JSON 등) 형태로 변환

역직렬화(Deserialization): 바이트 데이터를 다시 객체로 복원



2) 역직렬화가 필요한 상황 - Redis Cache

모놀리식인 저희 프로젝트는 값을 주고받으며 가공할 일이 없었고, JSON을 다시 페이징 객체로 역직렬화할 상황이 뭐가 있을까 생각했습니다. 답은 캐싱이었습니다.

 

@Cacheable(cacheNames = CacheName.BOARD_LIST, keyGenerator = "readableKeyGenerator", cacheManager = "redisCacheManager")
public RestPageImpl<BoardListResponse> getBoardList(SearchBoard search, Pageable pageable) {
    return boardRepository.searchBoardPage(search, pageable);
}


Redis Cache는 데이터를 JSON 문자열로 저장합니다.

@Cacheable 이 붙은 메서드가 처음 호출되면 반환 값을 JSON으로 직렬화 해서 Redis에 저장하고, 이후 호출에서는 Redis에 저장된 JSON을 다시 객체로 역직렬화해서 반환합니다.


즉, Redis Cache를 사용하려면 직렬화뿐만 아니라 역직렬화도 가능해야 합니다.

PageImpl을 역직렬화하기 위한 개념들을 알아보고 구현해보겠습니다.



2. 개념

1) Jackson의 역직렬화 방식과 PageImpl의 문제

Java 환경에서는 Jackson을 통해서 객체를 직렬화/역직렬화 합니다.

Jackson의 기본 역직렬화 방식은 다음과 같습니다.

  1. 기본 생성자로 빈 객체 생성
  2. setter 또는 필드 주입으로 값을 채움

하지만 PageImpl에는 두 가지 문제가 있습니다.

public class PageImpl<T> extends Chunk<T> implements Page<T> {
    private static final long serialVersionUID = 867755909294344406L;
    private final long total;

    public PageImpl(List<T> content, Pageable pageable, long total) {
        super(content, pageable);
        this.total = (Long)pageable.toOptional().filter((it) -> {
            return !content.isEmpty();
        }).filter((it) -> {
            return it.getOffset() + (long)it.getPageSize() > total;
        }).map((it) -> {
            return it.getOffset() + (long)content.size();
        }).orElse(total);
    }

    public PageImpl(List<T> content) {
        this(content, Pageable.unpaged(), null == content ? 0L : (long)content.size());
    }

 

보면 알 수 있듯이

PageImpl은 기본 생성자가 없고, 매개변수로 들어가는 Pageable은 인터페이스라 Jackson은 어떤 구현체를 써야할 지 알 수 없습니다.

결과적으로 Jackson이 사용할 수 있는 생성자가 없어서 역직렬화가 실패합니다.

Jackson: Java환경에서 Json 데이터를 쉽게 처리하기 위한 파싱 및 직렬화 라이브러리



2) @JsonCreator

@JsonCreator는 Jackson에게 "역직렬화할 때 이 생성자를 사용해라" 라고 알려주는 어노테이션 입니다.

기본 생성자가 없어도 @JsonCreator가 붙은 생성자가 있으면 Jackson은 그 생성자를 역직렬화 진입점으로 사용합니다.


@JsonCreator에는 4가지 Mode가 있습니다.

@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface JsonCreator {
    Mode mode() default JsonCreator.Mode.DEFAULT;

    public static enum Mode {
        DEFAULT,
        DELEGATING,
        PROPERTIES,
        DISABLED;

        private Mode() {
        }
    }
}

 

각 Mode에 대해서 간단하게 알아보겠습니다.

PROPERTIES

  • JSON 필드를 생성자 파라미터에 이름으로 1:1 매핑
  • 파라미터마다 @JsonProperty로 JSON 키를 명시
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public User(@JsonProperty("name") String name,
            @JsonProperty("age") int age) { ... }

DELEGATING

  • JSON 전체를 파라미터 하나의 타입으로 통쨰로 넘김
  • 파라미터가 반드시 1개여야 함
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public User(Map<String, Object> data) { ... }

DEFAULT

  • Jackson이 임의로 판단
  • 파라미터가 여러개면 PROPERTIES
  • 파라미터가 1개면 Jackson이 파라미터명과 일치하는 JSON 필드가 있으면 PROPERTIES, 없으면 DELEGATING으로 판단
    -> PROPERTIES 혹은 DELEGATING을 명시하는 것이 안전

DISABLED

  • 해당 생성자를 역직렬화에 사용하지 말라는 뜻



3. 구현

이제 위의 개념들을 바탕으로 RestPageImpl을 구현해보겠습니다.

@JsonIgnoreProperties(ignoreUnknown = true)
public class RestPageImpl<T> extends PageImpl<T> {

    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
    public RestPageImpl(@JsonProperty("content") List<T> content,
                        @JsonProperty("number") int number,
                        @JsonProperty("size") int size,
                        @JsonProperty("totalElements") long totalElements) {

        super(content, PageRequest.of(number, size), totalElements);
    }
}

 

코드를 순서대로 살펴보겠습니다.


@JsonIgnoreProperties(ignoreUnknown = true)는 어노테이션을 통해 매핑되지 않는 필드를 무시할 수 있습니다.

역직렬화 시 생성자에 없는 필드가 있으면 Jackson이 오류를 낼 수 있는데, 이 어노테이션으로 매핑되지 않는 필드를 무시합니다.

 

@JsonProperty는 JSON 필드명과 파라미터를 매핑합니다.

public PageImpl(List<T> content, Pageable pageable, long total) {. . .}

PageImpl의 내부 필드명은 total이지만, 직렬화된 JSON에서는 getTotalElements()의 메서드명 기준으로 totalElements로 출력됩니다. 이에 "totalElements"의 이름으로 매핑합니다.


PageImpl 생성자가 요구하는 Pageable은 구현체인 PageRequest로 직접 생성해서 넘깁니다. 이렇게 하면 Jackson이 Pageable 인터페이스를 어떤 구현체로 만들어야 할지 몰라 실패하는 문제를 피할 수 있습니다.

'Backend > Spring' 카테고리의 다른 글

[Spring] 테스트 코드 도입기 - JUnit과 Mockito를 활용한 단위 테스트  (0) 2026.05.01
[Spring] 1 대 1 실시간 채팅 구현하기 - Stomp, MongoDB, Redis  (0) 2026.01.24
[Spring] Redis Sorted Set, ZSet 을 이용한 매칭 시스템 구현하기  (0) 2025.10.15
[Spring] 로컬, AWS LightSail에서 AWS parameter store로 환경변수 관리하기  (0) 2025.07.19
[Spring] Spring AOP의 동작원리, JDK Dynamic Proxy와 CGLIB  (1) 2025.04.28
'Backend/Spring' 카테고리의 다른 글
  • [Spring] 테스트 코드 도입기 - JUnit과 Mockito를 활용한 단위 테스트
  • [Spring] 1 대 1 실시간 채팅 구현하기 - Stomp, MongoDB, Redis
  • [Spring] Redis Sorted Set, ZSet 을 이용한 매칭 시스템 구현하기
  • [Spring] 로컬, AWS LightSail에서 AWS parameter store로 환경변수 관리하기
단군왕건영
단군왕건영
널리 세상을 이롭게 하고 싶은 개발자
  • 단군왕건영
    홍익인간 개발자
    단군왕건영
  • 전체
    오늘
    어제
    • 분류 전체보기 (90)
      • TroubleShooting (16)
      • Backend (13)
        • Java (2)
        • Spring (9)
        • JPA (2)
      • DB (1)
      • Algorithm (7)
        • 백준 (4)
      • Infra (3)
      • CS (40)
        • 컴퓨터구조 (25)
        • 네트워크 (12)
        • 운영체제 (3)
      • Git (3)
      • Mac (2)
      • 회고 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    컴퓨터구조
    java
    springboot
    Jenkins
    백준
    네트워크
    MariaDB
    컴퓨터 구조
    spring
    docker
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
단군왕건영
[Spring] PageImpl 역직렬화를 위한 Wrapper 클래스, PageImpl Redis 캐싱
상단으로

티스토리툴바