BatchUtils에 static <T> T withRetryAndTiming(String label, int maxAttempts, Supplier<T> action)을 추가하세요. timed와 retry를 겹쳐서 재시도 전체 시간이 측정되게 합니다.
그 다음 Runnable을 받는 io 오버로딩 static void io(IORunnable action)(IORunnable은 void run() throws IOException)을 추가해 w.write(...)를 return null 없이 쓸 수 있게 하세요.
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
public class Q1 {
static <T> T withRetryAndTiming(String label, int maxAttempts, Supplier<T> action) {
return BatchUtils.timed(label, () -> BatchUtils.retry(maxAttempts, action)); // timed가 바깥
}
@FunctionalInterface
interface IORunnable { void run() throws IOException; }
static void io(IORunnable action) {
try { action.run(); }
catch (IOException e) { throw new UncheckedIOException(e); }
}
public static void main(String[] args) throws IOException {
AtomicInteger n = new AtomicInteger();
String r = withRetryAndTiming("connect", 3, () -> {
if (n.incrementAndGet() < 3) throw new RuntimeException("timeout");
return "ok";
});
// 출력: 재시도 1/3: timeout
// 출력: 재시도 2/3: timeout
// 출력: [connect] 0 ms
System.out.println(r); // 출력: ok
Path tmp = Files.createTempFile("q1", ".txt");
BatchUtils.withWriter(tmp, w -> io(() -> { w.write("no return null"); w.newLine(); })); // return null 불필요
String line = BatchUtils.withReader(tmp, br -> BatchUtils.io(br::readLine));
System.out.println(line); // 출력: no return null
Files.delete(tmp);
}
}JobParams에 LocalDate endDate(선택, 기본값은 date와 같음)를 추가하고, JobRunner에 runRange(String[] args)를 추가하세요.
date부터 endDate까지 BatchUtils.dateRange로 일자 목록을 만들어 하루씩 잡을 실행하고, 각 리포트의 summary()를 출력한 뒤 오류가 하나라도 있으면 true를 반환합니다. endDate < date면 IllegalArgumentException.
import java.time.LocalDate;
import java.util.List;
import java.util.function.Function;
public class Q2 {
// JobParams 수정본 (필드 추가 + compact 생성자 검증)
record JobParams2(String job, LocalDate date, LocalDate endDate, int chunkSize) {
JobParams2 {
if (job == null || job.isBlank()) throw new IllegalArgumentException("--job 필수");
if (endDate.isBefore(date)) throw new IllegalArgumentException("--end는 --date 이후여야 함");
}
static JobParams2 of(String job, String ymd, String endYmd, int chunk) {
LocalDate d = BatchUtils.parseYmd(ymd);
LocalDate e = endYmd == null ? d : BatchUtils.parseYmd(endYmd); // 기본값 = date
return new JobParams2(job, d, e, chunk);
}
}
static boolean runRange(JobParams2 p, Function<LocalDate, BatchReport> job) {
boolean anyError = false;
for (LocalDate d : BatchUtils.dateRange(p.date(), p.endDate())) {
BatchReport r = job.apply(d);
System.out.println(r.summary());
anyError |= r.hasErrors();
}
return anyError;
}
public static void main(String[] args) {
JobParams2 p = JobParams2.of("daily", "20240315", "20240317", 100);
boolean failed = runRange(p, d ->
d.getDayOfMonth() == 16 ? BatchReport.of("daily-" + d).error("입력 없음") : BatchReport.of("daily-" + d).read(10).written(10));
// 출력: [daily-2024-03-15] SUCCESS read=10 written=10 skipped=0 errors=0
// 출력: [daily-2024-03-16] COMPLETED_WITH_ERRORS read=0 written=0 skipped=0 errors=1
// 출력: [daily-2024-03-17] SUCCESS read=10 written=10 skipped=0 errors=0
System.out.println("failed=" + failed); // 출력: failed=true
try {
JobParams2.of("daily", "20240317", "20240315", 100);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage()); // 출력: --end는 --date 이후여야 함
}
}
}static int copyFiltered(Path in, Path out, int chunkSize, Predicate<String> keep)를 작성하세요. forEachLineChunk로 in을 읽고, 각 청크에서 keep을 통과한 줄만 out에 원자적으로 씁니다(전체가 끝난 뒤 한 번에 writeAtomically).
유지된 줄 수를 반환합니다. 진행률은 progress로 청크마다 출력하되 총 청크 수를 미리 알 수 없으므로 "청크 N 처리" 형식으로 바꾼 Consumer<Integer>를 직접 만드세요.
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;
public class Q3 {
static int copyFiltered(Path in, Path out, int chunkSize, Predicate<String> keep) {
List<String> kept = new ArrayList<>();
int[] chunkNo = {0};
Consumer<Integer> progress = n -> System.out.println(" 청크 " + n + " 처리");
BatchUtils.forEachLineChunk(in, chunkSize, lines -> {
for (String l : lines) if (keep.test(l)) kept.add(l);
progress.accept(++chunkNo[0]);
});
BatchUtils.writeAtomically(out, w -> kept.forEach(l -> BatchUtils.io(() -> { w.write(l); w.newLine(); return null; })));
return kept.size();
}
public static void main(String[] args) throws Exception {
Path dir = Files.createTempDirectory("q3");
Path in = dir.resolve("in.txt"), out = dir.resolve("out.txt");
BatchUtils.withWriter(in, w -> BatchUtils.io(() -> {
for (int i = 1; i <= 7; i++) { w.write("row" + i + (i % 2 == 0 ? ",ok" : ",skip")); w.newLine(); }
return null;
}));
int n = copyFiltered(in, out, 3, l -> l.endsWith(",ok"));
// 출력: 청크 1 처리
// 출력: 청크 2 처리
// 출력: 청크 3 처리
System.out.println(n); // 출력: 3
List<String> result = BatchUtils.withReader(out, r -> r.lines().toList());
System.out.println(result); // 출력: [row2,ok, row4,ok, row6,ok]
System.out.println(Files.exists(dir.resolve("out.txt.tmp"))); // 출력: false
Files.delete(in); Files.delete(out); Files.delete(dir);
}
}