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

환경: springboot3.1.5, spring batch5, junit5

 

어찌어찌 배치 프로그램은 짰는데, 테스트코드는 어떻게 짜야할지 막막했다.

심지어 이 배치는 디비에서 오늘에 해당하는 데이터를 읽어 다른 디비에 적재하는 배치인데 

  1. "오늘"이라는 날짜 디펜덴시가 있는 데이터가 필요하고
  2. 이걸 타 디비에 실제로 넣어야 한다.

h2를 추가하여 로컬 배치로 돌리는 방법이 있겠지만 돌리는 날짜에 기반한 샘플 데이터를 만들어 넣는 게 좀 귀찮았고

디비 작업이야, 쿼리만 정확하면 보증되는 것이라(이미 다른 곳에서 돌고 있는 쿼리라서 실행이 보장되어 있음)

내가 검증하고 싶은 건 데이터를 정확히 꺼내오는 것이 아닌 job, step 등이 순차적으로 잘 도는지에 대해 작성하고 싶었다.

 

하여 db select, insert 부분을 mocking 할 수 있으면 좋겠다는 생각을 했다.

 

step1. get job launcher 

bean으로 등록하거나

(아래 코드 테스트 안 해봄)

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TestBatchConfig {

    @Bean
    public JobLauncherTestUtils jobLauncherTestUtils() {
        return new JobLauncherTestUtils();
    }
}
@SpringBootTest
@SpringBatchTest // mandatory?
@Import({TestBatchConfig.class, YourJobConfig.class})  // Replace YourJobConfig with your actual job configuration class
public class YourJobTest {

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

util로 만들어 빈으로 등록

public class JobTestUtils {

      @Autowired private ApplicationContext applicationContext;
      @Autowired private JobRepository jobRepository;
      @Autowired private JobExplorer jobExplorer;
      @Autowired private JobLauncher jobLauncher;

      public JobLauncherTestUtils getJobTester(String jobName) {
        Job bean = applicationContext.getBean(jobName, Job.class);
        JobLauncherTestUtils jobLauncherTestUtils = new JobLauncherTestUtils();
        jobLauncherTestUtils.setJobLauncher(jobLauncher);
        jobLauncherTestUtils.setJobRepository(jobRepository);
        jobLauncherTestUtils.setJob(bean);
        return jobLauncherTestUtils;
      }

      public JobParameters makeJobParameters(JobParameters parameters) {
        return new JobParametersBuilder(jobExplorer).addJobParameters(parameters).toJobParameters();
      }
      ...
  }
@TestConfiguration
public class TestBatchConfig {

  @Bean
  public JobTestUtils jobTestUtils() {
    return new JobTestUtils();
  }
}
@ActiveProfiles("test")
@Import({TestBatchConfig.class})
@SpringBootTest
class DailyRankingJobConfigTest {

  @Autowired private JobTestUtils jobTestUtils;

...
}

 

step2. mocking 하고자 하는 reader/writer가 빈으로 등록되어야 한다.

실제 job class에서 아래와 같이 item reader/writer가 주입되도록 하고..

@Configuration
@RequiredArgsConstructor
public class DailyRankingJobConfig {

  private final DailyRankingJobParameter jobParameter;

  @Qualifier("dailyRankingMatchCntReader")
  private final MyBatisCursorItemReader<Ranking> dailyRankingMatchCntReader;

  @Qualifier("dailyRankingGameMoneyReader")
  private final MyBatisCursorItemReader<Ranking> dailyRankingGameMoneyReader;

  @Qualifier("dailyRankingWriter")
  private final ItemWriter<Ranking> dailyRankingWriter;

테스트 코드에도 빈을 주입하는데.. @MockBean어노테이션을 이용한다. 여기서 주의할 건 name에 꼭 빈 이름을 넣어야 한다.. 안 그럼 못 찾는 듯.. 에러가 발생한다.

@ActiveProfiles("test")
@Import({TestBatchConfig.class})
@SpringBootTest
class DailyRankingJobConfigTest {

  @Autowired private JobTestUtils jobTestUtils;

  @MockBean(name = "dailyRankingMatchCntReader")
  private MyBatisCursorItemReader<Ranking> dailyRankingMatchCntReader;

  @MockBean(name = "dailyRankingGameMoneyReader")
  private MyBatisCursorItemReader<Ranking> dailyRankingGameMoneyReader;

  @MockBean(name = "dailyRankingWriter")
  private ItemWriter<Ranking> dailyRankingWriter;
  
  ...
  
   @Test
  @DisplayName("성공 케이스")
  void job__success() throws Exception {
    // given
    JobParameters parameters =
        new JobParametersBuilder()
            .addString(
                "date", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")), true)
            .addString("test version", UUID.randomUUID().toString(), true)
            .toJobParameters();

    given(dailyRankingMatchCntReader.read()).willReturn(getRanks().get(0), getRanks().get(1), null);
    given(dailyRankingGameMoneyReader.read()).willReturn(getRanks().get(1), null);
    doNothing().when(dailyRankingWriter).write(any());

    // when
    JobExecution jobExecution =
        jobTestUtils
            .getJobTester(DailyRankingJobConfig.JOB_NAME)
            .launchJob(jobTestUtils.makeJobParameters(parameters));

    // then
    assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED);
    // reader의 경우 chunk의 갯수만큼 호출
    verify(dailyRankingMatchCntReader, times(3)).read();
    verify(dailyRankingGameMoneyReader, times(2)).read();

    // writer의 경우 chunk 당 한번 호출(여기선 갯수가 적어 스텝 당 한 번임)
    final ArgumentCaptor<Chunk> captor = ArgumentCaptor.forClass(Chunk.class);
    verify(dailyRankingWriter, times(2)).write(captor.capture());
    List<Chunk> chunks = captor.getAllValues();
    assertThat(chunks.size()).isEqualTo(2);
    assertThat(chunks.get(0).size()).isEqualTo(2);
    assertThat(chunks.get(1).size()).isEqualTo(1);
  }

그러면 given.. willReturn/willThrow 등 기존에 사용하던 mocking 함수를 사용할 수 있게 된다!!


참고

https://jojoldu.tistory.com/236

 

SpringBatch에서 ItemReader를 Mock객체로 교체하기

안녕하세요? 이번 시간엔 SpringBatch에서 ItemReader를 Mock객체로 교체하는 예제를 진행해보려고 합니다. 모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다. (공부한 내용

jojoldu.tistory.com

 

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
반응형
반응형

웹 페이지의 성능 최적화는 로딩최적화와 렌더링최적화 두 단계로 나뉜다.

로딩 최적화

브라우저 렌더링

5단계

  • 파싱
    • html을 해석해 DOM 트리 구축
    • html에 포함되어 있거나 리소스로부터 다운 받은 css를 해석해 CSSOM 트리 구축
  • 스타일 계산
    • DOM에 CSSOM 정보 매칭 -> render 트리 그림
      • script, meta, link 태그는 렌더링에 반영하지 않음
      • CSS로 감춘 노드 역시 렌더 트리에 포함하지 않음
  • 레이아웃(=리플로우)
    • 브라우저의 뷰포트 안에서 노드가 가져야 할 정확한 위치와 크기를 계산
    • 객체의 정확한 크기와 위치를 파악하기 위해 루트부터 노드를 순회하면서 계산
    • 레이아웃 결과로 각 노드의 정확한 위치와 크기값을 픽셀값으로 렌더 트리에 반영
  • 페인트
    • 레이아웃에서 계산된 값을 이용해 렌더트리의 각 노드를 화면 상의 실제 픽셀로 변환함
    • 위치와 관계 없는 CSS 속성들이 적용됨 (색상, 투명도 등)
  • 합성 & 렌더
    • 페인트 된 레이어들을 합성하여 스크린을 업데이트
    • CSS Transform 동작

상세: https://hacks.mozilla.org/2017/08/inside-a-super-fast-css-engine-quantum-css-aka-stylo/

 

로딩 최적화

로딩 최적화 기준: 브라우저

  • W3C Navigation Timing API - Processing Model: https://www.w3.org/TR/navigation-timing-2/
  • 전통적인 로딩 최적화의 기준이 됨.
  • DOMContentLoaded, Load 이벤트 시점이 빠르다 => 브라우저가 페이지에 포함된 리소스를 준비하는 것이 빠르다.

로딩 최적화 기준: 사용자

  • 같은 크기의 리소스, 같은 타이밍에 Load이벤트가 발생해도 얼마든지 사용자 입장에서 더 빨라 보이는 페이지가 있다.
  • 조금조금씩 보여주면 사람들은 기다릴 수 있다(https://web.dev/articles/critical-rendering-path?hl=ko)

관련 지표들

  • First Contentful Paint (FCP)
  • Largest Contentful Paint (LCP)
  • Speed Index (SI)
  • Interaction to Next Paint (INP)
  • Toatal Blocking Time (TBT)
  • Cumulative Layout Shift (CLS)

로딩, 렌더링 지표 비유

  • 뭔가 진행되고 있구나 : FP, FCP
  • 이제 원하는 내용을 읽을 수 있다 : LCP
  • 이제 얼추 동작하는구나 : FID, INP, TTI
  • FCP, LCP, INP를 향상시키는데 주요한 리소스 기준으로 최적화
  • 메인 섹션의 컨텐츠, 현재 경로의 내용등을 먼저
  • 메뉴, 배너, 공지사항, 분석툴, 다음 경로 캐싱등을 나중에

 

CSS 최적화

CSS : render blocking resource

  • 렌더 트리를 구성하기 위해서는 DOM 트리와 CSSOM 트리가 모두 필요함
  • DOM 트리는 순차적으로 구성될 수 있지만, CSSOM 트리는 전체 CSS 를 모두 해석해야 구성 가능 (캐스캐이딩 방식)
  • CSSOM 트리가 구성되기 전까지는 렌더 트리를 만들 수 없음
  • media 속성에 따라 Blocking을 피할 수 있음

최적화 가이드

  • CSS 는 항상 최상단 (head 영역)에 배치한다.
<head>
  <link href="style.css" rel="stylesheet">
</head>
  • media 쿼리를 올바르게 사용한다.
<link href="style.css"    rel="stylesheet">
<link href="style.css"    rel="stylesheet" media="all">
<link href="portrait.css" rel="stylesheet" media="orientation:portrait">
<link href="print.css"    rel="stylesheet" media="print">
  • @import 를 사용하지 않는다.
  • 경우에 따라 CSS 를 HTML에 인라인으로 포함시킨다.(네트워크 요청 수 줄이기)

 

자바스크립트 최적화

자바스크립트 : parser blocking resource

  • 자바스크립트는 DOM 과 CSSOM 을 동적으로 변경할 수 있음
  • 자바스크립트는 자신이 실행되기 직전까지의 DOM 트리에만 접근이 가능함
  • HTML 파싱 과정에서 script 태그를 만나면 스크립트 실행이 완료될 때까지 DOM 트리 생성이 중단됨
  • 외부 자바스크립트의 경우 모든 스크립트가 다운로드 된 후 실행될 때까지 DOM 트리 생성이 중단됨
  • 자바스크립트 실행은 CSSOM 이 완성될 때까지 중단됨

최적화 가이드

  • 자바스크립트는 항상 문서의 최하단 (</body> 직전) 에 배치
<body>
  <div>...</div>
  <div>...</div>
  <script src="app.js" type="text/javascript" />
</body>
  • 초기 렌더링에 쓰이지 않는 스크립트는 defer, async 속성을 명시하여 Blocking 을 방지
    • defer IE10 >= 지원
    • async는 순서보장 없음
    • defer는 순서보장(IE9에서는 순서보장안됨)
    • async vs defer attributes
<head>
    <link rel="preload" href="style.css" as="style">
    <link rel="preload" href="main.js" as="script">

    <link rel="stylesheet" href="style.css">
</head>
<body>
    ...
    <script src="https://google.com/analatics.js" type="text/javascript" defer />
    <script src="main.js"></script>
</body>

 

리소스 최적화

  • 브라우저, 사용자기준에서 모두 효과적
  1. 요청(Request) 줄이기
  2. 실제 리소스의 용량 줄이기

 

요청을 줄이는 방법

  1. 중복되거나 불필요 파일 제거
  2. 하나의 JS, CSS 파일 사용
    • webpack을 이용한 번들링
    • 단순한 concat도 의미가 있음
  3. HTML, CSS로 대체 가능한 이미지 제거(작은 이미지인 경우 base64 이미지도 방법)
  4. 이미지 Sprites
    1. 작은 이미지가 여러개 들어간 웹앱
    2. css의 backgound-position을 설정해 사용
    3. Webpack 사용자의 경우 webpack-spritesmith를 사용하여 이미지 합치기와 css 설정 등 자동화 가능

 

불필요 데이터 제거

  • CSS
    • 간결한 css selector 사용
    • 불필요한 css rule 제거
  • JS
    • 만능 util.js 정리
    • 오버스펙 라이브러리 지양
    • 파일에 포함된 sourcemap 제거
  • HTML 마크업 최적화
    • HTML을 단순하게 구성한다 (태그의 중첩을 최소화한다)
    • 공백, 주석 등을 제거한다

Minify (Uglify)

  • HTML, Javascript, CSS 모두 Minify 해서 사용
  • 불필요한 주석이나 공백등을 제거할 수 있음

 

<요약>

네트워크 리소스 최소화

  • 네트워크 요청의 개수를 줄인다 (Javascript 와 CSS 파일을 가능한한 합친다)
  • 아이콘이 많은 경우 이미지 스프라이트를 사용한다
  • HTML, CSS, Javascript 를 Minified 해서 사용한다

크리티컬 렌더링 패스 최적화

  • HTML 과 CSS 의 구조를 최대한 단순하게 만든다
  • CSS는 HTML 문서의 최상단에 배치한다
  • CSS의 media 타입을 정확하게 지정한다
  • 크리티컬 CSS는 인라인 시킨다
  • Javascript 는 HTML 문서의 최하단에 배치한다
  • 초기 로딩과 렌더링에 꼭 필요없는 Javascript는 async나 defer 속성을 사용한다

렌더링 최적화

DOM 조작으로 인한 렌더트리 변경

JavaScript

  • 자바스크립트 코드를 통해 동적으로 Style 속성 or 클래스명을 변경
  • CSS Animation (Transition) 등으로 인한 변경도 이 과정에 속함

Style 계산

  • 어떤 CSS 룰이 어떤 DOM 요소에 적용되어야 하는지를 계산하는 과정

Layout

  • 실제 그려질 좌표정보를 픽셀단위로 계산하는 과정
  • 자식이나 형제 요소들에게 영향을 줌

Paint

  • Layout 과정에서 정해진 영역에 픽셀을 채우는 과정

Composite

  • 각각의 분리된 레이어들을 합성하는 과정

 

리플로우와 리페인트

  • 리플로우는 전체 픽셀을 다시 계산해야 하기 때문에 부하가 큼
  • 리페인트는 실제 계산된 픽셀로 화면에 그리는 과정이기 때문에 주로 부하가 적음

DOM 트리 변경

  • DOM 추가 / 삭제
  • 리플로우 발생

위치나 사이즈 변경

  • CSS 속성 중 높이, 넓이, 위치 등에 영향을 주는 속성값 변경
  • height, width, left, top, font-size, line-height 
  • 리플로우 발생

색상이나 투명도 설정

  • CSS 속성 중 높이, 넓이, 위치 등에 영향을 주지 않는 속성값 변경
  • background-color, color, visibility, text-decoration 
  • 리페인트 발생

레이아웃 최적화 방법

CSS 규칙 수 최소화

  • 사용하는 규칙이 적을 수록 계산이 빠름
  • 복잡한 selector도 지양해야 함

DOM 깊이 최소화

  • 문서가 작고 얕을 수록 계산이 빠름

가능한한 하위 노드의 스타일을 변경

  • DOM 트리 상위 노드의 스타일을 변경하면 하위 노드에 모두 영향을 미치기 때문
  • 변경범위를 최소화할수록 레이아웃 범위가 줄어듦

숨겨진(display: none) 엘리먼트 수정

  • 숨겨진 엘리먼트를 변경할 경우에는 레이아웃이 동작하지 않음

영향 받는 엘리먼트 제한

  • 변경으로 인해 영향을 받는 엘리먼트를 제한 해야 함
  • ex: 콘텐츠로 인해 높이가 변경 될 경우 위 아래 위치한 엘리먼트들에 위치에도 영향을 줌
    • fixed 혹은 absolute position을 사용하여 영향을 받은 엘리먼트를 제한 해야 함

애니메이션

목표 : 60fps

  • 한 프레임에 대한 처리가 16ms 내로 완료되어야 함 (브라우저가 동작하는 시간을 고려하면 10ms 이내)

requestAnimationFrame() 사용

  • 브라우저의 프레임 속도 (60fps) 에 맞추어 애니메이션을 실행할 수 있도록 해줌(혹은 모니터 주사율에 맞추어준다)
  • 정확한 시간에 호출됨(프레임 시작 시 호출), setTimeout, setInterval은 프레임 종료 시 호출되므로 일정하지 않음
  • 현재 페이지가 보이지 않는 상태인 경우에는 렌더링이 발생하지 않도록 해 줌

position: absolute 처리

  • position을 absolute나 fixed로 설정하면 주변 레이아웃에 영향을 주지 않음
  • 애니메이션 영역이 주변 영역에 영향을 주지 않도록 할 것

transform 사용

  • position, width, height 등은 Layout 을 발생시킴
  • transform 은 Composite 만 발생시키기 때문에 훨씬 빠름 (GPU 사용)

구형 브라우저의 일괄처리

동일한 요소의 스타일을 여러 번 변경

const myelement = document.getElementById('myelement');

myelement.style.width = '100px';
myelement.style.height = '200px';
myelement.style.margin = '10px';
  • 3번의 리플로우가 예상됨

스타일 변경은 한 번에 모아서 처리

<style>
  .newstyles {
      width: 100px;
      height: 200px;
      margin: 10px;
  }
</style>

<script>
  const myelement = document.getElementById('myelement');
  
  myelement.classList.add('newstyles');
</script>
  • 클래스 사용으로 한 번으로 줄일 수 있음

그 외

  • DOM 요소 추가 시에도 appendChild 를 여러 번 하면 리플로우 여러 번 발생 -> innerHTML 사용
  • 최신 브라우저에서는 최적화 필요 없음

강제 동기 레이아웃

강제 동기 레이아웃 피하기

  • 스타일 변경 후 offsetHeight, offsetTop과 같은 계산된 값을 요청할 경우 강제로 레이아웃을 수행함
const tabBtn = document.getElementById('tab_btn');
const menuBtn = document.getElementById('menu_btn');

tabBtn.style.fontSize = '24px';

console.log(testBlock.offsetTop); // offsetTop 호출 직전 레이아웃

tabBtn.style.margin = '10px';
// 레이아웃
  • 레이아웃 이유
    • 계산된 값을 반환하기 전에 변경된 스타일이 계산 결과에 적용되어 있지 않으면 변경 이전 값을 반환하기 때문
  • 최신 브라우저에도 영향 받는 부분이므로 강제 동기 레이아웃을 하지 않도록 주의해야 함

레이아웃 스래싱(thrashing) 피하기

  • 많은 레이아웃을 연속적으로 빠르게 실행하는 경우 강제 동기 레이아웃에서 더 좋지 않은 결과를 나타냄
function resizeAllParagraphs(paragraphs) {
  for (let i = 0; i < paragraphs.length; i += 1) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}
  • 이 코드는 단락 그룹을 반복 실행하고 각 단락의 너비를 상자의 너비와 일치하도록 설정함
  • 반복문 안에서 style.width를 설정하고 box.offsetWidth를 호출함으로 인해 매 반복 시 마다 레이아웃 발생
  • 레이아웃이 대량 발생함

개선 방법

  • 박스 너비를 읽어오는 부분을 분기분 바깥 쪽에서 수행하면 레이아웃 스래싱을 막을 수 있음
function resizeAllParagraphs(paragraphs) {
  const width = box.offsetWidth;

  for (let i = 0; i < paragraphs.length; i += 1) {
    paragraphs[i].style.width = width + 'px';
  }
}

강제 동기 레이아웃을 일으키는 동작 정리

 

메모리 관리

  • 의도하지 않은 전역 변수 사용
  • 잊혀진 타이머
  • 참조된 DOM Node 삭제
  • 클로저 사용에 따른 누수

메모리 누수 사례 및 해결방법

의도하지 않은 전역 변수 사용

메모리 누수

  • 전역 함수 내부에서 선언문(ex: const) 없이 변수를 사용하는 경우 해당 변수는 전역에 선언됨
function foo(arg) {
    bar = "의도하지 않은 전역 변수";
}
  • 전역 변수에 값을 할당하면 해당 함수가 종료되더라도 전역에 값에 대한 참조가 남게 되어 메모리 누수 발생
function foo(arg) {
    this.bar = "이 경우도 전역에 선언됨";
}
foo();
  • 전역 함수 선언문 안에서 this를 사용하면 전역에 선언됨

해결 방법

  • use strict 사용하여 의도 되지 않은 전역 변수를 미연에 방지

잊혀진 타이머

메모리 누수

  • Javascript에서 타이머로 사용하는 setInterval 함수는 일반적으로 아래의 코드와 같이 사용함
let someResource = getData();
const interval = setInterval(function() {
    const someNode = document.getElementById('some-node');
    if(someNode) {
        someNode.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
  • someNode 엘리먼트(id : 'some-node')가 유효할 경우에는 문제가 없음
  • 그러나 someNode 엘리먼트가 삭제될 경우 Interval 함수에서 사용하는 someResource는 불필요 함에도 불구하고 메모리 상에 남아있음

해결 방법

  • someNode가 삭제 될 경우 타이머 자체를 중단하여 불필요 메모리를 해제함
someResource = null; // IE7 이하 브라우저
clearInterval(interval);

참조된 DOM Node 삭제

메모리 누수

const elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};

function doStuff() {
    elements.image.src = 'http://some.url/image';
    elements.button.click();
    console.log(elements.text.innerHTML);
    // ...
}

function removeButton() {
    document.body.removeChild(document.getElementById('button'));
}
  • removeButton()을 수행할 경우 '#button'은 삭제되지만 '#button'에 대한 참조(elements.button)는 남아 있게 됨
  • 이 때문에 '#button'은 GC되지 않음

해결 방법

  • removeButton() 함수에서 #button 삭제 시 참조도 같이 삭제함
function removeButton() {
    document.body.removeChild(document.getElementById('button'));
    elements.button = null;
}

이중 클로저로 인한 메모리 누수

  • 이중 클로저 사용 시 메모리 누수가 발생할 수 있음
var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) {
      console.log("hi");
    }
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};
setInterval(replaceThing, 1000);
  • 매번 타이머 실행시 마다 새로운 함수가 실행
  • 과정 설명
    1. setInterval 1회 실행
      • [fn1]originalThing = null;
      • [fn1]theThing = {...};
    2. setInterval 2회 실행
      • [fn2]originalThing = [fn1]theThing;
      • [fn2]theThing = {...};
    3. setInterval 3회 실행
      • [fn3]originalThing = [fn2]theThing;
      • [fn3]theThing = {...};
    4. ...
    • 전제 조건
      • 실행 함수를 횟수에 따라 다음과 같이 표기하고 변수와 값에 prefix로 붙임
        • 첫번째 실행 함수 : [fn1]
        • 첫번째 실행 함수의 변수 a : [fn1]a
  • 타이머 실행으로 새로운 함수가 실행될 때 마다 이전 함수의 변수를 참조함

이중 클로저 메모리 누수 해결 방법

  • 타이머와 같이 반복적으로 매번 실행되는 함수에서 클로저 사용 시 이전 실행 함수의 변수를 참조하지 않도록 주의해야 함

누수 확인: 크롬 개발도구 > Memory 탭

  • Heap Snapshot
    • 페이지의 자바스크립트 객체와 관련된 DOM 노드 사이의 메모리 분포를 보여줌
    • 스냅샷끼리 비교해서 메모리 누수를 찾아낼 수 있음
  • Allocation instrumentation on timeline
    • 시간의 흐름에 따라 메모리 누수를 확인할 수 있음
  • Allocation sampling
    • 메모리가 할당된 Javascript 함수를 보여줌
728x90
반응형
반응형

MACAddress

EthernetSwitching

IPAddress

StaticRouting

DynamicRouting

 

transport layer L4 port정보; TCP, UDP

internet layer L3

network access layer L2

 

 

 

 

 

 

 

 

전체 ip?   2^32 개

공인 ip와 사설 ip를 나누는 이유: 공인 ip갯수 초과

cdn(contents delivery network)

 

broadcast: 목적지를 가지지 않고 통신; ARP request

bum traffic: unicast + multicast + unknown unicast(ARP reply 시 목적지 맥이 없을 때 발생)

 

스위치

  • 여러개의 물리적 랜에 걸쳐서 존재할 수 있는 논리적 브로드 캐스트 도매인을 나누는 것

VLAN

  • L2를 논리적으로 분리하는 기술
  • L2안에서 다른 VLAN간 통신할 수 없음
  • vlan별로 mac address를 분리 

 

ARP(address resolution protocol)

  • ip는 알지만 mac address를 모를 때 사용하는 프로토콜
  • mac address를 알아야 packet을 완성할 수 있다(헤더를 완성할 수 있다)
  • 브로드캐스트로 묻고(request) 유니캐스트로 답변(reply)
  • targetIp
  • 해당하는 서버 아니면 패킷 드랍
  • reply 못 받으면 engress buffer에 저장
    • 몇 초 기다리다 clear시킬 수 있음..(하드웨어 별 다름)

 

같은 네트워크? 앞에 3덩어리까지 같으면 같은 네트워크

 

ip가 왜 나왔을까? 맥이 고유값이면 ip가 필요없지 않나

ip는 2^32개 만큼 사용 가능 

국가별 대역이 있음; ip routing하기 위해서; 빨리 처리하기 위해서

mac address는 고유하지 않다!

윈도우는 윈도우 전용(MS가 만든) 더 간편한 방법이 있음 하지만 표준은 아님

 

 

untag가 비효율적이라 도입한 방식이 tag

hypervisor switch는 태그 값

 

같은 대역은 맥으로 통신한다. 맥테이블은 L2; 같은 VLAN

라우팅 태이블을 보지 않아도 된다(게이트웨이까지 안가도 된다); 최단거리

대역이 다른면 게이트웨이로 보내고 해당 대역대의 스위치가 dst의 맥 정보 모르면 스위치가 ARP request 보내고 학습한 후 원래 패킷 보냄

다른 대역간은 라우터 통신

 

라우팅?

  • 데이터를 출발지에서 목적지까지 가는 최적의 경로를 설정해주는 과정
  • 정적 라우팅: 엔지니어가 수동으로 경로 지정
  • 동적 라우팅: 라우터가 다른 라우터들과 경로를 주고 받아 best selection으로 자동으로 하게끔
    • best selection에 해당하는 중간에 라우터가 죽으면 
    • 다른 경로로 감(hidden routing)
  • next hop: 다음 가야할 지점
  • 맥 어드래스는 one hop by hop으로 맥이 세팅됨(경유지의 맥이 박힘)
    • 맥만 까지고 encap decap 반복 나머지는 그대로 
    • L2헤더를 깨서 버리고 다시 조립(맥 변경)
    • L3헤더의 정보는 변하지 않는다(IP 불변)

 

NAT

세션

  • TCP 연결되면 "세션이 맺어졌다"..
  • handshaking 시도 시 1800초(30분) 동안 아무 소식이 없으면 "세션 끊어짐(flushed)"
  • 브라우저 끌 때도 세션을 꺼야함! (4way hand shake) FIN은 호스트 a, b건 어디서든 가능
  • 한 컴터에서 세션의 갯수는 정해져 있음
    • 브라우저 여러개 띄우면 세션 테이블안에서 찾는 시간도 소요됨
  • 통신 시 first packet은 반드시 syn 이어야 함; 아니면 드랍
  • 라우터와 각 서버와의 타임아웃 값이 다르면 한쪽만 세션이 끊겨서 세션이 드랍될 수 있어서 맞추는게 중요
  • tcp 세션은 기본적으로 30분(timeout)_ nhn cloud의 경우
  • 세션을 늘리는 것에 방어적인 이유는 세션이 쌓여서 장애를 발생할 수 있음

 

세션 테이블

  • 테이블에 매치되는 세션이 있는가
  • 없으면 세션 만든다

 

NAT 

  • 인터넷 나갈 때 공인 ip로 바꿔줌
  • 24bit/22bit단위로 대역별로 nat ip를 쪼개서 운영(맨 마지막 ip split 덩어리)
    • 이런 세션이 65538개 생기면 하단의 소스를 인지할 수 있는 기준이 사라지게 됨
  • 같은 L3에 묶인 여러 ip가 하나의 NAT ip로(공인) 변환되어 나가는데
  • reply 받을 때 처음 받은 source port를 reuse하기 때문에 해당 값으로 각각이 누구로 부터온 응답(어떤 세션인지)인지 분류 가능
  • 회사별로 소스포트 range가 정해져 있음(reserve)

 

Loop network

  • ttl(time to live): max로 넘어갈 수 있는 hop의 카운트; 라우팅(L3)에만 해당됨
  • 여기는 L2(스위치)라서 ttl 카운트와 상관없이 계속 살아 있고 loop이 발생
    • L4(로드발랜싱)

 

이중화

모든 네트워크는 이중화

VRRP: 게이트웨이를 이중화

 

 

 

728x90
반응형
반응형

환경: springboot 2.7.6, mysql 8

 

@Id로 엔티티의 primitive key를 명시할 때 꼭 wrapper 클래스로 작성해야 하는지에 대한 의문이 생겼다.

wrapper클래스의 경우 null을 허용하는데, 엔티티를 저장할 때 키 값이 없으면(null) 자동 생성해 주는 기능 때문에 0이 의도치 않게 들어가면 이슈가 있을 수 있다고 생각했다.

하여 정리해본다.

 

id의 경우 아래와 같은 케이스가 있다.

  • select 시: id는 not null이기 때문에 굳이 wrapper를 쓸 일이 없음
  • insert 시:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long seq;
  • jpa의 @GeneratedValue(strategy = GenerationType.IDENTITY)가 선언된 id 콜롬 & 디비에서 AUTO_INCREMENT 설정된 경우
    • 해당 경우는 디비의 설정을 사용한다는 뜻으로 mysql에 설정된 seq bigint(20) NOT NULL AUTO_INCREMENT COMMENT처럼 AUTO_INCREMENT 옵션이 들어간 경우 1부터 저장하기 때문에 0은 무시
  @Id
  private long seq;
  • @GeneratedValue 가 선언되지 않은 id 콜롬 & 디비에서 AUTO_INCREMENT 설정된 경우
    • 0 자체가 null과 동일하게 auto increment를 생성하라는 의미이기 때문에 0이 저장되지 않고 다음 sequence가 저장됨, AUTO_INCREMENT 옵션이 들어간 경우 1부터 저장하기 때문에 0은 무시
  • @GeneratedValue 가 선언되지 않은 id 콜롬 & 디비에서 AUTO_INCREMENT 설정되지 않은 경우
    • 0이 저장됨

참고로 mysql 기본 설정은 AUTO_INCREMENT일 때 0은 제외한다.
sql_mode항목에 NO_AUTO_VALUE_ON_ZERO 값이 설정되어 있으면 auto increment 시 0을 추가한다.

각자 디비에 어떻게 설정되어 있는지는 아래 쿼리로 확인 가능하며

SHOW VARIABLES LIKE 'sql_mode';

혹시.. 0을 의미 있는 값으로 세팅하려거든 아래처럼 하면 된다.

SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';

 

즉, id값을 primitive로 사용할지 wrapper로 사용할지에 대한 판단은, db에서의 0에 대한 설정과 값의 null/0 여부를 파악 후 결정하는 게 좋겠다

728x90
반응형
반응형

0125024852  0125024850 0125024851

친구코드: 0125024852  0125024850 0125024851

추천번호: 0125024852  0125024850 0125024851

 

최장 6개월에 최대 6%임 소소한 돈으로 쏠쏠하게 ㄱㄱ!

인당 3개까지 가입해서 코드가 3개입니다!! 연달아 가입했더니 코드도 순서대로네요 ㅋ_ㅋ

https://obank.kbstar.com/quics?page=C020722&boardId=669&compId=b058336&articleId=121532&bbsMode=view&viewPage=1&articleClass=2&searchCondition=title&searchStr=

 

728x90
반응형
반응형

collation 이란

데이터베이스의 문자열 Datatype(CHAR, VARCHAR, TEXT 등)에는 캐릭터 셋(Character set)과 콜래이션(Collation)이라는 속성이 있다.

캐릭터 셋(Character set)은 각 문자가 컴퓨터에 저장될 때 어떻게 저장될지(encoding)에 대한 규칙의 집합이고,

콜래이션(Collation)은 특정 캐릭터 셋(Character set)에 의해

  • 데이터베이스에 저장된 값들을 비교 검색(where clause)하거나
  • 문자들을 서로 정렬(order by) 등의 작업을 위해 비교할 때
  • 그리고 인덱싱을 할 때

사용하는 규칙들의 집합을 의미한다.

예를 들어 

  • int형은 123 < 345 으로 명확히 비교할 수 있고
  • date형은 2013-01-01 < 2022-01-01로 명백하나

문자열의 경우

  • '가'와 '나' 중 어느 것이 큰지
  • 'a'와 'A' 중 어느 것이 큰지 
  • '가'와 'ㄱㅏ'는 어떻게 비교해야 하는지

혼란스럽다. 이와 관련하여 정리된 방식이 collation이라고 생각하면 된다.

 

대표적인 collation 타입

  • utf8mb4_bin
    • binary 저장 값으로 정렬; 각 문자를 byte 취급하여 byte 값을 비교(언어적인 규칙이 고려되지 않음)
    • A는 41, a는 61이기 때문에 오름차순 정렬 시 A~Z 다음 a~z가 정렬된다.
      • SELECT HEX(WEIGHT_STRING('A' collate utf8mb4_bin)) as 'A',  HEX(WEIGHT_STRING('a' collate utf8mb4_bin)) as 'a'  FROM dual;
  • utf8mb4_general_ci
    • 간단하고 빠르게 사용할 수 있는 타입
    • 모든 유니코드가 고려된 건 아니지만 일반적으로 많이 사용됨
      • 유니코드 중 Basic Multilingual Plane (BMP)를 벗어나면 정렬이 틀리게 될 수 있음
      • 하지만 중국어(C), 일본어(J), 한국어(K) 통칭 CJK는 BMP에 포함되어 있어 국내에서도 잘 쓰이는 타입
    • case insensitive로 A는 41, a도 41로 같기 때문에 A와 a가 혼용되어 정렬된다.
      • SELECT HEX(WEIGHT_STRING('A' collate utf8mb4_general_ci)) as 'A',  HEX(WEIGHT_STRING('a' collate utf8mb4_general_ci)) as 'a'  FROM dual;
  • utf8mb4_unicode_ci
    •  모든 유니코드를 고려한 정렬 규칙으로 고려하는 규칙 자체가 많아 utf8mb4_general_ci 방식보다 느림
      • 한국어, 영어, 중국어, 일본어 사용 환경에서는 utf8mb4_general_ci와 동일한 결과를 냄
      • 더 특수한 문자의 정렬 순서는 달라질 수 있음
    • case insensitive로 A는 0E33, a도 0E33으로 같기 때문에 A와 a가 혼용되어 정렬된다.
      • SELECT HEX(WEIGHT_STRING('A' collate utf8mb4_unicode_ci)) as 'A', HEX(WEIGHT_STRING('a' collate utf8mb4_unicode_ci)) as 'a'  FROM dual;

 

간단하게 collation 타입 읽는 법

ex. utf8mb4_0900_ai_ci

  • utf8mb4 : 문자 하나당 1~4byte 할당(mb4 : 4byte 지원), 바로 이어서 지역 및 언어를 나타내는 단어로 세분화되기도 함
    • utf8mb3는 3byte가 할당되는 방식으로 mysql8에서 deprecated
  • 0900 : Unicode Collation Algorithm (UCA) version 9.0 표준을 따른다는 뜻
    • mysql8 추가; 더 세분화된 정렬법 적용
  • ai : accent insensitive (이전버전에서는 악센트 구분이 안되었으며 MySQL 8.0부터 추가됨)
  • ci : case insensitive (대소문자 구분하지 않음)
 

그 외 아래와 같은 표기법이 사용될 수 있음

Suffix
Meaning
비고
_ai
Accent-insensitive
mysql8 추가
_as
Accent-sensitive
mysql8 추가
_ci
Case-insensitive
 
_cs
Case-sensitive
 
_ks
Kana-sensitive
mysql8 추가
일본어 히라가나-가타카나 같게할지 여부
_bin
Binary
 
  • ​MySQL 8.0.1 버전부터 utf8mb4_0900_ai_ci이 기본값임

 

mysql8부터 추가된 collation type의 pad attribute

  • 이전 버전에서는 PAD SPACE를 사용하던 것과 다르게 NO PAD 속성이 생김
    • 기본 값은 PAD SPACE
  • NO PAD 속성은 문자열 끝에 빈 문자열이 있는 경우에 문자열 비교 시 공백까지 포함하여 비교함(공백도 의미를 가진다는 전제)
  • 따라서 정렬 시 의도한 대로 정렬이 되지 않을 수 있으니 사용하고 있는 collation type이 어떤 pad attribute를 갖는지 확인해야 함

 

그냥 접속 시 매번 collation이 다르게 접속될 수 있음

  • jdbc 커넥터 버전에 따라 커넥션 별 collation이 달라질 수 있음
  • 워크벤치에서도 지정하지 않으면 외부 영향을 받아 달라질 수 있음
  • 따라서 커넥터 레벨에서 collation을 지정하거나 접속 시 명령어에 아래 내용 추가하여 항상 고정된 collation으로 붙을 수 있도록 하는 게 좋음
-- Set the character set and collation for the session
SET NAMES utf8mb4 COLLATE utf8mb4_general_ci;

 

collation이 다르게 접속이 될 경우 데이터 저장 시 에러 발생할 가능성 있음

문자열을 기준으로 다음 작업을 할 때 아래 에러가 발생할 수 있음:

  • 테이블을 join 하거나
  • 값을 비교, 필터링하거나
  • string 연산(concat 등)을 할 때
오류 코드: 1267Illegal mix
of collations (utf8_general_ci, IMPLICIT) and (utf8_unicode_ci, IMPLICIT) for
operation '='

이 경우 내 로컬(커넥션) 설정과 서버의 collation 설정이 다르거나 테이블, 콜롬 등에 설정된 collation이 상이하기 때문으로 collation에 대해서 확인 필요

그럴리는 없겠지만 혹시 테이블, 콜롬별 collation 설정이 다르다면 아래와 같이 join 조건에 collate 타입을 명시해야 한다.

SELECT *
FROM table1
JOIN table2 ON table1.name = table2.description COLLATE utf8_general_ci;

 

지금 내가 사용하는 디비에서 collation 값 조회하는 방법

  • mysql8
show variables where variable_name like '%collation%';

Variable_name                |Value             |
-----------------------------+------------------+
collation_connection         |utf8mb4_0900_ai_ci|
collation_database           |utf8mb4_general_ci|
collation_server             |utf8mb4_general_ci|
default_collation_for_utf8mb4|utf8mb4_0900_ai_ci|


#전체 값 확인
SHOW COLLATION WHERE Charset = 'utf8mb4';


Collation                 |Charset|Id |Default|Compiled|Sortlen|Pad_attribute|
--------------------------+-------+---+-------+--------+-------+-------------+
utf8mb4_0900_ai_ci        |utf8mb4|255|Yes    |Yes     |      0|NO PAD       |
utf8mb4_0900_as_ci        |utf8mb4|305|       |Yes     |      0|NO PAD       |
utf8mb4_0900_as_cs        |utf8mb4|278|       |Yes     |      0|NO PAD       |
utf8mb4_0900_bin          |utf8mb4|309|       |Yes     |      1|NO PAD       |
utf8mb4_bg_0900_ai_ci     |utf8mb4|318|       |Yes     |      0|NO PAD       |
...생략
  • mysql5
show variables where variable_name like '%collation%';

Variable_name       |Value             |
--------------------+------------------+
collation_connection|utf8mb4_general_ci|
collation_database  |utf8mb4_general_ci|
collation_server    |utf8mb4_general_ci|


#전체 값 확인
SHOW COLLATION WHERE Charset = 'utf8mb4';

Collation             |Charset|Id |Default|Compiled|Sortlen|
----------------------+-------+---+-------+--------+-------+
utf8mb4_general_ci    |utf8mb4| 45|Yes    |Yes     |      1|
utf8mb4_bin           |utf8mb4| 46|       |Yes     |      1|
utf8mb4_unicode_ci    |utf8mb4|224|       |Yes     |      8|
utf8mb4_icelandic_ci  |utf8mb4|225|       |Yes     |      8|

 

collation 변경 방법

1) 데이터베이스 레벨

ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

2) 테이블 레벨

ALTER TABLE my_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ;

3) 콜롬 레벨

ALTER TABLE mytable MODIFY name VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci;

4) 세션 레벨

SET collation_connection = 'utf8mb4_general_ci' ;

5) 쿼리 레벨(insert문 동일)

SELECT 
    @_now := now(),
    @ver := '1.17.0' collate utf8mb4_general_ci , 
    @domain := 'my domain' collate utf8mb4_general_ci , 
    @port_ := '1234'
;

6) 서버 레벨

[mysqld]
character-set-server=latin1
collation-server=latin1_swedish_ci

 

결론

  • 같은 문자셋이라도 콜레이션에 따라 영어의 경우 대소문자의 구분, 일본어의 경우 히라가나와 가타카나의 구분, 한글 자음과 결합문자를 구분하는 방법 등이 달라짐
  • 관련해서 정렬 시 정확도와 검색 속도에 영향이 있음
  • MySql 5-> 8로 올릴 때 collation 설정 값이 정렬 등에 영향을 줄 수 있다는 것을 인지할 필요 있음
  • MySQL 8.0의 기본 collation 인 utf8 mb4_0900_ai_ci는 utf8이며 글자당 4byte까지 저장하고, 0900 버전의 UCA 규칙을 따르며 accent, 대소문자, 히라가나와 가타카나, 한글 자음과 결합문자를 구분하지 않음

관련 상세 내용은 버전별 공식 문서를 확인하자

https://dev.mysql.com/doc/refman/8.4/en/charset.html

 

MySQL :: MySQL 8.4 Reference Manual :: 12 Character Sets, Collations, Unicode

MySQL 8.4 Reference Manual  /  Character Sets, Collations, Unicode Chapter 12 Character Sets, Collations, Unicode MySQL includes character set support that enables you to store data using a variety of character sets and perform comparisons according to a

dev.mysql.com

https://dev.mysql.com/doc/refman/5.7/en/charset.html

 

MySQL :: MySQL 5.7 Reference Manual :: 10 Character Sets, Collations, Unicode

MySQL 5.7 Reference Manual  /  Character Sets, Collations, Unicode Chapter 10 Character Sets, Collations, Unicode MySQL includes character set support that enables you to store data using a variety of character sets and perform comparisons according to a

dev.mysql.com

관련 내용 테스트 한 블로그

https://blog.naver.com/sory1008/223071678680

728x90
반응형

'개발 > sql' 카테고리의 다른 글

[mysql] delete, drop, truncate  (0) 2024.10.02
[mysql] basic functions  (0) 2024.09.09
DB isolation level  (0) 2024.05.22
[mysql] merge into..?  (0) 2024.05.17
[mysql] 유저의 등수 구하기 rank under v8  (0) 2024.02.06
반응형

(DB가 아닌) 애플리케이션 단 락에 대해 알아본다.

2023.01.12 - [개발/spring] - [jpa] lock종류와 사용 시 주의사항

 

Optimistic Locking 낙관적 락

  • @Version 어노테이션 사용(엔티티 당 하나)
  • 최초 커밋만 인정하는 방법
  • entity에 바로 락을하는 것이 아니라 버전 넘버를 저장하는 방식
    • 초기 버전 값은 0
  • 저장할 때 버전 넘버가 다르거나(해당 row못찾음) 0이면 롤백하고 에러 발생
  • intIntegerlongLongshortShortjava.sql.Timestamp
    • we can also use other approaches, such as timestamps, hash value computation, or serialized checksum.
  • 버전 애트리뷰트는 엔티티를 통해 읽을 수 있지만 절대 개발자가 직접 업데이트하거나 증가시켜선 안된다
    • 벌크 연산은 버전을 무시하기 때문에 벌크 연산을 할 경우 버전 필드를 직접 증가시켜야 한다
@Version
@UpdateTimestamp
@Column(nullable = false)
private LocalDateTime updated;

 

Pessimistic Locking 비관적 락

  • DB에서 제공하는 락기능을 사용
  • entity에 접근하는 순간 락이 걸림
  • 트랜잭션끼리의 충돌이 발생한다고 가정하고 우선 락을 거는 방법
  • @Lock 어노테이션 사용
  • 락이 실행할 때 transaction이 없으면 에러 발생(transactionRequiredException)
  • 락을 바로 얻을 수 없으면 LockTImeoutException 던짐
    • timeout 설정 필요: 락을 잡고 있는 최대 시간
      • QueryHint 이용; javax.persistence.lock.timeout; 단위 ms
    • 모든 DBMS가 지원하는 건 아님
  • If the time it takes to obtain a lock exceeds the value of this property, a LockTimeoutException will be thrown, but the current transaction will not be marked for rollback.

 

@Lock(LockModeType.PESSIMISTIC_READ)
@QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")})
public Optional<Customer> findById(Long customerId);

 

 

LockModeType

public enum LockModeType {
  READ,
  WRITE,
  OPTIMISTIC,
  OPTIMISTIC_FORCE_INCREMENT,
  PESSIMISTIC_READ,
  PESSIMISTIC_WRITE,
  PESSIMISTIC_FORCE_INCREMENT,
  NONE;

 

optimistic lock 관련

  • NONE
    • 락 모드를 적용하지 않아도 엔티티에 버전 애트리뷰트가 있으면 Optimistic Locking이 적용된다
    • Second Lost Update Problem을 예방할 수 있다
    • 조회한 엔티티를 수정할 때 버전을 체크하면서 버전을 증가한다(UPDATE 쿼리 사용)
    • 이때 데이터베이스의 버전 값이 현재 버전이 아니면 예외 발생
  • OPTIMISTIC
    • NONE 을 사용하면 엔티티를 수정해야 버전을 체크하지만 OPTIMISTIC을 사용하면 엔티티를 조회만 해도 버전을 체크한다
    • 쉽게 얘기하면 한 번 조회한 엔티티는 트랜잭션을 종료할 때까지 다른 트랜잭션에서 변경하지 않음을 보장한다
    • dirty read와 non repeatable read를 방지한다
    • 트랜잭션을 커밋할 때 버전 정보를 조회해서(SELECT 쿼리 사용) 현재 엔티티의 버전과 같은지 검증하고 같지 않으면 예외가 발생한다.
  • OPTIMISTIC_FORCE_INCREMENT
    • Optimistic Locking을 사용하면서 버전 정보를 강제로 증가한다
    • 논리적인 단위의 엔티티 묶음을 관리할 떄 사용한다
    • 엔티티를 수정하지 않아도 트랜잭션을 커밋할 때 UPDATE 쿼리를 사용해 버전을 강제로 증가시킨다
    • 이때 데이터베이스의 버전이 엔티티 버전과 다르다면 예외가 발생한다
    • 추가로 엔티티를 수정하면 수정 시 버전 UPDATE가 발생한다 따라서 총 2번의 버전 증가가 나타날 수 있다

persistent lock 관련

  • LockModeType.PESSIMISTIC_WRITE
    • 일반적인 옵션. 데이터베이스에 쓰기 락
    • 다른 트랜잭션에서 읽기도 쓰기도 못함. (배타적 잠금)
  • LockModeType.PESSIMISTIC_READ
    • 잘 사용하지 않음 
    • 반복 읽기만 하고 수정하지 않는 용도로 락을 걸 때 사용
    • 다른 트랜잭션에서 읽기는 가능함. (공유 잠금)
  • LockModeType.PESSINISTIC_FORCE_INCREMENT
    • Version 정보를 사용하는 비관적 락
    • 버전 정보를 강제로 증가시킴

jpa에 위와 같이 이름으로 ForUpdate 줘도 만들어지는 쿼리는 select for update로 나가게 된다.

728x90
반응형
반응형

springframework에서 제공하는 spring retry에 대해 알아보자

maven repo: https://mvnrepository.com/artifact/org.springframework.retry/spring-retry

 

spring aspect 라이브러리를 사용할 수도 있는데, 해당 라이브러리는 springboot starter에 있을 수 있기에 한번 확인하고 넣는 게 좋다.

springboot2.7.3 기준 5.3.24 버전이 들어가 있다.

 

관련 어노테이션을 사용하려면 아래처럼 enable 시켜줘야 한다.

import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;

@Configuration
@EnableRetry
public class AppConfig {
}

실행 원리는 proxy!!

 

재시도를 해야 하는 함수에 @Retryable을 달아주고

재시도 시 실행해야하는 함수에 @Recover를 달아준다.

import org.springframework.retry.annotation.Recover;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @Retryable(value = { SomeTransientException.class }, maxAttempts = 3)
    public void performTask() throws SomeTransientException {
        // Your logic here
        System.out.println("Trying to perform task...");
        throw new SomeTransientException("Temporary failure");
    }

    @Recover
    public void recover(SomeTransientException e) {
        // Recovery logic here
        System.out.println("Recovering from failure: " + e.getMessage());
    }
}

api에러 뿐만 아니라 아래와 같이 디비 에러에도 가능. IO에러 등등 많이 커버한다.. @Retryable에서 throw 하는 에러 타입과 @Recover에서 받는 에러타입이 반드시 같아야 한다.

@Service
public interface MyService { 

    @Retryable(retryFor = SQLException.class)
    void retryServiceWithRecovery(String sql) throws SQLException; 

    @Recover
    void recover(SQLException e, String sql); 
}

Retryable 어노테이션에는 어떤 exception이 발생할 때 재시도를 할지와 최대 몇 번 실행할지를 지정해 줄 수 있다.

  • maxAttempts: 실패가 n번 나면 recover 함수를 실행한다.
    • default: 3
  • backoff: 재실행하고 n초 쉬었다가 재실행(단위: ms)
    • default:  a fixed delay of 1000ms
  • multiplier: 재시도와 재시도 사이의 시간 간극이 n배로 점점 멀어짐
    • default: 0(무시)
  • BackOffPolicy: 아래처럼 설정 값들에 따라 재시도 간격이 달라진다.
    • With no explicit settings the default is a fixed delay of 1000ms
    • Only the delay() set: the backoff is a fixed delay with that value
    • When delay() and maxDelay() are set the backoff is uniformly distributed between the two values
    • With delay(), maxDelay() and multiplier() the backoff is exponentially growing up to the maximum value
    • If, in addition, the random() flag is set then the multiplier is chosen for each delay from a uniform distribution in [1, multiplier-1]

 

위 값들은 클래스에서 상수 값으로 관리할 수 있지만 공용사용과 환경별 관리를 위헤 프로퍼티 파일로 뺄 수도 있다.

이때 해당 변수 명이 달라지니(ex. maxAttemps -> maxAtempsExpression) 반드시 공식문서를 참고해야 한다!

https://docs.spring.io/spring-retry/docs/api/current/org/springframework/retry/annotation/Retryable.html

 

공통 설정 혹은 더 복잡한 설정을 위해 java config로 설정할 수도 있다.

@Configuration
@EnableRetry
public class RetryConfig {


    @Bean
    public RetryTemplate retryTemplate(){
        RetryTemplate retryTemplate = new RetryTemplate();

        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(3000l);
        
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

        Map<Class<? extends Throwable>, Boolean> includeExceptions = new HashMap<>();
        includeExceptions.put(CannotAcquireLockException.class, true);

        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(5, includeExceptions);
        retryTemplate.setRetryPolicy(retryPolicy);

        return retryTemplate;
    }
    
}

 

참고로 delay는 간격은 랜덤으로도 줄 수 있다.

@Retryable(
    value = {CannotAcquireLockException.class},
    maxAttempts = 4,
    backoff = @Backoff(random = true, delay = 1000, maxDelay = 5000, multiplier = 2)

 


기본설명:

https://www.baeldung.com/spring-retry

좀더 복잡하게 사용하기:

https://medium.com/@AlexanderObregon/using-springs-retryable-annotation-for-automatic-retries-c1d197bc199f

https://medium.com/@vmoulds01/springboot-retry-random-backoff-136f41a3211a


이럴 수가.. 적으면서 이거 전에 본적 있는데 싶었는데 무려 정리를 했던 적이 있던 것ㅋㅋㅋㅋ

2022.02.04 - [개발/spring] - [retry] spring-retry for auto retry

 

[retry] spring-retry for auto retry

spring-retry? exception이 난 작업에 대해 자동으로 재시도 해주는 스프링 라이브러리이다. 일시적인 connection error 등에 유용하게 사용할 수 있다. 환경: springboot2.6.2 implementation 'org.springframework.retry:spri

bangpurin.tistory.com

 

728x90
반응형

+ Recent posts