2월 1~3주차. 서버 스케줄링

변현섭·2024년 2월 22일
0

다우데이타 인턴십

목록 보기
6/17
post-thumbnail

1. Crontab

1) 개념

먼저 Cron은 리눅스에서 주기적으로 작업을 실행하기 위해 사용되는 시스템 시간 기반 스케줄러이다. Cron은 시간을 크론 표현식으로 나타내어 주기적인 작업을 예약하는데, 그 형식은 아래와 같다.

분(0-59) 시(0-23) 일(1-31) 월(1-12) 요일(0-6)

항상 반복해야하는 경우에는 '*' 표를 사용하면 된다. 예를 들어, "매주 금요일 새벽 2시 30분에 실행"하는 크론 작업을 설정하는 표현식은 30 2 * * 5가 되는 것이다. 참고로 요일은 일요일 ~ 토요일 순으로 나타내며, * * * * *은 1분마다 작업을 반복한다는 의미가 된다. (작업 간격을 1분 미만으로 설정할 수는 없다.)

Cron은 데몬 형태로 작업을 수행하는데 이 때, Cron이 어느 시점에 어떤 작업을 수행해야 하는지 저장하고 관리하는 파일(또는 명령어)이 바로 Crontab이다. 쉽게 말해, Cron에 작업을 등록하고 관리하기 위해 Crontab을 사용하는 것이다. Crontab은 반복 작업 및 예약 작업을 처리하기에 매우 유용하기 때문에 주기적인 자동 백업, 캐시 삭제 등에 목적으로 사용될 수 있다.

2) 기본 사용법

① Crontab의 기본 명령어는 아래와 같다.

crontab -l // Crontab에 저장되어 있는 작업들의 목록
crontab -r // Crontab에 저장되어 있는 작업 삭제
crontab -e // Crontab에 새로운 작업 등록 또는 기존 작업 수정

② crontab -e 명령을 입력한 후, 본인이 실행하고자 하는 새로운 작업을 등록해보자.

  • 주석은 지워도 되고, 안 지워도 된다.
  • ctrl + x > Save > 엔터를 눌러 빠져나올 수 있다.

③ crontab -l 명령을 이용해 Cron 작업이 잘 등록되었는지 확인해보자. 또한, Cron 작업을 삭제하는 명령의 실행 결과도 확인해보자.

crontab -l
crontab -r
crontab -l

④ 사용할 쉘 스크립트 파일을 작성해주자.

vi nuboshell.sh
  • 여기서는 Nubo Scheduling API를 호출하는 내용으로 작성해주었다.

⑤ crontab -e 명령을 이용해 쉘 스크립트 작업을 예약한다.

  • 다시 1분마다 nuboshell.sh를 반복 실행하도록 설정해주었다.

⑥ 아래의 명령을 통해 Cron이 제대로 실행되고 있는지 확인할 수 있다.

cat /var/log/syslog | grep CRON
  • 1분마다 반복적으로 실행되고 있음을 알 수 있다.

3) Crontab 시간 설정 API

일반적으로, Crontab의 시간을 설정하는 API를 제공하는 것은 보안적인 관점에서 권장되진 않는다. 하지만, Web UI 형태로 관리자 기능을 제공해야 하는 상황이라면, 편의성을 위해 API로 구현할 수도 있다.

지금부터 API를 통해 Crontab의 시간을 설정하는 방법에 대해 알아보자. 먼저 리눅스에서 한줄로 Crontab의 내용을 수정하는 명령어는 아래와 같다.

echo "0 0 * * * bash nuboshell.sh" | crontab -

위 예시에서는 매일 밤 12시마다 스케줄링을 수행하도록 설정하고 있다. 이제 이 명령을 이용하여 Crontab의 시간을 설정하는 API를 만들어보자.

const exec = require("child_process").exec; # 모듈 import
const util = require('util');
const execPromise = util.promisify(exec);

const setCrontabTime = async (req) => {
  try {
    crontabTime = req.minute + " " + req.hour + " " + req.day + " " + req.month + " " + req.dayOfWeek;
    const editCrontabCommand = `echo "${crontabTime} bash nuboshell.sh" | crontab -`;
  
    await execPromise(editCrontabCommand);
    logger.info(`--------- ${crontabTime}으로 Crontab의 시간을 재설정합니다. ---------`);
    return { status: statusCode.OK, message: message.SET_CRONTAB_TIME_SUCCESS };
  } catch (e) {
    logger.error(`Crontab 시간 설정 작업에 실패하였습니다.`);
    throw new FailedToSetCrontabTime();
  }
};

이제 API 호출을 통해 Crontab의 시간을 자유롭게 설정할 수 있게 된다.

4) Crontab On/Off

서버 스케줄링에 대한 On/Off 기능을 구현하는 방법 중 하나로, Crontab 작업 목록을 주석 처리 또는, 주석 해제 처리하는 방법을 고려할 수 있다. (단순히 crontab -r 커맨드로 삭제해버릴 경우, 기존에 설정되어있던 시간 데이터가 삭제되어 버린다.) 이 때, 사용할 수 있는 명령어는 아래와 같다.

// Crontab 작업 목록 주석 처리 (스케줄링 Off)
crontab -l 2>/dev/null | awk '!/^#/ {print "#", $0; next} {print}' | crontab -

// Crontab 작업 목록 주석 해제 처리 (스케줄링 On)
crontab -l 2>/dev/null | sed 's/^# //' | crontab -

이와 같은 방식으로 스케줄링 On/Off를 조절하는 API를 만들어보자.

const setSchedule = async (req) => {
  if (!isBoolean(req.useSchedule)) { throw new InvalidBooleanType(); }

  setEnvValue("USE_SCHEDULE", req.useSchedule);

  try {
    checkCrontabCommand = 'crontab -l';
    const crontabTask = await execPromise(checkCrontabCommand);
    let lines = 0

    if(crontabTask != '') {
      lines = crontabTask.stdout.trim().split('\n');
    }
      
    const isEmpty = lines.length === 0;
    let returnMessage = "";
    if (req.useSchedule == "true" || req.useSchedule) {
    
      if (isEmpty) { // 등록된 Crontab 작업이 없음.
        logger.info(`--------- 스케줄링 모드를 ON으로 설정합니다. 스케줄링을 사용하기 위해선 Crontab 시간을 등록해야 합니다. ---------`);
        returnMessage = message.SCHEDULING_ON_SUCCESS_NOT_SETTED_TIME;
      } else { // 최초 실행이 아니라면, 기존 Crontab의 주석을 해제
        uncommentCommand = "crontab -l 2>/dev/null | sed 's/^# //' | crontab -";
        await execPromise(uncommentCommand);
        logger.info(`--------- 스케줄링 모드를 ON으로 설정합니다. 기존 Crontab 시간을 그대로 사용합니다. ---------`);
        returnMessage = message.SCHEDULING_ON_SUCCESS_EXISTING_TIME;
      }
    
    } else {
      logger.info(`--------- 스케줄링 모드를 OFF로 설정합니다. OFF 상태에선 스케줄링이 중지됩니다. ---------`);
      if (!isEmpty) { // 기존 crontab 주석 처리
        commentCommand = `crontab -l 2>/dev/null | awk '!/^#/ {print "#", $0; next} {print}' | crontab -`;
        await execPromise(commentCommand);
      } 
      returnMessage = message.SCHEDULING_OFF_SUCCESS;
    } 

    return { status: statusCode.OK, message: returnMessage };

  } catch(e) { // 등록된 crontab 작업이 없어 'crontab -l'에서 에러 발생 -> 로그만 출력
    if (req.useSchedule == "true" || req.useSchedule) {
      logger.info(`--------- 스케줄링 모드를 ON으로 설정합니다. 스케줄링을 사용하기 위해선 Crontab 시간을 등록해야 합니다. ---------`);
      return { status: statusCode.OK, message: message.SCHEDULING_ON_SUCCESS_NOT_SETTED_TIME };
    } else {
      logger.info(`--------- 스케줄링 모드를 OFF로 설정합니다. OFF 상태에선 스케줄링이 중지됩니다. ---------`);
      return { status: statusCode.OK, message: message.SCHEDULING_OFF_SUCCESS };
    }
  }
  
}

2. Axios, setInterval

1) 개념

주기적으로 API를 반복 실행해야 할 때, setInterval 함수를 사용할 수 있다. setInterval 함수는 이름에서도 알 수 있듯, 일정 시간 간격을 두고 함수를 실행하기 위한 목적으로 사용된다. 아래는 setInterval과 Crontab의 차이점을 나타낸 것이다.

① 정확도와 정밀도

  • crontab
    • 더 정확하고 정밀한 스케줄링에 사용
    • 크론탭은 분(minute), 시(hour), 일(day), 월(month), 주(weekday) 등 다양한 기준으로 정밀한 스케줄 지정 가능
  • setInterval
    • 상대적으로 간단한 주기적 작업에 사용
    • 정확한 시간 간격이 보장되지 않음. (간격이 길어질수록 정밀성이 떨어짐)

② 사용 목적

  • crontab: 주기적인 백그라운드 작업, 정기적인 데이터베이스 백업 등에 사용
  • setInterval: UI 업데이트, 간단한 타이머 등 정확한 타이밍이 요구되지 않는 작업 등에 사용

③ 실행 환경

  • crontab
    • 운영체제 레벨에서 동작
    • 서버 환경에서 사용
  • setInterval: 주로 Node.js 환경에서 사용

④ 차별점

  • crontab
    • 특정 시각 기준 스케줄링
    • 초단위 스케줄링은 불가
    • ex) 12시마다 스케줄링
  • setInterval
    • 주기 기준 스케줄링
    • 밀리초단위 스케줄링 가능
    • ex) 1시간마다 스케줄링

2) 사용 방법

setInterval 함수를 통해 API를 반복 호출할 것이므로, Node.js 환경에서 API 호출 및 데이터 통신에 사용되는 Axios 라이브러리를 사용할 것이다.

① 아래의 명령어를 입력하여 axios 모듈을 설치한다.

npm install axios

② axios 모듈을 import 한다.

const axios = require("axios");

③ 호출하고자 하는 API와 관련된 Service 파일에 아래의 코드를 작성한다.

const periodicCall = async () => {
  try {
  	// 호출하고자 하는 API의 URL과 HTTP 메서드
    const response = await axios.post('http://localhost:3000/schedule/');

    // API 응답을 이용한 로직 작성
    console.log('API Response:', response.data);
  } catch (error) {
    // 에러 처리
    console.error('API Error:', error.message);
  }
};

const interval = 10000; // 호출 간격 지정 (밀리초 단위)

setInterval(() => {
  periodicCall();
}, interval);

const scheduleController = {
  systemStart
};

// 즉시 한번 호출한 이후에 주기적 호출을 원하는 경우, 아래의 코드 추가
periodicCall();

3. Node Cron

위에서도 잠깐 설명했지만, Cron은 운영체제 레벨에서 동작하기 때문에 잠재적으로 많은 위험성을 가지고 있다. 이러한 이유로 대부분의 경우에 JS 모듈을 이용한 스케줄러가 선호되는데, 여기서 소개할 모듈은 Node Cron이다.

Node Cron은 유닉스 크론탭과 유사하게 작업을 스케줄링할 수 있도록 지원하는 자바스크립트의 모듈이다. Cron 표현식은 그대로 사용하면서도, 내부적으로는 setTimeout()과 setInterval() 메서드를 사용하기 때문에, Node.js 서버에서 스케줄링을 쉽게 구현할 수 있게 된다. 당연히, Node.js 프로세스 내부에서만 동작하기 때문에 악용 가능성도 크게 낮출 수 있다. 그러면, 지금부터 Node Cron의 사용 방법에 대해 알아보기로 하자.

1) 기본 사용법

① node-cron 모듈 설치하기

npm install --save node-cron

② node-cron import 하기

  • 스케줄링 로직을 구현할 파일에 아래의 import 문을 추가한다.
const cron = require("node-cron");

③ cron 표현식을 이용하여 반복 주기를 지정한 후, 주기마다 반복 수행할 함수를 정의한다.

cron.schedule("* * * * *" , async () => {

  try {

    logger.info("######### [POST] /user/transfer 실행 #########");
    const data = await userService.transferSystem();
    return res.status(data.status).send(success(data.status, data.message));
  
  } catch (e) {

    logger.error("######### [POST] /user/transfer 실패 #########");
    return next(e);

  }
  
});

2) Cron 표현식 Formatting

입력 받은 값을 Cron 표현식으로 Formatting 하는 방법은 아래와 같다.

const formatCrontabTime = async (req) => {

  const crontabTime = req.minute + " " + req.hour + " " + req.day + " " + req.month + " " + req.dayOfWeek;
  return crontabTime;
      
}

3) Validation Check

Cron 표현식에 들어가야 할 입력 값을 req.body에 담아 전송한다고 할 때, 아래와 같은 방법으로 유효성을 검사할 수 있다.

function validationCheck(cron) {
  const parts = cron.split(' ');
  if (parts.length !== 5) {
      return false; // 크론탭 표현식은 반드시 5부분으로 구성되어야 함
  }

  const [minute, hour, day, month, dayOfWeek] = parts;

  // 각 필드의 유효 범위 검사
  const isMinuteValid = parseList(minute, 0, 59);
  const isHourValid = parseList(hour, 0, 23);
  const isDayValid = parseList(day, 1, 31);
  const isMonthValid = parseList(month, 1, 12);
  const isDayOfWeekValid = parseList(dayOfWeek, 0, 6);

  return isMinuteValid && isHourValid && isDayValid && isMonthValid && isDayOfWeekValid;
}

function parseList(field, min, max) {
  if (field === '*') {
      return true;
  }
  const values = field.split(',');
  return values.every(value => {
      const number = parseInt(value);
      return !isNaN(number) && number >= min && number <= max;
  });
}

4) 스케줄링 On/Off

기본적으로는 스케줄링 On/Off를 토글하면서, 사용자 입력이 유효하지 않은 경우에는 예외를 호출하는 방식으로 구현해보자.

let isScheduling = false; 
let scheduledTask = null;

const toggleScheduling = async (req, res) => {

  const isValidate = await validationCheck(req);
  
  if(!isValidate) { // 유효하지 않은 입력

    logger.info("입력 값이 유효하지 않습니다. 서버 스케줄링을 중지합니다.");
    throw new InvalidInputError();

  } 

  const crontabTime = await formatCrontabTime(req);

  if (isScheduling) { // 스케줄링이 진행 중인 상태

    scheduledTask.stop(); // 스케줄링 중지
    logger.info("서버 스케줄링을 중지합니다.");
    isScheduling = false;
    
    return { status: statusCode.OK, message: message.SCHEDULING_STOPPED };

  } else { // 스케줄링이 실행되지 않고 있는 상태

    isScheduling = true;
    logger.info("서버 스케줄링을 실행합니다.");
    scheduledTask = cron.schedule(crontabTime, async () => {
      await userService.transferSystem();
    });

    return { status: statusCode.OK, message: message.SCHEDULING_STARTED };

  }

};
profile
LG전자 VS R&D Lab. Connected Service 1 Unit 연구원 변현섭입니다.

0개의 댓글

Powered by GraphCDN, the GraphQL CDN