Project/대용량 트래픽 프로젝트

[프로젝트] QueryDSL 사용 시 페이징 응답 JSON 데이터 최적화 하기

쉬지마 이굥진 2024. 12. 11. 05:39

문제 상황

spring 3.3.4 버전, QueryDSL로 검색 기능을 구현하다, API 테스트 과정에서 처음 보는 WARN 로그를 발견했다.

2024-10-06T20:38:53.327+09:00  WARN 36852 --- [user-service] [io-19093-exec-1] ration$PageModule$WarningLoggingModifier : Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!
For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO))
or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.

요렇게 생겼다

 

원인 분석

이 처음 보는 이 WARN 로그가 뭔고 하니 ~ Spring Data 직렬화 경고였다.

로그 메세지를 풀어보면 Spring Data의 PageImpl 인스턴스를 직접 JSON으로 직렬화 할 때 불안정한 JSON 구조가 생성될 수 있음을 나타내는 말이었다. 검색 결과를 페이징 처리하면서 PageImpl 인스턴스를 가져다 썼는데, 이 부분 때문에 발생한 것 같다.

 

더 파보니 Page 인터페이스의 직렬화 문제 때문에 새로 들어온 스펙이라고 한다. (일관된 결과를 위해 PagedModel을 사용하는 것이 권장된다는 말)

 

로그 주요 내용

경고 유형: 이 경고는 애플리케이션이 PageImpl 객체를 직접 JSON으로 직렬화하려 할 때 생성됩니다.
안정성 문제: PageImpl 직렬화의 결과로 생성된 JSON 구조는 안정성이 보장되지 않으며, 기본 코드의 변경이 API 소비자에게 영향을 미칠 수 있습니다.

경고 로그 밑에 보면 대놓고 please use Spring Data's PagedModel => PagedModel 쓰셈 이라고 친절히 명시되어 있다.

 

그래서 PagedModel이 뭐지?

▪️ PagedModel
Spring HATEOAS 3.3부터 도입된 PagedModel은 RESTful 웹 서비스에서 페이징 처리된 데이터를 표현하는 표준 모델이다. 페이징된 데이터를 한눈에 확인할 수 있는 메타데이터를 포함한다.

 

당장 기능이 돌아가는 것에는 문제 없으나, 권장사항을 따라보고 어떻게 JSON 구조가 바뀌는지 궁금해서 리팩토링을 진행해봤다.

 

해결 방법

1. PagedModel 사용

기존의 Page 인스턴스를 PagedModel로 감싸서 안정적인 JSON 구조를 보장하는 방법

// 예시 코드
@GetMapping("/page")
PagedModel<?> page(Pageable pageable) {
    return new PagedModel<>(repository.findAll(pageable));
}

 

2. 전역 페이지 직렬화 활성화 

설정 구성 클래스(Config  클래스)에 아래 코드를 추가하는 방법

필자의 경우 QueryDSL을 설정하는 QueryDslConfig 클래스에 해당 코드를 붙여주었다. (pageSerializationMode = VIA_DTO)

@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
class QueryDslConfig{
    // 구성 코드
}

 

해결

알아보니 1번 방법은 이것저것 추가해줘야 할 것들이 있어, 비교적 더 간단한 2번으로 결정해 진행했다.

 

주의사항

  • VIA_DTO는 EnableSpringDataWebSupport.PageSerializationMode의 상수이므로, 이를 사용하기 위해 EnableSpringDataWebSupport 클래스를 import 할 때, VIA_DTO를 명시적으로 사용해야 한다.
  • VIA_DTO를 사용하기 위해서는 Spring Data의 버전이 지원하는지 확인해야한다. (최신 버전에서 지원됨)

코드 적용도 마쳤으니 결과를 보자.

 

결과 및 적용 전/후 비교

두 개의 결과는 모두 똑같은 검색 조건으로 검색했을 때의 결과이다.

적용 전

{
    "status": "OK",
    "message": "유저 검색 성공",
    "data": {
        "content": [
            {
                "userId": null,
                "username": "user1111",
                "nickname": "jinzza",
                "email": "jzza@naver.com",
                "role": "USER"
            },
            {
                "userId": null,
                "username": "user2222",
                "nickname": "jinzzay",
                "email": "jzzaa@naver.com",
                "role": "USER"
            }
        ],
        "pageable": {
            "pageNumber": 0,
            "pageSize": 10,
            "sort": [],
            "offset": 0,
            "paged": true,
            "unpaged": false
        },
        "last": true,
        "totalElements": 2,
        "totalPages": 1,
        "size": 10,
        "number": 0,
        "sort": [],
        "first": true,
        "numberOfElements": 2,
        "empty": false
    }
}

 

적용 후

{
    "status": "OK",
    "message": "유저 검색 성공",
    "data": {
        "content": [
            {
                "userId": null,
                "username": "user1111",
                "nickname": "jinzza",
                "email": "jzza@naver.com",
                "role": "USER"
            },
            {
                "userId": null,
                "username": "user2222",
                "nickname": "jinzzay",
                "email": "jzzaa@naver.com",
                "role": "USER"
            }
        ],
        "page": {
            "size": 10,
            "number": 0,
            "totalElements": 2,
            "totalPages": 1
        }
    }
}

 

딱 봐도 적용 후가 JSON 응답 형식이 훨씬 더 깔끔해졌음을 알 수 있다 !!

 

달라진 점을 정리해보자면,

  1. pageable → page:
    • 이전에는 페이지 정보를 pageable 키 아래에 포함시켰다면, 이제는 page라는 키로 변환되어 더 명확하게 페이지 정보를 나타낸다. 이는 가독성을 높여 주며, 페이지 정보를 더 직관적으로 이해할 수 있게 만든다.
  2. 불필요한 속성 제거:
    • 이전 JSON에서는 pageable 객체 안에 여러 속성이 포함되어 있었다. 예를 들어,  pageNumber, offset, paged, unpaged, last, first, numberOfElements, empty 등이 포함되어 있었는데, 이는 사실 페이지 정보를 이해하는 데는 필요하지 않은 정보들이다. (덕지덕지 붙어있는 느낌)
    • 이후 JSON에서는 필수적인 정보만 남겨두어, 페이지 관련 정보가 비교적 간결해졌다. size, number, totalElements, totalPages만 포함되어 있어 한눈에 필요한 정보만 확인할 수 있다.
  3. 가독성 향상:
    • 무엇보다 전체적으로 JSON 구조가 간단해져서 가독성이 좋아졌다. 페이지 정보가 page 객체로 그룹화되어 있어서, 필요한 정보를 쉽게 찾을 수 있게 됐다.

 

결론

이렇게 변경함으로써 API 응답의 가독성을 높이고, 클라이언트 쪽에서 페이지 정보를 처리할 때 더 용이하게 만들게 된 것 같다! 구현 방법이 간단하기도 하고 결과 차이도 만족스러웠기 때문에 시도할 가치가 있었던 기술적 의사결정이었다.


References

https://nhahan.tistory.com/m/153

https://github.com/spring-projects/spring-boot/pull/39797