728x90

업무에서 Spring Batch로 세미나를 진행하고, 앱 푸시 기능을 배치로 전환하는 작업을 진행했다.
여기에 저장하면서 글로만 보던 것들을 직접 경험해보면서 겪었던 일들을 기록하려고한다.

첫번째 에러

우리 푸시 배치 서버의 구조는 스프링 스케줄러 서버에서
푸시 서버의 api를 호출해서 해당 job들을 돌려주는 방식으로 구성이 되어있다.

물론 이 부분을 새롭게 개편해야 하는것은 맞다ㅋㅋㅋ

그래서 특정 시간이 되면 해당 job api로 호출을 하는데
여기서 대략 총 데이터가 100,000건 정도 되는데 전부 동기 + 블록킹처리로 진행했다.
그래서 스케줄러가 api를 쏘고 요청값이 최대 오래걸려도 limit을 30분을 잡았었다.
그런데 100,000건의 데이터를 여러 로그를 쌓고, 푸시를 하는데까지 1시간이 넘게 걸렸었다.
그래서 1시간이 지나도 받지 못하는 상황에 에러가 나서 해당 스케줄러가 돌다가 실패가 되었다. (근데 뒤에서의 푸시 서버는 계속 돌고있었다)

그러니 다시 정리해보면 1시간이 넘는 시간동안 작업을 하고 있던것이다.

-> 이부분에서 나도 대기중인 데이터만 뽑는 쿼리를 작성해야했는데 실수로 보낸 데이터까지 조회하게끔 만들었다.

Mono 객체를 block()을 사용해서 푸시를 진행했기 때문에, 블록킹 방식으로 동작하여

제어권도 아예 넘겨버려서 끝이나야 다음 작업을 수행하는 형태로 진행되서 굉장히 느렸었는데,

이방식을 subscribe() 방식으로 바꿔서 논블록킹으로 푸시 발송 명령만주고 다음 작업을 진행하게끔 해서

많이 속도를 줄일 수 있었다.
-> 이부분은 조만간 다른 포스팅에서 자세하게 다룰 예정이다.

찾은 부분

SEND, WAIT 두 상태의 데이터를 모두 가져오고 나서 SEND로 업데이트를 치고 있던 것이다.

이부분이 일단 성능 저하의 첫번째 원인이라 생각했고,
그리고 스케줄러는 다른 스케줄링들도 가지고 있으니, 푸시 서버에서 요청을 바로 돌려주고 해당 Job은 뒷편에서 실행해주는 것이 맞다고 생각했다.
그래서 이 두부분을 고쳐보았다.

  • 쿼리는 WAIT상태만 추출하여 갖고 있는다.
  • 푸시서버는 기본적으로 제공해주는 JobLauncher를 배제한다.
@RestController
@RequiredArgsConstructor
public class Demo {
    private final BasicBatchConfigurer basicBatchConfigurer;

    @PostMapping("/demo/push")
    public String appPushJobLauncher() throws Exception {
        SimpleJobLauncher jobLauncher = (SimpleJobLauncher) basicBatchConfigurer.getJobLauncher();
        jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());

        // jobLauncher 실행 로직....
    }
}

이렇게해서 요청을 바로 수행후 return값을 먼저 돌려주었다.
이렇게 해서 스케줄러의 동기처리로 늦어졌던 것에 대해서 일단락 짓게 되었고,
1시간 이내로 처리가 되게 되었다.

두번째 에러

두번째 에러는 페이징 처리에 대한 부분이었다.
참고 블로그 링크
예를 들어서, 총 10페이지로 구성되어있고, 1페이지당 10개의 데이터가 있다고 가정한다.
총 100개의 데이터가 업데이트가 되어야 한다.
근데 커밋이 한번 일어나게 되면,

1페이지 10 -> 2페이지 10 -> 3페이지 10 -> ... 이 될줄 알았었다.

애초에 도입하기 이전부터 해당 참고 블로그를 보면서 이런 에러가 있구나 하면서 개발을 진행했다.
인덱스 기준으로
1 ~ 10 번까지의 데이터가 작업 완료되면 해당 데이터들은 Update가 진행이 될 것이다.
그래서 11 ~ 20을 원하던 다음 데이터는 21 ~ 30을 조회하게 되는것.
이것은 배치의 문제가 아니라 그냥 페이징 쿼리 자체의 문제라고 한다.

그래서 유저들에게 푸시를 보낼때 50%의 유저만 푸시를 받았을 것이다.😇

이렇게 해서는 안됐다.
방법은 우선 2가지가 있었다.
그렇지만 나는 2번째 방법을 사용했다.

그렇다면 왜?

우리 회사의 배치 서버는 JPA로 구성하기로 했었고, 그래서 JPA를 사용한 배치 동작을 구현해서
Cursor대신 JpaPaging을 사용했다.

커서 사용

커서(Cursor)란??

쿼리문에 의해서 반환되는 결과값들을 저장하는 메모리공간
Fetch => 커서에서 원하는 결과값을 추출하는 것
커서는 한번 커넥션을 맺은 후 커서만을 다음으로 가기 때문에 조회하고 Update되어도 갱신되는 일이 없이 적용 가능하다.

@Bean
@StepScope
public JpaPagingItemReader<Pay> payPagingReader() {

    private final int chunkSize = 1000;

    JpaPagingItemReader<PushAlarm> reader = new JpaPagingItemReader<PushAlarm>() {
        @Override
        public int getPage() {
            return 0;
        }
    };

    reader.setQueryString("SELECT p FROM PushAlarm p WHERE p.sendStatus = :sendStatus");
    reader.setParameterValues(Map.of("sendStatus", "WAIT"));
    reader.setPageSize(chunkSize);
    reader.setEntityManagerFactory(entityManagerFactory);
    reader.setName("payPagingReader");

    return reader;
}

이런식으로 page를 0으로 고정시켜줘서 update가 일어나도 다시 0페이지만 계속 조회하는 것이다.
이렇게 해서 문제를 해결했다.

마무리

배치 세미나를 진행하면서 공식문서를 읽고 참고 블로그까지 더해서 학습해서
적용해본 결과 그래도 역시 글로 보는것보다 맞으면서 배우는게 좀 더 빠르게 습득이 가능하다는걸 느낀다.
지금은 데이터가 작아서 내가 일을 제대로 처리했을지 모르겠다.
그래서 강의를 하나 더 들으면서 좀더 뿌리를 깊게 내려야겠다...

728x90

+ Recent posts