이전 글)
https://kdh0518.tistory.com/99
[Task Scheduler] 불규칙적인 task 처리 (동적 스케줄링) + 영속성 처리
불규칙적 상황? 동적 스케줄링? 개발을 진행하다 보면 특정 시점에 기능이 동작해야 하는 경우가 존재한다.trigger가 존재한다면 단순하게 trigger 메서드에서 target 메서드를 실행하면 되지만,
kdh0518.tistory.com
문제 인식
Task Scheduler 글을 작성할 당시만 하더라도, 2026년 기준으로 하루 최대 144번 실행되던 메서드를, 평균 5.72회에서 최대 26회 실행되도록 줄임으로써 리소스를 81.9% ~ 96.0% 감소시켰다고 생각했다.
실제로 '실행 횟수'만 본다면 해당 수치만큼 감소된게 맞고, 실제 사용된 리소스도 비슷한 비율로 절감되었을 것이다.
하지만 나는 장담하건데, 내 자신이 '퍼센트의 함정'에 빠져있었다고 확신한다.
예를들어, 10ms의 실행시간을 가지던 로직을 0.1ms로 개선한 것도 99% 개선된 것이다. 그러나 이런 작업이 정말로 유저가 '체감될 정도'의 개선일까? 전혀 아닐것이다. 유저는 수백ms까지는 다 비슷하게 느낄것이고, 그 미세한 차이를 인지할 수 없고, 인지하고 싶지도 않을것이다.
심지어 내가 개선했던 로직은 유저가 직접 사용하는 로직도 아니고, 백그라운드에서 이루어지는 작업이다.
조회해야하는 데이터가 수억개가 넘어가는 대규모 작업도 아니고, 한번의 로직에서 DB I/O가 미친듯이 많은 작업도 아니며, 여러 도메인이 얽혀있어 커넥션을 오래 점유하는 작업도 아니다.
단순히 'entity 조회 -> 컬럼 변경 -> entity 저장' 과정이 이루어지는 단건 작업이다.
따라서 지금부터 좀 더 정밀하고 정량적인 측정을 통해, '정말로 해당작업이 해결해야만 했던 문제'이고, 내가 사용한 방식이 '개발 복잡도와 운영 난이도 대비 효율적인 방법'인지를 확인해보겠다.
그 전에, 이전 글을 읽을 시간이 독자들을 위해 간단하게 Task Scheduler를 사용할 당시 내 문제 접근 과정을 요약해둘테니, 가볍게 읽고나서 다음으로 넘어가주길 바란다.
[Task Scheduler를 통해 사용자 경험 개선 및 서버 효율성 증대]
- 문제
- 연간 수백개의 일정이 존재하는데, 각 일정은 ‘5개의 진행상태’와 ‘2개의 노출상태’가 존재함
- 각 일정은 ‘일정 시작일~일정 종료일’에 맞춰서, 진행상태와 노출상태가 변경되어야함
- 시작일/종료일에 맞춰서 ‘대기중’, ‘모집중’, ‘진행중’, ‘종료’, ‘폐강’ 과 같은 상태로 변경되고, 각 상태에 맞춰 ‘노출’ 혹은 ‘숨김’ 상태가 되어야한다
- '일정 시작 3개월 전', '일정 시작 1개월 전', '일정 시작', '일정 종료' 에 맞춰서 상태와 노출상태가 변한다
- 따라서 2026년 기준으로 존재하는 모든 이벤트는 2088개이다
- 하지만 관리자가 두 명 밖에 없어서, 수동으로 모든 이벤트에 대해 상태를 변경하기가 부담스러운 상황
- 접근
- 관리자가 설정한 ‘특정 시각’에 맞춰 ‘자동으로’ 변해야 한다는 점에 착안하여 Scheduler를 사용하기로 결정
- Cron을 사용하여 해당 날짜에서 변경해야할 데이터를 polling하는 방식은, ‘특정 시각’에 맞춰서 실행되려면 주기가 짧아야 하기에 너무 많은 polling이 일어나서 DB커넥션과 메모리가 낭비된다 판단 (ex. 10분 주기면 하루 144회 실행)
- 대신 ‘특정 시각’에만 실행시킬 수 있는 Task Scheduler를 사용하여 구현
- 해결
- Task Scheduler를 사용하여 ‘Task’가 실행되어야 하는 ‘특정 시각’에만 실행되도록 구현
- 비동기로 실행되는 작업이기에 Task 상태를 저장하는 Job Table을 활용해서 주기적으로 재처리하는 방식을 사용하고, 실패 횟수가 임계치를 넘어가면 Slack으로 메시지 전송
- 또한 Task가 메모리에 저장되기에 애플리케이션 재시작시 데이터가 초기화되는 문제를, @ApplicationReadyEvent 를 사용하여, 애플리케이션 시작시 최초 1회 Task를 다시 등록하는 로직을 구현하여 영속성 보장
- 결과
- 2026년도 일정표를 기준으로, 총 522개의 일정이 생성되고, 하나의 일정에 대해 4번의 상태변화가 필요하므로, 총 2088개의 이벤트가 필요하고, 이벤트가 발생한 고유한 날짜 수는 280일임
- 이때 상태 변경이 10분 단위로 이루어진다 가정했을 때 다음과 같이 리소스를 감소시킬 수 있음
- Cron을 사용한 주기적 polling → 144회/일 실행
- Task Scheduler를 사용 → 5.72회/일 (365일 전체 기준), 7.46회/일 (이벤트가 존재하는 날 기준), 26회/일 (이벤트가 가장 많이 발생하는 날 기준)
- 따라서 불필요한 Polling 제거로 리소스를 평균적으로 96.0% 감소
- 또한 상태 자동화를 통해 클라이언트의 UX 개선
목표
이번 글에서 내가 확인할 목표는 다음과 같다
- 정말로 해당작업이 해결해야만 했던 문제인가?
- 개발 복잡도와 운영 난이도 대비 효율적인 방법인가?
해당 질문들에 대한 답을 얻기위해 다시 한번 전체적인 상황을 검토하여 비즈니스 임팩트를 확인해볼 것이고, 두 가지 방식을 따로 구현한다음 Prometheus와 Grafana를 통해 단건 실행에 대한 리소스 사용량과 평균적인 사용량, 피크 타임에서의 사용량을 측정하여 리소스 사용량을 비율이 아닌 양으로 비교해볼 것이다.
접근
'정말로 해당 작업이 해결해야만 했던 문제이고, 리소스 낭비를 막을 수 있는걸까?'
처음 내가 문제를 인식했을때 다음과 같은 상황을 가정했다.
- '특정 시각'을 10분 단위로 생각했었고, 그 결과 cron을 사용하려면 하루 144회 실행이 되어야 한다
- 144회 실행됨으로써 서버 리소스가 낭비된다
이제 실제 운영상황을 통해 해당 가정들이 올바른지를 판단해보자
(다음 내용에서 주기적으로 등장하는 Task는 '교육 일정'의 '진행상태'와 '노출상태'를 변경하는 작업이라 생각하면 된다)
- '특정 시각'을 10분 단위로 생각해야하는가?
- Task의 대상이 되는 '교육 일정'은 '10분' 단위로 설정되는 것이 아니라 '일' 단위로 설정된다
- ex. 25년 1월 1일 ~ 25년 1월 10일까지, 총 10일 진행
- Task는 '진행상태'가 바뀌는 경우는 5가지, '노출상태'가 바뀌는 경우는 2가지 경우가 존재한다
- 이때 '유저'와 상호작용이 필수인 '진행상태'는 '모집중' 상태이다 ('모집중'에서 수강신청 가능)
- 수강신청은 '선착순'으로 참여할 수 있다 (정원이 정해져있음)
- 따라서 다른 상태는 언제 바뀌어도 상관없지만, '모집중'으로 상태 변경시에는 '선착순 참여'를 고려해야한다
- 그렇다면 Task가 실행되어야할 '최소 단위'가 어떻게 될까?
- 만약 '최소 단위'가 '일'이 된다면, '모집중'을 포함한 모든 상태변경이 00시에 실행된다
- 하지만 직장인 대상으로 진행되는 교육이기에, 00시에 선착순 진행은 유저 경험에 좋지 못하다
- 따라서 '일' 단위가 아니라 '시간' 단위로 진행되고, 09시나 20시 등, 관리자가 선택할 수 있게 하는 방식이 최선일 것이다
- 00시에 '모집중'으로 상태 변경이 일어나는게, 정말로 유저 경험에 좋지 못한가?
- 서비스 오픈 이후 2개월 정도 모니터링 해본 결과, '티켓팅'처럼 오픈 직후 유저 트래픽이 몰리고 동시성 문제가 심각하게 발생하는 경우는 존재하지 않았다
- 따라서 00시에 모든 상태가 일괄변경 되더라도 현재 상황에서는 전혀 문제가 없다
- Task의 대상이 되는 '교육 일정'은 '10분' 단위로 설정되는 것이 아니라 '일' 단위로 설정된다
- 그렇다면 서버 리소스 낭비가 일어나는가?
- 하루동안 polling하는 횟수는 1회만으로도 충분하다
- 위에서 확인한 것 처럼, 당장은 하루 한번, '00'시에 일괄 변경되더라도 전혀 문제가 없다
- 그런데 하루에 한번씩 Task가 존재하는가?
- 2026년도 일정표를 기준으로 상태변경이 필요한 고유한 날짜는 280일이다
- 따라서 매일 상태변경 polling이 이루어진다면, 약 85일 정도는 불필요한 polling이 발생한다
- 일정의 추가/변경이 일어난다면?
- 일정의 수가 늘어나면 늘어날수록 Task Scheduler 방식의 효율성은 감소한다
- 예를들어 현재는 280일만 Task가 실행되면 되지만, 교육 일정이 늘어나서 365일 모두 Task가 실행되어야 한다면 Task Scheduler와 Cron을 사용한 Polling 방식의 효율성은 똑같아진다
- 그러나 2026년 기준, 공휴일과 주말이 최대 117일이므로, 현재 280일이 '가능한 모든 날짜'에 대한 최댓값으로 해석해도 무방하다
- 따라서 일정의 추가/변경이 일어나더라도 cron을 사용한 polling과 Task Scheduler의 효율성은 변동이 없다고 가정하겠다
- 그렇다면 '횟수'만을 기준으로 봐도 되는걸까?
- 아니다. 명백하게 '단일 실행', '하루 평균', '피크 타임'에 대해 각각 cpu 사용률, 메모리 점유율, 전체 처리 시간동안 동시에 사용되는 DB 커넥션 수 등을 측정해야한다
- 만약 해당 수치들이 미미하다면 매일 실행되는것은 아무 문제가 없고, 하루에 144회 실행된다 하더라도 전혀 부담이 되지 않기 때문이다
- 하루동안 polling하는 횟수는 1회만으로도 충분하다
결론
1번과 2번에 따라, cron식을 사용한 주기적 polling 방식을 사용하더라도 하루 1회만으로 충분하므로, '횟수' 기준으로 Task Scheduler가 약 24%정도의 효율성만을 가지기에 DB 부하와 메모리 낭비를 '획기적으로 감소'시켰다고 보기는 힘들다.
또한 Task Scheduler를 구현함으로써 추가적으로 신경써야하는 영속화라던가 Task를 등록함으로써 발생하는 메모리 사용량을 고려했을때, Task Scheduler를 사용한 구현방법은 조금 더 엄밀한 검증이 필요하다는 결론이 나온다.
그러나 이는 '횟수'만을 기준으로 접근한 것이기에, 명확한 리소스 효율성을 따져보기 위해서는 다음과 같은 여러 지표가 필요하다.
- '하루 평균 이벤트 처리', '피크 이벤트 처리' 에 대해 측정한다
- 각 상황에 대해 'cpu 사용률 평균/최대', '메모리 점유율 평균/최대', '사용하는 DB 커넥션 평균/최대'를 측정한다
이때, HPE 하나의 회사에 대해 25년, 26년 이벤트 수는 다음과 같다.
- 2년동안 총 4480개의 이벤트 발생 (600*4 + 520*4)
- 1년 평균 약 2240개의 이벤트 발생
- 하루 최대 30개의 이벤트 발생
- 하루 평균 8개의 이벤트 발생
따라서 현재 상황에서의 리소스 사용량과, 확장성을 고려한 리소스 사용량을 둘 다 측정해보겠다.
확장성을 고려한 리소스 사용량 측정에서는, 현실적으로 플랫폼을 이용하는 회사가 5개이고 각 회사에서 5개년 데이터가 누적되었다고 가정하여 측정하겠다. 또한 각 회사마다 발생한 이벤트 수는 HPE와 동일하다고 가정하겠다.
이 경우 이벤트 수는 다음과 같을것이다.
- 5년동안 총 56000개의 이벤트 발생 (5개의 회사, 각 회사마다 2240*5년)
- 1년 평균 약 11,200개의 이벤트 발생
- 하루 최대 150개의 이벤트 발생
- 하루 평균 40개의 이벤트 발생
따라서 측정은 다음과 같이 진행하겠다.
- 현재 상황에서의 측정
- '일정' 테이블에는 데이터가 1120개(4480 / 4) 존재
- 하루에 8개, 30개의 이벤트가 발생하는 날이 존재하도록 셋팅
- 두 날짜에 대해 00:00 기준으로 'cpu 사용률 평균/최대', '메모리 점유율 평균/최대', '사용하는 DB 커넥션 평균/최대'를 측정
- Task Scheduler와 Cron을 사용한 Polling 방식, 두 가지 방식에 대해 모두 측정
- 확장성을 고려하여 측정
- '일정' 테이블에는 데이터가 14000개(56000 / 4) 존재
- 하루에 40개, 150개의 이벤트가 발생하는 날이 존재하도록 셋팅
- 두 날짜에 대해 00:00 기준으로 'cpu 사용률 평균/최대', '메모리 점유율 평균/최대', '사용하는 DB 커넥션 평균/최대'를 측정
- Task Scheduler와 Cron을 사용한 Polling 방식, 두 가지 방식에 대해 모두 측정
다음 글에서는, Task Scheduler와 Cron을 사용한 Polling 방식에서 리소스 사용'량'을 측정하고 비교하여 직접적으로 얼마나 영향이 미치는지, 그리고 정말로 24%의 효율성을 위해 Task Scheduler를 사용할만한 가치가 있는지 확인해보겠다.