public static <T> T withReader(Path file, Function<BufferedReader, T> body) {
try (BufferedReader r = Files.newBufferedReader(file, UTF_8)) { // 열기
return body.apply(r); // 본문은 호출자의 람다
} catch (IOException e) { // 닫기는 try-with-resources
throw new UncheckedIOException(e);
}
}| 부분 | 누가 | 몇 번 작성 |
|---|---|---|
| 열기, 인코딩 지정, 닫기, 예외 변환 | 헬퍼 | 1번 |
| 무엇을 읽어 무엇을 돌려줄지 | 잡(람다) | 잡마다 |
호출자는 BufferedReader를 닫는 것을 잊을 수 없습니다. 람다가 끝나면 try-with-resources가 닫습니다. 람다 안에서 예외가 나도 닫힙니다. 이것이 스프링의 JdbcTemplate, TransactionTemplate이 하는 일과 같은 원리(execute-around 패턴)입니다.
Function, Consumer는 체크 예외를 던질 수 없습니다. BufferedReader.readLine()은 IOException을 던지므로 람다 안에서 그냥 부르면 컴파일 오류입니다. 두 가지 해결책:
| 방법 | 형태 | 장단점 |
|---|---|---|
| 체크 예외를 던지는 자체 함수형 인터페이스 | IOAction<T> { T run() throws IOException; } + io() 래퍼 |
io(() -> r.readLine()) 로 감쌈. 명시적 |
헬퍼가 UncheckedIOException으로 변환 |
withReader 안 catch (IOException e) |
호출자는 언체크만 봄. 복구가 필요하면 getCause() |
배치에서 IOException은 대개 복구 불가(파일이 없거나 디스크 문제)이므로 언체크로 바꿔 잡 최상위에서 한 번에 처리하는 것이 일반적입니다.
파일 1,000,000줄, size=1000
┌───────────┐ ┌───────────┐ ┌───────────┐
│ chunk 1 │ │ chunk 2 │ ... │ chunk 1000│
│ 1~1000 │ │ 1001~2000 │ │ ... │
└───────────┘ └───────────┘ └───────────┘
handler handler handlerforEachLineChunk는 Files.lines(지연 스트림)에서 한 줄씩 버퍼에 담다가 size가 차면 handler를 부르고 버퍼를 새로 만듭니다. 전체를 List로 올리지 않으므로 파일 크기와 무관하게 메모리는 청크 하나 분량입니다. 마지막 자투리(buf가 비어 있지 않음)를 잊으면 마지막 청크가 유실됩니다. 03 레슨의 원리를 메서드 하나로 캡슐화한 것입니다.
timed("job", () -> retry(3, () -> job.run(params)))
│ │
│ └ 실패하면 3번까지
└ 전체 시간 측정timed, retry, processWithSkip은 원래 작업을 모르고, 원래 작업도 이들을 모릅니다. 겹치는 순서가 의미를 정합니다. timed(retry(...))는 재시도 전체 시간을, retry(timed(...))는 시도별 시간을 잽니다.
args는 String[]이라 순서에 의존하고 타입이 없습니다. --job=daily --date=20240315 형식으로 받아 record로 변환하면 타입(LocalDate, int)이 생기고, compact 생성자에서 검증하며, toString이 로그에 그대로 찍힙니다. 기본값은 파싱 단계에서 채웁니다. 스프링 배치의 JobParameters가 하는 일의 핵심입니다.
| 위험 | 대응 메서드 |
|---|---|
| 쓰다가 죽어 반쪽 파일이 남음 | writeAtomically: .tmp에 쓰고 Files.move(ATOMIC_MOVE) |
| 같은 날짜로 두 번 실행되어 결과가 중복 | 출력 파일 존재 확인 후 스킵 |
| 임시 디렉터리·락 파일이 남음 | finally + Runtime.addShutdownHook |
| 실패 건이 어디 갔는지 모름 | BatchReport.error + 에러 로그 파일 분리 |
finally는 그 메서드가 정상/예외로 끝날 때 실행되고, shutdown hook은 JVM이 종료될 때(정상 종료, System.exit, SIGTERM) 실행됩니다. kill -9에는 둘 다 무력하므로 "재실행하면 정리되는" 멱등 설계가 최후의 방어선입니다.