주기적으로 유저에게 메일을 보내보자
지난 시간동안 면접 질문을 chatGPT에 요청에 만들어 저장하고 이를 사용자에게 메일로 전송하는 로직을 완성했다. 이제 이 로직들을 호출하는 주체가 필요한데 추가적인 요구사항으로 주기적으로 작업을 수행해야한다.
이를 위해, Scheduler를 사용하자. Spring은 Spring Scheduler와 Spring Quartz를 지원한다. 둘다 스케쥴링할 수 있게 도와주는데 그 차이를 먼저 알아보자.
Spring Scheduler
- Spring-boot-starter 에 기본으로 있기 때문에 추가적인 의존성이 필요 없음
- 스케쥴링할 메서드는 void type이어야 하고 파라미터를 가질 수 없음
- 1개의 스레드에서 수행되므로 1개의 작업이 끝나지 않으면 다음 작업은 수행되지 않음
그럼, 1개의 작업이 끝나지 않은 상태에서 다른 스케쥴러를 실행할 시간이 되면 이 작업은 수행 못하고 묻히게 되는가? 그럼 큰 일나는 거 아닌가? 해서 실험을 해봤다.
@Component
public class Scheduler {
@Scheduled(fixedDelay = 1000)
public void fixedDelayTask() throws InterruptedException {
LocalDateTime dateTime = LocalDateTime.now();
System.out.println("Fixed delay task - " + dateTime);
Thread.sleep(5000);
}
@Scheduled(cron = "40 * * * * ?") // cron에 따라 실행
public void taskUsingCron() {
LocalDateTime dateTime = LocalDateTime.now();
System.out.println(dateTime + " : 내가 보여??");
}
}
fixedDelay는 한 스케쥴링 작업이 끝나고 난 뒤 지정된 ms 만큼 대기한 후 작업을 수행한다는 의미이다. 이와 관련된 여러 블로그들이 많으니 참고하길 바란다. fixedRate, fixedDelay, cron 방식이 있다. 한 가지 추천하는 블로그 링크를 달아두겠다.
cron 표현식을 통해 매 시간 40초가 되면 현재 시간과 함께 내가 보여?? 라는 텍스트가 출력된다. 그런데, fixedDelayTask 메서드에서 5초씩 기다리고 있다. 이 때, 40초가 되었는데 Thread.sleep에 의해 스레드가 정지해 있다면 즉, 아직 다른 스케쥴링이 진행중이라면 이 스케쥴링은 어떻게 될까?
결론을 보자면 39초에 FixedDelayTask가 수행되었고 스레드는 5초간 중단되어 있다. 공교롭게도 40초가 되기 직전인데 40초가 되면 내가 보여??라는 텍스트가 출력되어야 한다. 아래 줄에 내가 보여??라는 텍스트의 출력 시간을 보자. 44초다! 40초가 아니라 44초라는 의미는 스케쥴링이 씹히는 게 아니라 밀려서 수행된다는 것을 알 수 있다.
하지만, 스케쥴링은 시간이 중요한 작업일 수도 있는데 만약 여러 개의 스케쥴러가 같이 돌아가는 환경이라면 스케쥴링을 사용하는 것이 적합하지 않을 수도 있겠다.
Spring Quartz
또 다른 스케쥴링 기능으로 Spring Quartz가 있는데 스케쥴러 간의 클러스터링 기능이나 스케쥴러 실패에 대한 후처리 기능을 제공한다. 하지만, 그렇기 때문에 구조가 복잡하다.
이 프로젝트에서는 스케쥴러 간의 클러스터링이 필요 없고 단순히 질문을 가져다가 저장하고 메일링해주면 된다. 프로젝트의 사용자 수도 많을 것이라 예상하지 않기 때문에 일단은 스프링 스케쥴러를 사용해 구현하자
Scheduler 클래스를 만들고 loadQuestions와 sendQuestions 메서드를 만들자. 앞서 설명했듯이 void 리턴 타입의 파라미터는 없는 메서드로 만들어야 한다. 다 만든 예시는 다음과 같다.
@Component
@Slf4j
public class Scheduler {
private final QuestionLoader questionLoader;
private final QuestionSender questionSender;
public Scheduler(QuestionLoader questionLoader, QuestionSender questionSender) {
this.questionLoader = questionLoader;
this.questionSender = questionSender;
}
@Scheduled(cron = "0 0 4 * * *")
public void loadQuestions() {
String startTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));
log.info(MessageFormat.format("{0} - [QuestionSender] load questions to database.", startTime));
questionLoader.load();
String endTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));
log.info(MessageFormat.format("{0} - [QuestionSender] load complete.", endTime));
}
@Scheduled(cron = "0 32 6-23,0-3 * * *")
public void sendQuestions() {
String startTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));
log.info(MessageFormat.format("{0} - [QuestionSender] send questions to members.", startTime));
questionSender.send();
String endTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"));
log.info(MessageFormat.format("{0} - [QuestionSender] send complete.", endTime));
}
}
여기서 헤맸던 점은 자정이 넘어가는 시간의 경우 cron 표현식으로 어떻게 표현할까 였다. 새벽 6시부터 익일 새벽 3시까지에 대해 적용하기 위해 처음에 6-3으로 했었는데 에러가 발생했다. 그에 대한 해결방법은 24시를 기준으로 끊어서 쉼표(,)로 구분하는 것이다. 위 예시처럼 6-23, 0-3으로 표현하면 제대로 적용된다.
chat GPT에 요청해 질문 데이터를 수집하는 작업은 사람들이 메일로 받지 않을 시간대인 새벽 4시로 지정해두었다. 그 외의 시간으로 새벽 6시부터 익일 새벽 3시까지는 사용자가 메일을 받을 수 있는 시간대로 설정해두었다. 추후 만들 사용자 입력 웹 페이지에서도 이에 맞추어 설정할 예정이다.
참고
https://sabarada.tistory.com/113
https://unix.stackexchange.com/questions/67158/crontab-entry-with-hour-range-going-over-midnight
'프로젝트' 카테고리의 다른 글
Chat GPT 활용 서비스 구현하기 - 7. 회고 (0) | 2023.05.06 |
---|---|
Chat GPT 활용 서비스 구현하기 - 5. 메일로 전송하기 (2) | 2023.05.05 |
Chat GPT 활용 서비스 구현하기 - 4. 데이터 저장하기 (0) | 2023.05.04 |
Chat GPT 활용 서비스 구현하기 - 3. Open AI API 연동하기 ver2. (0) | 2023.05.02 |
Chat GPT 활용 서비스 구현하기 - 2. Open AI API 연동하기 (0) | 2023.05.02 |