Development note/Java

[Java] 63. Spring boot에서 cron 스케줄러와 Component 어노테이션

v명월v 2022. 3. 16. 18:55

안녕하세요. 명월입니다.


이 글은 Java의 Spring boot에서 cron 스케줄러와 Component 어노테이션에 대한 글입니다.


제가 이전에 Web Framework에서 cron 스케줄러를 사용한 적이 있습니다.

링크 - [Java] JSP Spring framework 환경에서 scheduler의 cron 사용법


우리가 웹 서버를 운영하다 보면 브라우저(Browser)에서 요청에 의한 실행 처리만 할 것이 아니라, 정해진 시간에 프로그램 내부의 캐쉬 관리라던가 데이터베이스의 데이터 관리던가 여러가지 처리를 해야 할 때가 있습니다.

물론 Window 서버라면 윈도우 스케줄러가 있고 Linux 서버라면 crontab 스케줄러가 있습니다. 웹 서버와 독립적으로 스케줄러를 운영해서 사용해도 됩니다만, 시스템 내부에서 운영을 해야 할 때가 있습니다.


cron 스케줄러를 프로그램 안에서 구현을 하게 되면 문제점이 하나 있습니다.

예를 들면, 정해진 시간에 메일을 전송하는 로직이 있다고 가정하면 이것을 cron 스케줄러에 의해 구현했습니다. 그런데 우연히 이 웹서버 로드밸런싱으로 분산 처리가 되어 있습니다.

그럼 스케줄러가 여러 곳에서 중복 실행이 됩니다.. 그래서 cron 스케줄러를 작성할 때는 항상 이 부분을 생각하고 만들어야 합니다.(제가 이런 바보같은 경험을 가지고 있다는 뜻이 아닙니다.)


Spring boot에서 cron 스케줄러를 사용하는 건 매우 간단하네요.. 심지어 따로 pom.xml 라이브러리를 추가하지 않아도 됩니다.

이 부분은 예전 Web framework보다는 매우 편하네요.


Spring boot를 실행하는 main 함수가 있는 클래스에 @EnableScheduling 어노테이션을 추가하면 됩니다.

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

// 패키지 베이스 설정
@SpringBootApplication(scanBasePackages = "com.example.demo.*") 
// Redis에 세션 공유를 한다.
@EnableRedisHttpSession 
// cron 스케줄러를 사용할 수 있게 설정한다.
@EnableScheduling   
public class SpringBootTestApplication {
  // main 실행 함수
  public static void main(String[] args) {
    SpringApplication.run(SpringBootTestApplication.class, args);
  }
}

위처럼 어노테이션만 추가하면 cron 스케줄러를 사용할 수 있습니다.


그럼 이제 cron 스케줄러를 사용하기 위해 bean을 추가합시다.

package com.example.demo.scheduler;

import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import com.example.demo.dao.UserDao;

// @Component 어노테이션
@Component 
public class SchedulerTest {
  // DI 의존성 주입
  @Autowired 
  // FactoryDao에서 취득
  @Qualifier("UserDao") 
  private UserDao userdao;
  
  // cron 스케줄러 설정
  @Scheduled(cron = "0/10 * * * * *")
  public void test() {
    // 콘솔 출력
    System.out.println(new Date());
    // 콘솔 출력
    System.out.println(userdao.selectById("nowonbun").getName());
  }
}

여기서 @Component 어노테이션을 추가해서 Bean에 등록합니다.

그리고 스케줄링 할 함수에 @Scheduled 어노테이션을 추가해서 문법을 이용해서 스케줄을 설정합니다.


여기서는 제가 10초마다 실행하라고 설정을 했습니다.

여기서 Scheduled 어노테이션에 설정하는 cron 문법이 있습니다.

이 부분은 이전에 설명한 적이 있습니다만, 다시 정리하도록 하겠습니다.

링크 - [Java] JSP Spring framework 환경에서 scheduler의 cron 사용법


먼저 문법은 스페이스를 구분으로 총 7개의 단위가 있고 마지막 하나는 생략이 가능합니다.

*  *  *  *  *  *  *
초 분 시 일 월 요일 년도(생략가능 - 생략시에는 매년의 의미)

먼저 위의 예제는 별 표시(*)를 했는데 이건 매초, 매분을 의미합니다. 즉, 위는 매초마다 실행하라는 의미입니다.

별 표시 말고는 숫자를 보통 넣는데, 단위가 아니라 지정 시간입니다.

즉, 「1 1 1 1 1 *」의 의미는 1월 1일 1시 1분 1초에 실행하라는 의미입니다. 6번째는 요일인데 「1 1 1 1 1 1」의 의미 1월 1일 1시 1분 1초 일요일에 실행하라라는 뜻인데.. 1월 1일이 일요일이 아니면 실행 안합니다.

참고로 요일은 1이 일요일부터 2는 월 3은 화.. 순으로 7은 토요일의 의미를 갖습니다.

여기서 우리는 복수의 시간을 설정할 수 있는데 콤마(,)의 구분으로 설정합니다. 「1,11 * * * * *」는 매분 1초와 11초에 실행하라는 뜻입니다.

다시 지정 시간이 아닌 단위 시간으로 설정하고 싶을 때도 있겠네요. 즉 10초 단위, 5분 단위로 처리하는 것입니다. 그럴 경우에는 0/단위 시간을 넣으면 되는데 「0/10 * * * * *」는 10초단위가 됩니다.


그 밖의 특수 표현식도 있습니다.

표현식 설명
* ALL의 의미로 매초, 매분, 매시간, 매일, 매월, 매요일, 매년
? 일 요일에서만 사용되는 조건 없음의 의미
/ 주기 반복의 의미
- 범위의 의미
L 일, 요일에서만 사용되는데 마지막날의 의미입니다.
W 일에서만 사용되는데 지정된 가장 가까운 평일을 찾습니다.
# 요일에서만 사용되는데 주#요일을 나타냅니다.

특수 표현식으로 L,W,#이 있는데 L은 마지막 날의 의미를 가지고 있습니다.일과 요일에서만 사용할 수 있는데 일에서 L를 사용하면 해당 달의 마지막 날, 요일에는 토요일을 의미합니다.

W는 일에서만 사용되는 데 가장 가까운 평일을 뜻합니다. 10W의 경우 10일이 토요일이면 9일에 실행. 10일이 일요일이면 11일에 실행하는 표현식입니다.

#은 요일에 사용되는 표현식입니다. 2#2라고 하면 두번째 주 월요일에 실행이라는 의미입니다.


참고로 cron의 문법은 cron 라이브러리 뿐아니라 Linux 스케줄러에서도 같은 문법으로 적용됩니다.

링크 - [CentOS] Linux에서 스케줄러(Crontab)를 사용하는 방법


cron 라이브러리를 사용할 때 우리는 Component 어노테이션을 설정해서 Spring Bean을 등록했습니다.

참고로 Spring에서는 Bean이라는 어노테이션도 있습니다.

먼저 Component라는 것은 클래스에 붙이는 어노테이션인데, Spring에서 DI 의존성 주입을 위해 사용되는 클래스를 뜻합니다.(scan-auto-detection,dependency injection)

즉, Singleton 패턴으로 사용되는 데이터 형식이 아닌 Controller 형식으로 사용되는 클래스를 설정하는 것입니다.

조건으로는 파라미터가 없는 생성자가 있어야하며, 그 생성자의 접근 제한자는 상관없습니다. 즉, 생성자를 작성하지 않거나 작성하더라도 반드시 파라미터가 없는 생성자가 있어야 합니다.

즉, 이렇게 작성하면 아래와 같이 에러가 발생합니다.

여기서 Bean 어노테이션은 Component와 비슷합니다. 그런데 Bean 어노테이션은 함수에 붙습니다.

함수는 메모리에 할당하는 의미가 없는데 함수에 붙는 것이 의미가 있을까 라고 생각할 수 있습니다만..


Autowired로 Bean으로 등록된 것을 호출하게 되면 함수가 실행된 결과가 리턴이 됩니다. 그런데 이게 Autowired로 호출될 때마다 Bean이 설정된 함수가 호출되는 것이 아니고 Singleton의 개념과 비슷하게 서버가 기동할 때, 한번 함수를 호출해서 그 결과를 메모리에 할당합니다.

그리고 Autowired로 호출될 때마다 메모리에 할당된 인스턴스를 가져다 사용하는 구조입니다.

이게 주의할 점이 반환 값이 같은 인스턴스 타입일 경우 에러가 발생할 수 있습니다. 클래스 명을 같은 이름으로 만들 수가 없습니다만 함수 반환은 그게 가능하니 의존성 주입에서 인스턴스를 받는 입장에서는 어떤 것을 받을 지 에러가 발생하네요

이렇게 에러가 발생합니다. 즉, 하나의 타입만 Bean 등록을 하거나 Bean에 name을 넣어서 DI쪽에서는 @Qualifier 어노테이션으로 받을 수 있습니다.

저는 그래서 애초에 Bean 어노테이션은 name을 설정해 놓습니다. 물론 name은 String 값이기 때문에 여기가 다른 값과 겹치게 되면 에러가 발생합니다.


여기서 또 다른 부분이 의문이 생기는 데 Repository 어노테이션, Controller 어노테이션, Service 어노테이션입니다.

Repository 어노테이션, Controller 어노테이션, Service 어노테이션도 Component 어노테이션과 비슷하게 scan-auto-detection와 dependency injection를 사용할 수 있는데, Component 어노테이션과 무슨 차이가 있는 것일까?


쉽게 생각하면 Repository 어노테이션, Controller 어노테이션, Service 어노테이션이 Component 어노테이션을 상속 받은 어노테이션이라고 생각하면 됩니다. 실제는 상속을 받은 것은 아닙니다만 개념적으로 그렇습니다.

즉, 위 스케줄러에 제가 Component 어노테이션을 사용해서 @Scheduled 어노테이션으로 cron 스케줄러를 사용했습니다.


저기에 Component 어노테이션 대신에 Repository 어노테이션, Controller 어노테이션, Service 어노테이션를 사용하면? 작동됩니다. 아무런 에러없이 똑같이 작동이 됩니다.

Controller는 Component 기능 + 요청 매핑이 가능한 기능이 있습니다. 즉 RequestMapping 어노테이션을 사용할 수 있는 어노테이션인 것입니다.

그래서 Component 어노테이션이 들어가는 클래스에 Repository 어노테이션, Controller 어노테이션, Service 어노테이션를 대신 사용해도 문제없지만, Repository 어노테이션, Controller 어노테이션, Service 어노테이션를 사용하는 어노테이션에 Component 어노테이션이나 다른 어노테이션을 사용하면 에러가 발생할 것입니다.

Controller 어노테이션은 RequestMapping 어노테이션을 사용할 수 있는 어노테이션이라고 생각하면 Repository 어노테이션과는 무슨 차이가 있을까?


이게 저도 정확한 의미를 잘 모르겠는데 JPA를 사용할 때에 특정 에러, 즉 디버깅 중에 에러를 잡지 않는 요소(RuntimException)가 있는데, 이런 에러를 추가해 주는 기능을 하고 있다라고 합니다.

링크 - https://www.baeldung.com/spring-component-repository-service

사실 이게 저도 무슨 소리인지 몰라 여러가 테스트를 해 봤는데 명확한 차이를 모르겠더군요...


Service 어노테이션은 현재로서는 Component 어노테이션과 크게 차이가 있는 어노테이션이 아니라고 합니다.


즉, 우리가 Component 어노테이션을 사용해도 되는 데 업무적 특성상, Controller 어노테이션, Repository 어노테이션, Service 어노테이션으로 구분해서 사용하게 됩니다. Component 어노테이션으로 하면 프로젝트가 커질수록 살짝 헤갈리는 요소가 있긴 하더라고요..

Controller는 RequestMapping용으로 사용하고, 우리가 기능적인 함수등을 만들 때는 Service 어노테이션, 데이터와 관계된 것은 Repository 어노테이션을 사용하는 것이 좋을 듯 싶습니다.


여기까지 Java의 Spring boot에서 cron 스케줄러와 Component 어노테이션에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.