공공부하자개발 · 영어 학습 노트
자바
실무 확장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정리‹ 이전다음 ›

6. 연습 문제

이 레슨의 네 파일에 직접 코드를 덧붙이는 문제 5개입니다. 정답을 보기 전에 먼저 힌트만 보고 스스로 작성해 보세요.

문제 1. SafeTask 에 연속 실패 알림 훅 추가

SafeTask 에 연속 3회 실패하면 Consumer<String> 훅을 호출하는 기능을 추가하세요. 힌트: 성공하면 연속 실패 카운터를 0으로 되돌리고, 실패할 때만 증가시킵니다.

java
private final AtomicInteger consecutiveFails = new AtomicInteger();
private Consumer<String> onAlert = msg -> {};   // 기본은 아무것도 안 함

public void onAlert(Consumer<String> hook) { this.onAlert = hook; }

// run() 의 catch, finally 를 아래처럼 바꾼다
} catch (Throwable t) {
    failures.incrementAndGet();
    log("FAIL  " + t);
    if (consecutiveFails.incrementAndGet() >= 3)
        onAlert.accept(name + " 연속 3회 실패: " + t);
} finally {
    if (running.get() == false) {}   // 자리 표시, 아래 두 줄이 핵심
}
// 성공 분기(body.run() 다음)에 아래 한 줄 추가
consecutiveFails.set(0);

연속 실패 카운터를 runs/failures 와 별도로 둬야, "실패했다가 성공했다가" 를 반복하는 배치에서 오탐이 안 납니다.

문제 2. MiniCron 에 L(말일) 지원 추가

MiniCron 의 일(day) 필드에 L 을 넣으면 그 달의 말일로 해석되게 하세요. 힌트: 파싱 시점에는 몇 일까지인지 모르므로, next() 에서 날짜를 비교할 때 그 달의 마지막 날인지 직접 계산합니다.

java
private boolean lastDayOfMonth = false;

// parse 호출 직전, day 필드에서 "L" 을 감지
if (parts[3].equals("L")) { lastDayOfMonth = true; parts[3] = "1"; }  // 자리만 채움

// next() 의 일 필드 검사 줄을 아래로 교체
boolean dayOk = lastDayOfMonth
        ? t.getDayOfMonth() == t.lengthOfMonth()
        : fields[3].get(t.getDayOfMonth());
if (!dayOk || !fields[5].get(t.getDayOfWeek().getValue() % 7)) {
    t = t.plusDays(1).withHour(0).withMinute(0).withSecond(0); continue;
}

lengthOfMonth() 가 2월 28·29일까지 알아서 계산해 주므로, 윤년을 따로 신경 쓸 필요가 없습니다.

문제 3. 지수 백오프 재시도를 스케줄러로 구현

외부 API 호출이 실패하면 Thread.sleep 없이, schedule 로 다음 시도를 예약하는 지수 백오프를 구현하세요. 힌트: 재시도 자신을 다시 ses.schedule 로 넘기는 재귀 형태입니다.

java
void callWithBackoff(ScheduledExecutorService ses, int attempt) {
    try {
        callExternalApi();                          // 성공하면 끝
    } catch (Exception e) {
        if (attempt >= 5) { log("포기: " + e); return; }
        long delayMs = 1000L << attempt;             // 1s, 2s, 4s, 8s, 16s
        log("실패, " + delayMs + "ms 후 재시도 " + (attempt + 1));
        ses.schedule(() -> callWithBackoff(ses, attempt + 1),
                delayMs, TimeUnit.MILLISECONDS);
    }
}

Thread.sleep 으로 재시도하면 그 스레드가 대기 시간 내내 풀을 점유합니다. schedule 로 다음 시도를 예약하면 스레드를 즉시 반납하므로 같은 풀의 다른 작업을 막지 않습니다.

문제 4. LeaderLock 을 DB 행 기반으로 바꾸기

LeaderLock 을 파일 잠금 대신 Oracle SELECT ... FOR UPDATE NOWAIT 로 바꾸세요. 힌트: 잠금 행은 미리 하나 만들어 두고, 트랜잭션이 커밋·롤백되면 잠금이 자동으로 풀립니다.

sql
CREATE TABLE job_lock (name VARCHAR2(64) PRIMARY KEY);
INSERT INTO job_lock VALUES ('dailyReport');
java
public static boolean tryRun(Connection conn, String name,
                              Runnable body) throws SQLException {
    conn.setAutoCommit(false);
    try (PreparedStatement ps = conn.prepareStatement(
            "SELECT name FROM job_lock WHERE name = ? FOR UPDATE NOWAIT")) {
        ps.setString(1, name);
        ps.executeQuery();               // 잠긴 행이면 SQLException(락 대기 아님)
    } catch (SQLException e) {
        conn.rollback();
        return false;                     // 다른 인스턴스가 이미 실행 중
    }
    try { body.run(); conn.commit(); return true; }
    catch (RuntimeException e) { conn.rollback(); throw e; }
}

FOR UPDATE NOWAIT 는 잠긴 행이면 대기하지 않고 즉시 예외를 던지므로, tryLock 과 같은 "기다리지 않는다" 의미가 됩니다. MERGE 로 만료 시각을 같이 관리하면 ShedLock 과 비슷한 구조가 됩니다.

문제 5. 평소보다 3배 느린 실행 찾기

job_run 이력 테이블에서, 같은 배치의 평소 소요 시간보다 3배 이상 걸린 실행을 찾는 SQL 을 작성하세요. 힌트: 배치별 평균을 서브쿼리로 구하고, 각 실행과 조인해서 비교합니다.

sql
SELECT r.job_name, r.started_at,
       (r.finished_at - r.started_at) * 24 * 60 AS took_min
FROM   job_run r
JOIN  (SELECT job_name,
              AVG((finished_at - started_at) * 24 * 60) AS avg_min
       FROM   job_run
       WHERE  status = 'OK'
       GROUP  BY job_name) avg_t
  ON  r.job_name = avg_t.job_name
WHERE  r.status = 'OK'
  AND  (r.finished_at - r.started_at) * 24 * 60 > avg_t.avg_min * 3;

이 조회를 알림 배치로 스케줄링해 두면, 배치가 "돌긴 도는데 점점 느려지는" 초기 신호를 로그를 일일이 보지 않고 잡을 수 있습니다.

연습 문제
    이전 섹션5 자주 하는 실수 (Tip)6 / 7다음 섹션7 정리