공공부하자개발 · 영어 학습 노트
자바
실무 배치대용량 데이터 처리0/6 완료
  • 01배치 프로세스 개념과 아키텍처
  • 02대용량 파일 I/O (NIO, Buffered)
  • 03Chunk 단위 처리와 OOM 방지
  • 04트랜잭션: 커밋과 롤백 시뮬레이션
  • 05Skip과 Retry 로직 구현
  • 06메서드 활용 패턴 (배치 유틸)
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 배치 › 06 / 6

메서드 활용 패턴 (배치 유틸)

섹션 6진행 0 / 6
1왜 배우는가2핵심 원리3패턴 모음4자주 하는 실수 (Tip)5연습 문제6정리‹ 이전다음 ›

4. 자주 하는 실수 (Tip)

❌ 실수 1: withReader 람다에서 스트림을 밖으로 반환

java
Stream<String> lines = BatchUtils.withReader(file, r -> r.lines());
lines.forEach(System.out::println);      // UncheckedIOException: Stream closed

람다가 끝나는 순간 try-with-resources가 BufferedReader를 닫습니다. 지연 스트림은 그 뒤에 읽으려 하므로 실패합니다.

✅ 람다 안에서 소비를 끝내고 결과(리스트, 숫자)를 돌려줍니다.

java
List<String> lines = BatchUtils.withReader(file, r -> r.lines().toList());

❌ 실수 2: 청크 버퍼를 clear()로 재사용

java
List<String> buf = new ArrayList<>();
for (String line : lines) {
    buf.add(line);
    if (buf.size() == size) { executor.submit(() -> save(buf)); buf.clear(); }   // 비동기 작업이 빈 리스트를 봄
}

핸들러가 비동기이거나 청크를 보관하면, clear() 후 같은 객체가 비워지거나 다음 청크로 덮입니다.

✅ 청크마다 새 리스트를 만듭니다: buf = new ArrayList<>(size);

❌ 실수 3: 마지막 자투리 청크 누락

java
for (String line : lines) {
    buf.add(line);
    if (buf.size() == size) { handler.accept(buf); buf = new ArrayList<>(); }
}
// 루프 끝. buf에 남은 3건은?

총 건수가 청크 크기의 배수가 아니면 마지막 몇 건이 처리되지 않습니다. 테스트 데이터가 우연히 배수면 발견하지 못합니다.

✅ 루프 뒤에 if (!buf.isEmpty()) handler.accept(buf);를 반드시 둡니다.

❌ 실수 4: 출력 파일을 직접 쓰고 존재 여부로 완료 판단

java
try (BufferedWriter w = Files.newBufferedWriter(out)) {
    for (...) w.write(...);          // 중간에 죽으면 반쪽 파일
}
// 다음 실행: Files.exists(out) → true → "이미 처리됨"으로 스킵. 반쪽 결과가 영원히 남음

파일이 존재한다는 것과 완전하다는 것은 다릅니다.

✅ writeAtomically처럼 .tmp에 쓰고 ATOMIC_MOVE로 이름을 바꿉니다. 존재 = 완전.

❌ 실수 5: 재시도에 대기 없음, 재시도 불가 예외까지 재시도

java
retry(5, () -> Integer.parseInt("abc"));     // 5번 모두 즉시 같은 NumberFormatException
retry(5, () -> callApi());                   // 서버가 과부하인데 0ms 간격으로 5연타

결정적 실패는 몇 번을 해도 같고, 일시적 실패는 간격 없이 때리면 상대를 더 악화시킵니다.

✅ 재시도할 예외 종류를 지정하고 백오프를 넣습니다.

java
static <T> T retry(int max, long waitMs, Class<? extends Exception> retryOn, Supplier<T> action) {
    for (int i = 1; ; i++) {
        try { return action.get(); }
        catch (RuntimeException e) {
            if (!retryOn.isInstance(e) || i == max) throw e;
            try { Thread.sleep(waitMs * i); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw e; }
        }
    }
}

❌ 실수 6: 유틸 안에서 LocalDate.now() 사용

java
public static String todayFileName(String prefix) {
    return prefix + "_" + LocalDate.now().format(YMD) + ".csv";     // 어제 데이터를 재처리할 방법이 없음
}

날짜가 코드에 박히면 밀린 잡 재실행, 테스트, 자정 근처 실행이 모두 문제가 됩니다.

✅ 날짜는 항상 파라미터(JobParams.date)로 받고, "오늘"은 main에서 한 번만 결정합니다.

❌ 실수 7: shutdown hook에서 오래 걸리는 작업

java
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    uploadAllLogsToS3();          // 수 분 소요. 스케줄러가 타임아웃으로 kill -9
}));

훅은 JVM 종료를 붙잡습니다. 컨테이너 환경은 보통 10~30초 후 강제 종료하므로 그 안에 끝나야 합니다.

✅ 훅에는 삭제·플래그 기록처럼 밀리초 단위 작업만 두고, 무거운 정리는 다음 실행 시작 시 "이전 실행 잔재 정리" 단계로 옮깁니다.

자주 하는 실수 (Tip)
  • ❌ 실수 1: withReader 람다에서 스트림을 밖으로 반환
  • ❌ 실수 2: 청크 버퍼를 clear()로 재사용
  • ❌ 실수 3: 마지막 자투리 청크 누락
  • ❌ 실수 4: 출력 파일을 직접 쓰고 존재 여부로 완료 판단
  • ❌ 실수 5: 재시도에 대기 없음, 재시도 불가 예외까지 재시도
  • ❌ 실수 6: 유틸 안에서 LocalDate.now() 사용
  • ❌ 실수 7: shutdown hook에서 오래 걸리는 작업
이전 섹션3 패턴 모음4 / 6다음 섹션5 연습 문제