공공부하자개발 · 영어 학습 노트
자바
실무 확장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 공부하자
홈 › 실무 확장 › 17 / 22

SFTP·FTP 파일 연계

연계 규격, 원자적 전송·마커 파일, 체크섬, 재처리·중복 방지, 키 인증, JSch
섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

6. 연습 문제

문제 1. `LocalTransfer` 에 다운로드 진행률 로그 추가

LocalTransfer.download 를 스트리밍 복사로 바꾸고, 4096바이트마다 진행 바이트 수를 로그로 남기세요. 힌트: upload 의 버퍼 반복 구조를 그대로 가져와 방향만 바꿉니다.

java
@Override
public void download(String remoteName, String localPath) throws TransferException {
    Path src = root.resolve(remoteName);
    Path dst = Paths.get(localPath);
    try (InputStream in = Files.newInputStream(src);
         OutputStream out = Files.newOutputStream(dst)) {
        byte[] buf = new byte[4096];
        long total = 0;
        int n;
        while ((n = in.read(buf)) != -1) {
            out.write(buf, 0, n);
            total += n;
            System.out.println("다운로드 진행: " + total + " bytes");
        }
    } catch (IOException e) {
        throw new TransferException(Kind.IO, "다운로드 실패: " + remoteName, e);
    }
}

기존 Files.copy 한 줄보다 코드는 늘지만, 진행 상황을 알아야 하는 대용량 다운로드에서는 이 구조가 필요합니다.

문제 2. 처리 이력을 파일 내용 해시까지 확인하도록 강화

pollAndProcess 는 지금 파일명만으로 중복을 판단합니다. 같은 파일명인데 내용이 다르면 오류로 처리하도록 바꿔보세요. 힌트: Set<String> 대신 Map<String,String>(파일명 → SHA-256)을 씁니다.

java
private static void pollAndProcess(Path incoming, Path processed,
        Map<String, String> processedHash) throws IOException {
    try (var s = Files.list(incoming)) {
        List<Path> markers = s.filter(p -> p.toString().endsWith(".ok")).sorted().toList();
        for (Path marker : markers) {
            String name = marker.getFileName().toString();
            String base = name.substring(0, name.length() - 3);
            Path dataFile = incoming.resolve(base);
            if (!Files.exists(dataFile)) continue;

            String hash = sha256(dataFile);
            String prevHash = processedHash.get(base);
            if (prevHash != null) {
                if (prevHash.equals(hash)) {
                    System.out.println("건너뜀(동일 파일 재수신): " + base);
                } else {
                    System.out.println("오류(같은 이름, 다른 내용): " + base);
                }
                continue;
            }
            Files.move(dataFile, processed.resolve(base));
            Files.delete(marker);
            processedHash.put(base, hash);
            System.out.println("처리 완료 -> processed/" + base);
        }
    }
}

파일명만 보던 기존 방식보다 안전하지만, 매번 해시를 계산하는 비용이 늘어나므로 대용량 파일에는 크기 비교를 먼저 하는 식으로 절충합니다.

문제 3. `TransferException.Kind` 에 따라 재시도 여부를 결정하는 유틸 작성

Kind 가 CONNECT 또는 IO 면 재시도하고, AUTH·PERMISSION 이면 바로 알림을 보내는 shouldRetry(Kind) 메서드를 작성하세요.

java
static boolean shouldRetry(FileTransfer.Kind kind) {
    return kind == FileTransfer.Kind.CONNECT || kind == FileTransfer.Kind.IO;
}

이 판단을 배치 잡의 재시도 루프에 넣으면, 영구적 실패를 붙잡고 무한 반복하는 사고를 막을 수 있습니다.

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