Serializing PageImpl instances as-is is not supported 경고 발생

2024. 7. 20. 10:14TIL

 

 문제발생

프로젝트 진행중 와인 프로모션 DB를 조회할때 FETCH JOIN을 사용하여 N+1 문제를 해결하려고 시도하였는데,  직렬화 관련 문제가 발생하였다.

PageModule$PlainPageSerializationWarning : 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.

 

🆘 해결 과정

 

해당 경고 메시지는 PageImpl 인스턴스를 직렬화할 때 JSON 구조의 안정성을 보장하지 못한다는 내용이며  Spring Data의 PageModel을 사용하는것을 권고하고 있다. 즉 페이지네이션을 적용할때 발생하는 문제이다.

 

 

의존성 추가

Spring HATEOAS와 Spring Data의 PagedResourcesAssembler를 사용하여 Page를 PagedModel로 변환할 수 있기 때문에 hateoas 의존성을 추가해줘야 한다.

dependencies {

    implementation("org.springframework.boot:spring-boot-starter-hateoas")

}

 

 

PagedResourcesAssembler를 빈으로 등록

package sparta.nbcamp.wachu.infra.hateoas

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.web.PagedResourcesAssembler
import org.springframework.data.web.config.PageableHandlerMethodArgumentResolverCustomizer
import org.springframework.hateoas.config.EnableHypermediaSupport

@Configuration
@EnableHypermediaSupport(type = [EnableHypermediaSupport.HypermediaType.HAL])
class HateoasConfig {

    @Bean
    fun pagedResourcesAssembler(): PagedResourcesAssembler<*> {
        return PagedResourcesAssembler<Any>(null, null)
    }

    @Bean
    fun pageableCustomizer(): PageableHandlerMethodArgumentResolverCustomizer {
        return PageableHandlerMethodArgumentResolverCustomizer {
            it.setOneIndexedParameters(true)
        }
    }
}

 

WinePromotion을 감싸는 모델 클래스를 생성

ATEOAS (Hypermedia as the Engine of Application State) 원칙에 따라, 리소스를 감싸는 래퍼를 사용하여 각 리소스에 대한 링크를 포함시키기 위해서 모델 클래스를 생성한다.

package sparta.nbcamp.wachu.domain.wine.entity

import org.springframework.hateoas.EntityModel
import org.springframework.hateoas.server.core.Relation

@Relation(itemRelation = "winePromotion", collectionRelation = "winePromotions")
class WinePromotionModel(winePromotion: WinePromotion) : EntityModel<WinePromotion>(winePromotion)

 

WinePromotion을 WinePromotionModel로 감싸는 어셈블러 생성

WinePromotion 엔티티를 WinePromotionModel로 변환하는 어셈블러

package sparta.nbcamp.wachu.domain.wine.entity

import org.springframework.hateoas.server.RepresentationModelAssembler
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder
import org.springframework.stereotype.Component
import sparta.nbcamp.wachu.domain.wine.controller.WineController

@Component
class WinePromotionAssembler : RepresentationModelAssembler<WinePromotion, WinePromotionModel> {

    override fun toModel(entity: WinePromotion): WinePromotionModel {
        val model = WinePromotionModel(entity)
        model.add(
            WebMvcLinkBuilder.linkTo(
                WebMvcLinkBuilder.methodOn(WineController::class.java)
                    .getPromotionWineList(direction = "asc", sortBy = "price", page = 0, size = 10)
            ).withSelfRel()
        )
        return model
    }
}

 

 

 

Service 에서 PagedModel 적용

override fun getPromotionWineList(
    page: Int,
    size: Int,
    sortBy: String,
    direction: String
): PagedModel<WinePromotionModel> {

    val pageable: Pageable = PageRequest.of(page, size, getDirection(direction), sortBy)
    val promotionsPage: Page<WinePromotion> = winePromotionRepository.findPromotionWineList(pageable)

    return pagedResourcesAssembler.toModel(promotionsPage, winePromotionAssembler)
}

 

 

Controller 반환값 PagedModel로 수정

@RequestMapping("/api/v1/wines")
@RestController
class WineController(
    private val wineService: WineService,
){

@GetMapping("/promotion")
fun getPromotionWineList(
    @RequestParam(value = "page", defaultValue = "0") page: Int,
    @RequestParam(value = "size", defaultValue = "10") size: Int,
    @RequestParam(value = "sort_by", defaultValue = "createdAt") sortBy: String,
    @RequestParam(value = "sort_direction", defaultValue = "asc") direction: String,
): ResponseEntity<PagedModel<WinePromotionModel>> {
    return ResponseEntity.status(HttpStatus.OK).body(
        wineService.getPromotionWineList(
            page = page,
            size = size,
            sortBy = sortBy,
            direction = direction
        )
    )
}
}

 

 해결

 

이제는 쿼리를 날려도 해당 경고창이 발생하지 않는 것을 확인할 수 있다.

'TIL' 카테고리의 다른 글

pagedModel fetchJoin 적용후 redis Cache 적용시 발생하는 오류  (0) 2024.07.22
QueryDslSupport  (0) 2024.07.21
Fetch Join  (0) 2024.07.19
최종 프로젝트 (7일차)  (0) 2024.07.18
@AfterEach 그리고 @DirtiesContext (테스트 코드)  (0) 2024.07.17