Spring의 @Scheduled 어노테이션은 Cron 표현식으로 실행 시점을 지정합니다.
순서: 초 / 분 / 시 / 일 / 월 / 요일
예시: "0 0 1 * * *" → 매일 새벽 1시 0분 0초
| 필드 | 허용 값 | 예시 |
|---|---|---|
| 초 | 0~59 | 0 |
| 분 | 0~59 | 30 |
| 시 | 0~23 | 1 |
| 일 | 1~31 | * (매일) |
| 월 | 1~12 | * (매월) |
| 요일 | 0~7 (0,7=일요일) | * |
공식 문서: CronExpression - Spring Docs
[Scheduler 실행]
↓
DB에서 전체 관심상품 목록 조회
↓
각 상품 제목으로 네이버 쇼핑 API 검색 (1초 간격 > 네이버 정책)
↓
검색 결과의 최저가(lprice)로 DB 업데이트
@EnableScheduling 활성화스케줄러 기능을 사용하려면 메인 클래스에 어노테이션을 추가해야 합니다.
@EnableScheduling // 스케줄러 활성화
@EnableJpaAuditing // 시간 자동 변경 활성화
@SpringBootApplication
public class MyselectshopApplication {
public static void main(String[] args) {
SpringApplication.run(MyselectshopApplication.class, args);
}
}
@Slf4j(topic = "Scheduler") // 로그 출력 시 태그명 지정
@Component // 스프링 빈으로 등록
@RequiredArgsConstructor
public class Scheduler {
private final NaverApiService naverApiService;
private final ProductService productService;
private final ProductRepository productRepository;
// 초, 분, 시, 일, 월, 요일 순서
@Scheduled(cron = "0 0 1 * * *") // 매일 새벽 1시 실행
public void updatePrice() throws InterruptedException {
log.info("가격 업데이트 실행");
// 1. 모든 관심상품 조회
List productList = productRepository.findAll();
for (Product product : productList) {
// 2. 네이버 API 호출 제한을 고려하여 1초 대기
TimeUnit.SECONDS.sleep(1);
// 3. 상품 제목으로 네이버 쇼핑 API 검색
String title = product.getTitle();
List itemDtoList = naverApiService.searchItems(title);
if (itemDtoList.size() > 0) {
ItemDto itemDto = itemDtoList.get(0); // 첫 번째(가장 관련성 높은) 결과 사용
// 4. 해당 상품의 최저가 업데이트
Long id = product.getId();
try {
productService.updateBySearch(id, itemDto);
} catch (Exception e) {
log.error(id + " : " + e.getMessage()); // 개별 실패는 로그만 남기고 계속 진행
}
}
}
}
}
updateBySearch// 스케줄러에서 호출하는 최저가 업데이트 메서드
@Transactional
public void updateBySearch(Long id, ItemDto itemDto) {
// 상품 조회, 없으면 예외 발생
Product product = productRepository.findById(id).orElseThrow(
() -> new NullPointerException("해당 상품은 존재하지 않습니다.")
);
// 최저가 업데이트
product.updateByItemDto(itemDto);
}
updateByItemDto// 네이버 API 응답으로 최저가(lprice) 갱신
public void updateByItemDto(ItemDto itemDto) {
this.lprice = itemDto.getLprice();
}
TimeUnit.SECONDS.sleep(1) 을 사용하는 이유
개별 try-catch 처리 이유
@Component로 등록하는 이유
@Scheduled가 동작하려면 해당 클래스가 스프링 컨테이너에 빈으로 등록되어 있어야 합니다.@Component는 이를 위한 가장 기본적인 방법입니다.단일 서버에서는 @Scheduled가 충분하지만, 멀티 인스턴스 환경(서버가 n개)에서는
모든 서버에서 동시에 스케줄러가 실행되는 문제가 발생할 수 있습니다.
이런 경우에는 다음과 같은 도구를 검토할 수 있습니다.
학습 단계에서는
@Scheduled의 구조와 Cron 표현식을 확실히 이해하는 것이 우선입니다.
규모가 커진 환경에서는 Scheduler가 문제가 될 수 있다는 것을 기억.
현재 코드에서 NaverApiService 내부적으로 사용 중인 RestTemplate은 Spring 5부터 유지보수 모드로 전환되었습니다.
신규 프로젝트에서는 WebClient (Spring WebFlux) 또는 RestClient (Spring 6.1 / Boot 3.2+에서 추가)를 사용하는 방향이 권장됩니다.
다만 레거시 코드베이스에서는 RestTemplate이 여전히 많이 사용되므로, 두 방식 모두 파악해두는 것이 좋습니다.