기존과 같이 new PageImpl()을 이용하여 Page<T> 객체를 내리는데 아래와 같은 에러(warn)가 발생한다.
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))
에러 발생 이유
Spring Boot 3.x
내부적으로 사용하는:
Spring Data Commons 3.x (2023.0.x 이상)
Spring Framework 6.x
이 조합에서 PageImpl을 그대로 @RestController에서 반환하면 Jackson 직렬화 구조가 불안정하다는 경고가 발생한다.
Spring 팀은 PageImpl<T>의 직렬화가 다음 문제를 가진다고 판단했다:
Jackson 직렬화 시 필드 순서나 구조가 변경될 수 있음
필드 이름이 내부 구현에 의존
API 스펙을 안정적으로 유지하기 어려움
그래서 Spring Data 팀이 공식적으로 PagedModel<T> 또는 DTO 변환을 권장하게 됨.
해결 방법
1. hate oas 를 사용하는 경우
PagedModel<T> 사용
return pagedResourcesAssembler.toModel(page);
: 결과가 PagedModel<EntityModel<T>> 형태가 되어 안정적인 JSON 구조를 보장
2. hate oas 사용 안하는 경우
1) 설정 추가로 해결
@Configuration
@EnableSpringDataWebSupport(pageSerializationMode = PageSerializationMode.VIA_DTO)
public class WebConfig {
}
2) 직접 커스텀 DTO를 만들어서 사용
public class PageResponse<T> {
private List<T> content;
private int page;
private int size;
private long totalElements;
public PageResponse(List<T> content, int page, int size, long totalElements) {
this.content = content;
this.page = page;
this.size = size;
this.totalElements = totalElements;
}
// getters, setters
}
최신 버전의 스프링부트를 쓰면 어느 새부터 아래와 같은 워닝을 만나는데 상당히 신경 쓰인다. 그동안 Page 인터페이스를 아주 많이 사용했던 터라 혹시 안되거나 deprecated 된다면 난감하기 때문이다..
찾아보니 springboot3.3부터 변경되었다고 한다!
2024-12-12 13:57:23 WARN [ration$PageModule$WarningLoggingModifier.changeProperties : 156] 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.
내용은 직렬화 시 안정적이지 않으니 Spring Data의 PagedModel 또는 PagedResourcesAssembler를 사용하여 안정적인 JSON 구조를 생성하라는 것이다.
그동안 직렬화할 때 PageImpl을 직접 직렬화하였는데, 더이상 안정적인 방식이 아니니 아래 두 방식 중 하나를 고르라는 뜻
HATEOAS를 쓰면 PagedResourcesAssembler, 그렇지 않으면 PagedModel
JSON 구조가 API의 변경이나 버전 업그레이드에 따라 변할 수 있으므로, 안정성이 떨어질 수 있음
이 모드는 이전 버전의 Spring Data에서 기본적으로 사용되던 방식
DTO
DTO(Data Transfer Object)를 사용하여 페이지 데이터를 직렬화
안정적이고 일관된 JSON 구조를 제공
DTO를 사용함으로써 페이지 데이터의 구조가 API 변경에 영향을 덜 받음
이 모드는 PagedModel 또는 PagedResourcesAssembler와 함께 사용되며, 이를 통해 클라이언트가 예측 가능한 형식의 데이터를 수신할 수 있다.
설정하고 기존과 똑같이 페이징하면 된다.
@GetMapping
public Page<BaseResponse> getPrices(
참고로 HATEOAS
HATEOAS는 REST API 설계의 원칙 중 하나로, 클라이언트가 서버 응답에 포함된 하이퍼미디어(hypermedia)를 통해 애플리케이션 상태를 동적으로 탐색할 수 있도록 하는 방식이다. 이 원칙은 REST의 자기 설명(self-descriptive) 특성을 강화한다
HATEOAS의 구성 요소
링크(Link): API 응답에 포함된 URL. 다음 가능한 액션을 안내.
상태(State): 현재 리소스의 상태.
동작(Action): 링크를 따라가면 수행할 수 있는 작업.
HATEOAS의 사용 이유
API 탐색성 증가:
클라이언트는 추가적인 문서 없이 서버 응답에 포함된 링크를 통해 어떤 작업이 가능한지 동적으로 파악할 수 있음
클라이언트-서버 결합도 감소:
클라이언트는 서버가 제공하는 링크를 따라가기 때문에 특정 엔드포인트에 강하게 의존하지 않음
유연한 확장성:
서버에서 새로운 액션이나 엔드포인트를 추가하더라도, 클라이언트는 변경 없이 새로운 기능을 사용할 수 있음
자기 설명적 API:
서버 응답에 포함된 하이퍼미디어가 클라이언트에게 리소스 상태 및 가능한 작업을 설명하므로 API의 문서화와 유지보수가 용이
@Value("#{jobParameters[dateParam]}")
public String dateParam;
Caused by: org.springframework.expression.spel.SpelEvaluationException:
EL1008E: Property or field 'jobParameters' cannot be found on object of type 'org.springframework.beans.factory.config.BeanExpressionContext' - maybe not public or not valid?
빈만 등록되면 안 되고 scope에서 보이게끔 선언해야 한다.
Job 안에서 보이게 하려면 JobScope, Step 안에서 보이려면 StepScope 안에서 사용하게 끔 아래와 같이 빈 선언부에 등록한다.
In Spring Batch 5, when job parameters are passed as strings, Spring Batch will automatically infer the correct type ( String, Long, Double, or Date) based on the format of the input.
trial3. 날짜는 같은데도 반복 실행이 된다?
위에서 날짜를 받아서 job parameter로 넘기는 것을 해봤다. 근데도 여전히 반복 실행이 된다. 왜 그런가 싶어 job execution에 사용된 파라미터를 확인해 보니 아래와 같이 run.id와 복합 키로 잡고 있어서 매번 다르게 인식하고 있었다.
해당 부분은 소스로 보면 아래와 같은데, RunIdIncrementer가 run.id의 키로 하나씩 키를 증가시키면서 실행하기 때문이다.
@Bean(JOB_NAME)
public Job rankingJob(JobRepository jobRepository, Step rankingStep) {
return new JobBuilder(JOB_NAME, jobRepository)
.incrementer(new RunIdIncrementer()) // <----
.start(rankingStep)
.build();
}
따라서 해당 부분을 주석하면 여러 번 실행되지 않는 것을 확인할 수 있다.
16:32:21.071 [main] ERROR o.s.boot.SpringApplication - Application run failed
java.lang.IllegalStateException: Failed to execute ApplicationRunner
...
Caused by: org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException: A job instance already exists and is complete for parameters={'dateParam':'{value=2024-08-21, type=class java.lang.String, identifying=true}'}. If you want to run this job again, change the parameters.
체크포인트
RunIdIncrementer와 같이 매번 다른 키를 생성하는 job incrementer를 사용하지 않았는지 확인
: GameMapper삭제하고 (Config에 annotationClass 주석하니; 하건 안하건 둘 다) 에러 발생
Caused by: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.batch.domain.mapper.gamemapper.GameMapper.insertRank
xml의 namespace가 인터페이스와 연결되어 있어야 쿼리 주입이 가능
2. 직접적인 함수의 호출이 없으므로 함수는 삭제 가능? Yes
xml안에는 <select>, <insert> 등 여러 쿼리가 있지만 직접 호출하지 않으므로 interface에 연결하는 함수는 없어도 된다.
@DataSource
public interface GameMapper {
// List<Ranking> selectDailyTop1000UsersByMatchCnt();
}
@Bean
@StepScope
public MyBatisCursorItemReader<Ranking> dailyRankingMatchCntReader(
@Qualifier(DataSourceConfig.SESSION_FACTORY) SqlSessionFactory logDb) {
return new MyBatisCursorItemReaderBuilder<Ranking>()
.sqlSessionFactory(logDb)
.queryId(LOG_MAPPER + "selectDailyTop1000UsersByMatchCnt")
.build();
}