728x90
반응형
728x90
반응형
반응형

환경: springboot3.2

 

기존과 같이 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
}
728x90
반응형
반응형

환경: springboot3.4, java17

 

최신 버전의 스프링부트를 쓰면 어느 새부터 아래와 같은 워닝을 만나는데 상당히 신경 쓰인다. 그동안 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

프로젝트 전역으로 설정하는 방식은 아래와 같다.

@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)

https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables

PageSerializationMode 에는 아래와 같이 두 가지가 있다.

DIRECT

PageImpl 객체를 직접 JSON으로 직렬화함

  • 직렬화된 JSON 구조는 PageImpl의 내부 구조에 따라 다를 수 있음
  • 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의 문서화와 유지보수가 용이
728x90
반응형
반응형

환경: 자바 17, springboot3.1.5, springCloud 2022.0.4

목표: 배치가 하루에 한 번 돌아야 하고 (성공했어도) 종종 수동으로 한번 더 돌릴 수 있어야 함.

 

trial1: program argument로 date를 넘겨 중복 실행을 막아보자

step1. job parameter를 program argument로 넘겨야 한다.

시도한 방법

java -jar aaa.jar --job.name=sampleBatchJob dateParam=2022-09-09

에러 발생

Caused by: org.springframework.batch.core.converter.JobParametersConversionException: Unable to decode job parameter 2024-08-22
...
Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character ('-' (code 45)): Expected space separating root-level values
 at [Source: (String)"2024-08-22"; line: 1, column: 6]
...

그 어떤 글을 봐도 job param을 넘길 때 그냥 넘기길래.. 계속 저 방식으로 시도했지만.. 실패가 났다..

'-'가 문제 되나 싶어 지우고 해 봐도, 숫자가 아닌 임의의 문자열을 줘도 비슷한 에러가 나길래, 타입의 문제는 아닌 것 같았다.

 

step2. 혹시 springboot2 와 3의 차이로 인해 발생?

구글링 한 자료들이 outdated 된 것일 수 있다고 판단하였다.

그 이유는 springboot3 로 오면서 크게 변한 것 중 하나가 javax -> jakarta로 패키지명이 변한 것인데

사실 그 때는 jackson이라고 착각했다. 어쨌건 라이브러리 변화가 있어서 추가 설정이나 파라미터 넘기는 방식이 변했을지도 모르겠다고 생각했다.

그러다 구글링하다 파라미터에 type을 주는 예시를 봤는데, 아래와 같이 시도해 보았지만 역시나 같은 에러가 발생하였다.

dateParam(String)=2024-08-22

더 파고들어보니 위 방식은 fade out 되었고 boot3으로 버전이 오르면서 아래와 같이 바뀌었다는 글을 보게 된다.

parameter=value,type,identifying

그래서 아래와 같이 시도해 보았지만 여전히 실패하였다.

dateParam=2024-08-22,String,true

 

 

step3. 파고들기

관련 글을 좀 더 보다 보니 위와 같은 형태로 파라미터를 전달하려면 아래의 잡 파라미터 컨버터를 사용해야 한다고 한다. 이름 그대로 설정이 없을 경우 "기본적"으로 사용하는 컨버터이다.

DefaultJobParametersConverter

해당 프로젝트의 컨버터 설정이 뭔지 찾아보니 맙소사.. 다른 것이었다.

 @Bean
  public JobParametersConverter jobParametersConverter() {
    return new JsonJobParametersConverter();
  }

해당 컨버터를 사용할 경우 잡 파라미터를 아래와 같은 형태로 넘겨야 한다고 한다.

parameterName='{"value": "parameterValue", "type":"parameterType", "identifying": "booleanValue"}'

그래서 비슷하게 만들고 실행해 본다.

--job.name=sampleBatchJob
dateParam='{"value":"2024-08-22","type":"java.lang.String","identifying":"true"}'

아래의 에러가 발생한다.

Caused by: org.springframework.batch.core.converter.JobParametersConversionException: Unable to decode job parameter '{value:2024-08-22,type:java.lang.String,identifying:true}'
...
Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character (''' (code 39)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
...

json parsing 에러다. single quotaion, double quotation 문제인가 싶어서 여러 조합으로 수정해 봤는데도 비슷한 에러만 발생한다.

그러던 중 한 글을 보게 되는데, json 안의 quote에는 escape 처리를 해주어야 한다는 것! (https://github.com/spring-projects/spring-batch/issues/4299)

그래서 아래처럼 수정했더니 드디어 돌아간다!

--spring.profiles.active=local
--job.name=sampleBatchJob
dateParam="{\"value\":\"2024-08-22\",\"type\":\"java.lang.String\",\"identifying\":\"true\"}

복수개의 파라미터를 넘기게 된다면 아래와 같다. 필요없는 콤마, 따옴표.. 등등이 들어가면 뜬금없는 에러가 나며 인식이 되지 않는다(에러 상황을 알기 어려움).

--spring.profiles.active=local
--job.name=DailyRankingJob
date="{\"value\":\"2024-08-21\",\"type\":\"java.lang.String\",\"identifying\":\"true\"}"
version="{\"value\":\"1\",\"type\":\"java.lang.Integer\",\"identifying\":\"true\"}"

json job converter 예시: https://spring.io/blog/2022/11/24/spring-batch-5-0-goes-ga

배치 최신 문서: https://docs.spring.io/spring-batch/reference/job/running.html

 

trial2. 날짜는 넘겼는데, 잡에서 사용하게 해야 하네!

job parameter를 프로젝트에서 보이게 하려면 우선 빈으로 등록되어 있어야 한다.

아래와 같이 일반적인 string으로 받을 경우 아래의 에러를 만난다.

  @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 안에서 사용하게 끔 아래와 같이 빈 선언부에 등록한다.

@Bean(STEP_NAME)
@JobScope
public Step rankingStep(
    JobRepository jobRepository,
    PlatformTransactionManager transactionManager,
    MyBatisCursorItemReader<Ranking> dailyRankingReader,
    ItemWriter<Ranking> dailyRankingWriter,
    @Value("#{jobParameters[dateParam]}") String dataParam) {
    	...
    }

이때 Job Parameter의 타입으로 사용할 수 있는 것으로는 Double, Long, Date, String이 있다.(배치4 기준)

LocalDate나 LocalDateTime같은 타입은 String으로 받아서 타입 변환을 해야 한다. 반환하는 방법은 크게 세 가지가 있다(https://jojoldu.tistory.com/490). 여기서는 setter주입 방식으로 해본다.

@Getter
@NoArgsConstructor
@Component
@JobScope
public class DailyRankingJobParameter {
  private LocalDate date;

  @Value("#{jobParameters[dateParam]}")
  public void setDate(String date) {
    this.date = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
  }
}

빈으로 등록되어야 JobScope이 먹기 때문에 굳이? 싶어도 Component 등록을 해줘야 한다.

사용하고자 하는 Job이나 Step에서는 argument로 전달할 필요 없이 클래스에서 생성자로 받아서 바로 사용하면 된다.

@Configuration
@RequiredArgsConstructor
public class DailyRankingJobConfig {

	private final DailyRankingJobParameter jobParameter;
    
    ...
    
  @Bean(STEP_NAME)
  @JobScope
  public Step sinyutnoriDailyRankingStep(
      JobRepository jobRepository,
      PlatformTransactionManager transactionManager,
      MyBatisCursorItemReader<SinyutnoriRanking> dailyRankingReader,
      ItemWriter<SinyutnoriRanking> dailyRankingWriter
      //      @Value("#{jobParameters[dateParam]}") String dataParam
      ) {
    System.out.println(jobParameter);
    return new StepBuilder(STEP_NAME, jobRepository)
        .<SinyutnoriRanking, SinyutnoriRanking>chunk(CHUNK_SIZE, transactionManager)
        .reader(dailyRankingReader)
        .writer(dailyRankingWriter)
        .build();
  }

 

위 내용은 fade out된 내용이고(물론 위처럼 해도 작동은 됨) 실제로는 job parameter class를 만들 필요도 없이! argument에 아래와 같이 전달하면 된다.

--job.name=DailyRankingJob
date="{\"value\":\"2024-08-21\",\"type\":\"java.time.LocalDate\",\"identifying\":\"true\"}"
version="{\"value\":\"2\",\"type\":\"java.lang.Integer\",\"identifying\":\"true\"}"

사용하려는 job/step에서 바로 땡겨다 사용 가능. 클래스에 선언하면 scope이 정의되지 않아 에러가 난다.

@Bean(STEP1_NAME)
@JobScope
public Step DailyRankingMatchCntStep(
    JobRepository jobRepository,
    PlatformTransactionManager transactionManager,
    @Value("#{jobParameters[date]}") LocalDate date) {

batch 5에 추가된 내용

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를 사용하지 않았는지 확인
  • argument로 파라미터를 넘길 때 identifying 값을 true로 넘겼는지 확인
    • 해당 의미는 고유 키값인지 의미로 true면 execution key로 인식한다.
dateParam="{\"value\":\"2024-08-21\",\"type\":\"java.lang.String\",\"identifying\":\"true\"}
  • 이전 execution 결과, params 확인
    • 이전 execution이 fail이면 execution id는 달라도 같은 job instance의 execution으로 묶여 재실행이 가능하다.
    • 해당 내용을 확인할 수 있는 쿼리 참고..
select execution.JOB_EXECUTION_ID, execution.JOB_INSTANCE_ID, execution.CREATE_TIME, execution.STATUS, execution.EXIT_CODE, execution.EXIT_MESSAGE,
params.PARAMETER_NAME, params.PARAMETER_TYPE, params.PARAMETER_VALUE, params.IDENTIFYING
FROM BATCH_CASUAL_JOB_EXECUTION execution inner join BATCH_CASUAL_JOB_EXECUTION_PARAMS params on execution.JOB_EXECUTION_ID = params.JOB_EXECUTION_ID 
order by JOB_EXECUTION_ID  DESC
;

job instance 45의 경우 dateParam, identifying: true로 실패 -> 성공을 했고(execution 47, 48)

job instance 46번의 경우, 같은 dateParam이지만 identifying:false로 실행을 하니 다른 job parameter로 인식을 해서 실행을 되었고 실패 난 것을 알 수 있다.

 

결론

배치가 하루에 한 번 돌아야 하고 종종 수동으로 한번 더 돌릴 수 있으려면..

배치 당 unique 한 값(날짜 등)을 argument로 넘기고 job parameter로 받아서 적당한 scope의 빈에 등록해야 한다.

나의 경우, json의 형식으로 parameter를 넘기고 혹시 재실행이 필요할 경우 identifying: false로 재실행하려고 한다.

 


참고

argument로 파라미터 보내는 방법

https://velog.io/@guswns3371/Spring-Boot-Framework-%EB%B2%84%EC%A0%84-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%EA%B3%BC%EC%A0%95

 

Spring Boot Framework 버전 업그레이드 과정

new features of jdk 17 & spring boot 3major spring projectsJDK를 최소 17부터 19까지 지원함.Java 11과 비교하여 GC 등 성능 개선문자열, 리스트 등 다양한 API 지원타입 추론 키워드 추가switch 문 확장r

velog.io

 

파라미터를 bean으로 등록한다는 것의 의미

https://velog.io/@lxxjn0/Spring-Batch-Guide-05.-Spring-Batch-Scope-Job-Parameter

 

Spring Batch Guide - 05. Spring Batch Scope & Job Parameter

Spring Batch Guide 시리즈는 이동욱 개발자님의 Spring Batch 가이드를 보고 학습한 내용을 정리한 글입니다.많은 내용이 원 글과 유사할 수 있습니다. 이 점 양해바랍니다 🙏🏻 이번에는 Spring Batch의 S

velog.io

batch5에 추가된 내용

https://devfunny.tistory.com/931

 

[Kotlin + SpringBatch5] SpringBatch5의 다양한 파라미터 지원 - Job 생성해서 테스트 및 메타테이블 확인, i

SpringBatch5의 다양한 파라미터 지원 https://devfunny.tistory.com/930 SpringBatch5 변경사항 정리 (vs SpringBatch4) SpringBatch 5.0 이전 SpringBatch 공부할때 SpringBatch 4.0 버전이였다. 최근, SpringBatch 복습을 위해 새로

devfunny.tistory.com

 

728x90
반응형
반응형

환경: springboot3, spring batch5, mybatis

그동안 jpa만 주구장창 사용했어서 올만에 Mybatis 설정이다!

 

1. 디비 정보 등록(application.yml)

2. 빈 등록

@Configuration
@RequiredArgsConstructor
@MapperScan(
    value = {"com.batch.ranking.mapper"},
    annotationClass = LogDataSource.class,
    sqlSessionFactoryRef = "LogDbSqlSessionFactory",
    sqlSessionTemplateRef = "LogDbSqlSessionTemplate")
public class LogDataSourceConfig {

  public static final String SOURCE_DATASOURCE_NAME = "LogDbDataSource";

  @Value("classpath:mybatisConfig.xml")
  private Resource configLocation;

  @Bean(SOURCE_DATASOURCE_NAME)
  public DataSource LogDbDataSource() {
    DataSourceProperty dataSourceProperty = //get them from property

    Properties properties = new Properties();
    properties.setProperty("url", dataSourceProperty.getJdbcUrl());
    properties.setProperty("user", dataSourceProperty.getUsername());
    properties.setProperty("password", dataSourceProperty.getPassword());

    AtomikosDataSourceBean dataSource = new AtomikosDataSourceBean();
    dataSource.setUniqueResourceName(SOURCE_DATASOURCE_NAME);
    dataSource.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
    dataSource.setXaProperties(properties);
    dataSource.setMaxPoolSize(connectionPoolProperty.getMaximumPoolSize()); //from property
    dataSource.setMinPoolSize(connectionPoolProperty.getMinimumIdle());

    return dataSource;
  }

//Qualifier is mandatory otherwise it will connect to Primary bean
  @Bean
  public SqlSessionFactory LogDbSqlSessionFactory(
      @Qualifier("LogDbDataSource") DataSource LogDbDataSource) throws Exception {
    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
    bean.setConfigLocation(configLocation);
    bean.setDataSource(LogDbDataSource);
    return bean.getObject();
  }

  @Bean
  public SqlSessionTemplate LogDbSqlSessionTemplate(
      @Qualifier("LogDbSqlSessionFactory") SqlSessionFactory LogDbSqlSessionFactory) {
    return new SqlSessionTemplate(LogDbSqlSessionFactory);
  }
}

2-1. mybatis 설정은 자바로 해도 되지만 분리하는 게 가독성이 좋아서 분리하였다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0/EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <typeAliases>
        <typeAlias type="com.batch.adapter.mybatis.handlers.RankingTypeHandler" alias="RankingTypeHandler" />
    </typeAliases>
</configuration>

2-2. 매퍼 마킹하는 어노테이션

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface LogDataSource {}

3. 사용하는 job에서 부를 경우

  private final SqlSessionFactory LogDbSqlSessionFactory;

  @Bean
  @StepScope
  public MyBatisCursorItemReader<Ranking> DailyRankingReader() {
    return new MyBatisCursorItemReaderBuilder<Ranking>()
        .sqlSessionFactory(LogDbSqlSessionFactory)
        .queryId(
            "com.batch.domain.ranking.mapper.DailyRankMapper.selectDailyTop100")
        .build();
  }

4. 매퍼에 쿼리 작성

@LogDataSource
public interface DailyRankMapper {

  List<Ranking> selectDailyTop100();
}
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.batch.domain.ranking.mapper.DailyRankMapper">
  <resultMap id="sinyutnoriRanking"
    type="com.batch.domain.ranking.model.Ranking">
    <constructor>
      <idArg column="memberid" javaType="java.lang.String" name="memberId"/>
      <arg column="regdate" javaType="java.time.LocalDate" name="registerDate"/>
      <arg column="kind" javaType="com.batch.domain.ranking.type.RankingType" name="RankingType" typeHandler="RankingTypeHandler"/>
      <arg column="gamemoney" javaType="java.lang.Long" name="gameMoney"/>
      <arg column="winrate" javaType="java.lang.Long" name="winRate"/>
      <arg column="matchcnt" javaType="java.lang.Long" name="matchCount"/>
      <arg column="wincnt" javaType="java.lang.Long" name="winCount"/>
      <arg column="defeatcnt" javaType="java.lang.Long" name="defeatCount"/>
      <arg column="ranking" javaType="java.lang.Integer" name="ranking"/>
    </constructor>
  </resultMap>

  <select id="selectDailyTop100" resultMap="Ranking">
    <![CDATA[
    SELECT  memberid
         , regdate
         , kind
         , gamemoney
         , winrate
         , matchcnt
         , wincnt
         , defeatcnt
         , @RNUM := @RNUM + 1 AS ranking
    FROM Table
      ORDER BY (wincnt + defeatcnt + drawcnt) DESC
      , (wincnt / (wincnt + defeatcnt + drawcnt)) * 100 DESC
      , gamemoney
      ) B, (SELECT @RNUM := 0) r
    WHERE @RNUM < 100
    ]]>
  </select>
</mapper>

5. enum으로 바로 꺼내고 싶다면 type handler 작성

public class RankingTypeHandler extends BaseTypeHandler<RankingType> {

  @Override
  public void setNonNullParameter(
      PreparedStatement ps, int i, RankingType parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setInt(i, parameter.getCode());
  }

  @Override
  public RankingType getNullableResult(ResultSet rs, String columnName)
      throws SQLException {
    return RankingType.findByCode(rs.getInt(columnName)).orElse(null);
  }

  @Override
  public RankingType getNullableResult(ResultSet rs, int columnIndex)
      throws SQLException {
    return RankingType.findByCode(rs.getInt(columnIndex)).orElse(null);
  }

  @Override
  public RankingType getNullableResult(CallableStatement cs, int columnIndex)
      throws SQLException {
    return RankingType.findByCode(cs.getInt(columnIndex)).orElse(null);
  }
}

 

여기서 궁금한 사항

Mapper interface를 직접 사용하지 않은데도(자바 로직에서) 클래스가 필요한 것인가?

reader/writer를 보면 아래와 같이 직접 db factory를 연결했으니 interface는 필요 없는 게 아니냐는 문의를 주셔서 좀 더 알아본다.

  @Bean
  @StepScope
  public MyBatisCursorItemReader<SinyutnoriRanking> dailyRankingMatchCntReader(
      @Qualifier(HangameLogDataSourceConfig.SESSION_FACTORY) SqlSessionFactory logDb) {
    return new MyBatisCursorItemReaderBuilder<SinyutnoriRanking>()
        .sqlSessionFactory(logDb)
        .queryId(LOG_MAPPER + "selectDailyTop1000UsersByMatchCnt")
        .build();
  }

 

설정이 대략 이런식으로 연결되어 있다고 할 때...

@Configuration
@RequiredArgsConstructor
@MapperScan(
    value = {"com.batch.domain.mapper.gamemapper.*"},
    annotationClass = DataSource.class,         //mapper interface에 해당 어노테이션을 달아야 
    sqlSessionFactoryRef = DataSourceConfig.SESSION_FACTORY,
    sqlSessionTemplateRef = "DbSqlSessionTemplate")
public class DataSourceConfig {
@DataSource
public interface GameMapper {
<mapper namespace="com.batch.domain.mapper.gamemapper.GameMapper">

 

1. interface 삭제 가능? No

:  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();
}

select 함수가 직접적으로 선언되지 않아도 작동한다.

작동은 되지만 나중에 관리차원에서 헷갈릴까 봐 지울지 말지 약간 걱정은 된다..

 

 

728x90
반응형
반응형

이슈

- build 시 intelliJ면 아래와 같은 에러가 남

- gradle 일 경우 에러 안 남

 

해결

- interface일 경우 @PathVariable에 name 을 명시

 

참고

https://shanepark.tistory.com/331

728x90
반응형

+ Recent posts