이 레슨의 네 파일에 직접 코드를 덧붙이는 문제 5개입니다. 정답을 보기 전에 먼저 힌트만 보고 스스로 작성해 보세요.
SafeTask 에 연속 3회 실패하면 Consumer<String> 훅을 호출하는 기능을 추가하세요. 힌트: 성공하면 연속 실패 카운터를 0으로 되돌리고, 실패할 때만 증가시킵니다.
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 와 별도로 둬야, "실패했다가 성공했다가" 를 반복하는 배치에서 오탐이 안 납니다.
MiniCron 의 일(day) 필드에 L 을 넣으면 그 달의 말일로 해석되게 하세요. 힌트: 파싱 시점에는 몇 일까지인지 모르므로, next() 에서 날짜를 비교할 때 그 달의 마지막 날인지 직접 계산합니다.
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일까지 알아서 계산해 주므로, 윤년을 따로 신경 쓸 필요가 없습니다.
외부 API 호출이 실패하면 Thread.sleep 없이, schedule 로 다음 시도를 예약하는 지수 백오프를 구현하세요. 힌트: 재시도 자신을 다시 ses.schedule 로 넘기는 재귀 형태입니다.
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 로 다음 시도를 예약하면 스레드를 즉시 반납하므로 같은 풀의 다른 작업을 막지 않습니다.
LeaderLock 을 파일 잠금 대신 Oracle SELECT ... FOR UPDATE NOWAIT 로 바꾸세요. 힌트: 잠금 행은 미리 하나 만들어 두고, 트랜잭션이 커밋·롤백되면 잠금이 자동으로 풀립니다.
CREATE TABLE job_lock (name VARCHAR2(64) PRIMARY KEY);
INSERT INTO job_lock VALUES ('dailyReport');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 과 비슷한 구조가 됩니다.
job_run 이력 테이블에서, 같은 배치의 평소 소요 시간보다 3배 이상 걸린 실행을 찾는 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;이 조회를 알림 배치로 스케줄링해 두면, 배치가 "돌긴 도는데 점점 느려지는" 초기 신호를 로그를 일일이 보지 않고 잡을 수 있습니다.