공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 09 / 22

@Scheduled 운영

fixedRate·fixedDelay·cron, 예외·겹침·다중 인스턴스·종료
섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

5. 자주 하는 실수 (Tip)

❌ 실수 1: 예외 한 번에 배치가 영구 정지되는데 아무도 모른다

@Scheduled 메서드가 예외를 던지면 그 스케줄은 다시 돌지 않습니다. 로그도 남지 않아서, 다음 날 데이터가 안 쌓인 걸 보고서야 알게 됩니다. SafeTask 처럼 메서드 본문 전체를 try-catch(Throwable) 로 감싸고 실패를 로그·메트릭으로 남겨야 합니다.

❌ 실수 2: 스레드 1개라 느린 배치가 다른 배치를 밀어낸다

Spring 기본 풀은 스레드 1개입니다. 배치를 하나씩 추가하다 보면 어느 순간 서로 밀어내기 시작합니다. spring.task.scheduling.pool.size 를 배치 개수에 맞춰 늘려야 합니다.

❌ 실수 3: fixedRate 로 밀린 회차가 몰아서 실행돼 DB 부하가 튄다

작업이 간격보다 길어지는 날(예: 데이터량 급증)에 fixedRate 는 밀린 회차를 간격 없이 연달아 돌립니다. 짧은 시간에 DB 커넥션을 몰아 써서 부하가 튑니다. 부하가 우려되면 fixedDelay 로 바꾸거나, 실행 시간이 임계치를 넘으면 이번 회차를 건너뛰는 로직을 추가합니다.

❌ 실수 4: 인스턴스 2대에서 리포트가 두 번 발송된다

서버를 2대로 늘렸는데 겹침 방지 로직이 프로세스 안(AtomicBoolean)에만 있으면, 두 인스턴스가 각자 잠금을 얻은 것처럼 동작해 같은 리포트를 두 번 보냅니다. 프로세스를 넘는 잠금(ShedLock, DB 행 잠금)이 필요합니다.

❌ 실수 5: cron 을 5필드로 써서 파싱 오류가 난다

java
// ❌ 리눅스 crontab 습관대로 5필드로 씀
@Scheduled(cron = "0 2 * * *")   // IllegalArgumentException
// ✅ Spring 은 초가 맨 앞에 온다
@Scheduled(cron = "0 0 2 * * *")

리눅스 crontab 경험이 있으면 초 필드를 빠뜨리기 쉽습니다. Spring 은 초까지 포함한 6필드이므로 앱 시작 시점에 IllegalArgumentException 으로 바로 드러납니다.

❌ 실수 6: 컨테이너가 UTC 라 새벽 2시 배치가 오전 11시에 돈다

zone 속성 없이 배포한 cron 배치는 JVM 기본 시간대를 씁니다. 도커 이미지 기본 시간대가 UTC 라면, 02:00 은 KST 로 11:00 이 됩니다. zone = "Asia/Seoul" 을 항상 명시해야 합니다.

❌ 실수 7: @Scheduled 메서드가 private·인자 있음·내부 호출이라 프록시를 안 탄다

java
// ❌ private 이거나 인자가 있으면 스케줄 등록 자체가 안 되거나 무시된다
@Scheduled(fixedDelay = 60000)
private void sync(String region) { /* ... */ }

Spring 의 스케줄링은 프록시 기반입니다. private 메서드는 프록시가 감쌀 수 없고, 인자가 있는 메서드는 스케줄러가 호출할 방법이 없습니다. public·인자 없음이 기본 요건입니다. 같은 클래스 안에서 this.sync() 로 직접 호출하는 것도 프록시를 건너뛰어 겹침 방지 등 부가 로직이 적용되지 않으니 피해야 합니다.

❌ 실수 8: @Transactional 을 @Scheduled 메서드에 직접 붙여 긴 트랜잭션을 잡는다

배치 전체를 한 트랜잭션으로 묶으면 몇만 건을 처리하는 동안 커넥션 하나를 계속 물고 있습니다. 다른 요청이 커넥션 풀 고갈로 대기합니다. @Scheduled 메서드는 짧게 단위를 나눠 각 단위마다 별도 트랜잭션(chunk)으로 처리해야 합니다.

❌ 실수 9: shutdown 때 배치 중간에 끊겨 반쪽 커밋이 남는다

shutdownNow() 를 바로 호출하거나 awaitTermination 대기 시간을 너무 짧게 잡으면, 진행 중인 배치가 인터럽트로 끊깁니다. 이미 일부만 커밋된 상태로 남을 수 있습니다. shutdown() 뒤 충분한 awaitTermination 을 기다리고, 배치는 재실행해도 안전하게 멱등 설계를 해 둬야 합니다.

❌ 실수 10: lockAtMostFor 를 짧게 잡아 잠금이 먼저 풀린다

ShedLock 의 lockAtMostFor 를 작업의 평소 소요 시간과 같게 잡으면, 데이터가 늘어 작업이 조금만 길어져도 잠금이 먼저 풀립니다. 그 사이 다른 인스턴스가 같은 작업을 또 시작합니다. lockAtMostFor 는 최대 예상 시간의 2~3배로 여유 있게 잡아야 합니다.

자주 하는 실수 (Tip)
  • ❌ 실수 1: 예외 한 번에 배치가 영구 정지되는데 아무도 모른다
  • ❌ 실수 2: 스레드 1개라 느린 배치가 다른 배치를 밀어낸다
  • ❌ 실수 3: fixedRate 로 밀린 회차가 몰아서 실행돼 DB 부하가 튄다
  • ❌ 실수 4: 인스턴스 2대에서 리포트가 두 번 발송된다
  • ❌ 실수 5: cron 을 5필드로 써서 파싱 오류가 난다
  • ❌ 실수 6: 컨테이너가 UTC 라 새벽 2시 배치가 오전 11시에 돈다
  • ❌ 실수 7: @Scheduled 메서드가 private·인자 있음·내부 호출이라 프록시를 안 탄다
  • ❌ 실수 8: @Transactional 을 @Scheduled 메서드에 직접 붙여 긴 트랜잭션을 잡는다
  • ❌ 실수 9: shutdown 때 배치 중간에 끊겨 반쪽 커밋이 남는다
  • ❌ 실수 10: lockAtMostFor 를 짧게 잡아 잠금이 먼저 풀린다
이전 섹션4 응용 변형 예제5 / 7다음 섹션6 연습 문제