ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Wee.T] 스케줄러(@Scheduled)를 사용해 주기적으로 자동실행되는 로직 구현하기
    DEVELOP/Wee.T 2022. 9. 16. 13:09

     

     

    Wee.T에서 PT를 개설하면, 종료일이라는 개념이 없고, 횟수차감제로 운영됩니다.

    이는 실제 헬스 PT가 이루어지는 방식을 재현하고자 한 것인데, 정해진 횟수를 결제하고(ex. 30회) 헬스장에 갈 때마다 횟수가 차감되어 30회를 다 채우면 종료되는 방식입니다.

     

    이를 프로그램으로 구현하기 위해서는 날짜가 지나면 횟수가 자동으로 차감되는 로직을 구현해야 합니다. 진행상황이 업데이트 되어야하는데, 업데이트 되는 조건이 시간인 상황입니다.

     

     

    PT에 관련된 ERD

     

    PT 테이블(t_class)의 ERD 중 스케줄과 등록 관계를 나타내는 관계만 나타내었습니다. 형광펜으로 표시한 컬럼들이 중요합니다.

    t_class_listenr(PT 수강테이블) 테이블에 PT를 등록한 사람들의 진행현황을 나타내기 위한 progress 컬럼과, 해당 PT가 종료되었는지 여부를 나타내는 status 컬럼을 추가하였습니다.

     

    t_class 테이블의 class_count 컬럼은 해당 PT가 몇회짜리 수업인지 나타내는 컬럼이고, 해당 클래스의 스케줄정보는 t_class_schedule테이블에 저장되어 있습니다. cs_day컬럼에 요일을 ('월', '화', '수', ..) 형식으로 저장하고 있습니다. 스케줄 정보는 1주일간 반복되는 시간표를 저장하고 있습니다. 

     

    따라서 구현해야할 로직은

    (1) 현재날짜와 유저가 등록한 pt의 스케줄 요일을 비교하여, 있으면 progress 컬럼을 1씩 증가시킨다.

    (2) progress가 PT의 지정된 횟수에 도달하면 status를 종료상태로 변경한다.

    (3) (1), (2) 과정을 시간에 따라 자동으로 수행되도록 만든다.

    정도가 됩니다.

     


    1. Mapper

     

    2개의 UPDATE문과 1개의 SELECT문을 만들었습니다.

    progress 업데이트, status 업데이트, 그리고 등록된 전체 클래스 ID를 가져오는 SQL 쿼리문 입니다. SELECT문은 비교적 간단하기 때문에 어노테이션으로 처리하였습니다.

     

    mapper interface

    // 전체 클래스 ID 조회
    @Select("SELECT class_id FROM t_class")
    public abstract List<String> selectAllClassId() throws DAOException;
    
    // 진행상황 업데이트
    public abstract int updateProgress(String classId) throws DAOException;
    
    // 종료여부 업데이트
    public abstract int updateStatus(String classId) throws DAOException;

     

    mapper.xml

    <update id="updateProgress">
        UPDATE t_class_listenr
        SET progress = progress + 1
        WHERE 
            class_id = #{classId}
            AND status = 0
            AND to_char(current_date - 1, 'dy') IN (
                SELECT
                    cs_day
                FROM
                    t_class_schedule
                WHERE
                    class_id = #{classId}
            )
    </update>

     

    to_char(current_date - 1, 'dy')의 결과는 전날의 요일을 '월', '화' 이런 형식으로 반환합니다. cs_day컬럼이 해당 형식으로 요일이 저장되어있기 때문에, 클래스ID에 해당하는 요일들을 IN연산자로 비교하여, 전날의 요일이 스케줄 안에 있으면 progress를 1 증가시키도록 작성하였습니다.

    전날의 요일을 비교하는 이유는 스케줄러를 매일 자정에 실행하도록 작성할 것이기 때문입니다.

     

     

    <update id="updateStatus">
        UPDATE t_class_listenr
        SET status = 1
        WHERE 
            class_id = #{classId}
            AND status = 0
            AND progress >= (
                SELECT
                    class_count
                FROM
                    t_class
                WHERE
                    class_id = #{classId}
            )
    </update>

     

    종료여부를 업데이트하는 쿼리입니다. progress가 지정된 class_count보다 같거나 커지는 순간 status를 1(종료됨)로 업데이트 합니다.

     


    2. Service

     

    Service interface

    // 진행상황 관리
    public abstract void ptScheduler() throws ServiceException;

     

    ServiceImpl

    @Override
    @Scheduled(cron = "0 0 0 * * *")
    public void ptScheduler() throws ServiceException {
        log.trace("ptScheduler() invoked.");
    
        try { 
            List<String> classIds = this.mapper.selectAllClassId();
            log.info("\t 클래스 아이디 리스트: {}", classIds);
    
            int progress = 0;
            int status = 0;
    
            for(String classId : classIds) {
                progress += this.mapper.updateProgress(classId);
                status += this.mapper.updateStatus(classId);
            } // enhanced for
    
            log.trace("\t+ *** Scheduler - 업데이트 완료 ***");
            log.trace("\t+ *** 1. progress 업데이트 행 수 : {} ***", progress);
            log.trace("\t+ *** 2. status 업데이트 행 수 : {} ***", status);
        } catch(DAOException e) { throw new ServiceException(e); } // try-catch
    } // ptScheduler

     

    작성한 쿼리문을 실행하는 서비스 메소드를 만듭니다.

    먼저 전체 클래스ID를 가져오는 쿼리를 실행한 결과를 String타입 List로 받아옵니다.

    그리고 이 리스트를 반복하여 각 업데이트메소드의 매개변수로 전달합니다.

    이렇게 하면 모든 클래스에 대한 업데이트를 수행합니다.

     

    이 메소드를 매일 자정에 수행하도록 하면, 전체 클래스의 진행상황, 그리고 종료여부 업데이트를 자동으로 체크할 수 있습니다.

     

    @Scheduled 어노테이션이 스케줄러 역할을 수행합니다.

    cron 속성에 주기를 지정하면, 원하는 주기에 메소드를 실행하게 할 수 있습니다.

    0 0 0 * * *은 매일 자정에 반복하겠다는 의미입니다.

    cron은 6자리로 주기를 표현할 수 있으며, 앞에서부터 순서대로 초, 분, 시간, 일, 월, 요일 입니다.

     


    3. root-context.xml 설정

     

    @Scheduled 어노테이션을 사용하기 위해서는 root-context.xml에 설정을 해주어야 합니다.

     

     

    [Namespaces] 탭에서 task를 활성화 시킨 후, 소스코드에 해당 코드를 추가해야 합니다.

    (스케줄러를 사용한 서비스 패키지가 아직 scan되지 않았다면 context:component-scan으로 등록하는 코드도 작성해야합니다.)

    <task:annotation-driven />

     

     


    구현 예시

     

    임의로 cron설정을 10초마다 반복하도록 설정한 후, 동작이 잘 되는지 실행해보았습니다.

    글을 작성한 시간 기준으로 전날은 목요일이고, 목요일은 class2의 스케줄(월, 목 반복)에 등록되어있습니다.

    class2의 횟수는 20회로 설정되어 있어서, 수강한 유저의 진행상황을 18로 해놓고 서버를 구동하였습니다.

     

     

    처음엔 progress만 업데이트 되고, 20회가 되자 status가 종료상태로 변경되는 것을 확인할 수 있습니다.

    종료된 이후에는 스케줄러가 작동해도 더이상 업데이트 되지 않습니다.

     

     

    DB에도 잘 반영된 것이 확인됩니다.

     

    사실 실제처럼 구현하려면 시간까지 체크하고, 각 회원의 출석도 모두 체크해야 하며 주기도 매번 바뀌기 때문에 Quartz같은 job scheduler 프레임워크를 사용해야 할 것 입니다. 그러나 그렇게 구현하려면 기존 프로젝트에서 변경해야할 부분이 너무 많고 이 기능 하나를 구현하기 위해 너무 많은 시간을 할애해야 할 것 같아 이정도 로직으로 구현해 보았습니다. 

    댓글

Designed by Tistory.