세 프로토콜은 이름은 비슷하지만 동작 방식이 다릅니다. 신규 연계는 특별한 사정이 없으면 SFTP 를 우선 검토합니다.
| 구분 | 전송 방식 | 특징 |
|---|---|---|
| FTP | 평문 | 인증정보·데이터 그대로 노출 |
| FTP | 포트 | 수동 모드도 방화벽 이슈 잦음 |
| FTPS | FTP + TLS | 제어·데이터 채널 암호화, 포트 복잡 |
| SFTP | SSH 기반 | 22 포트 하나로 제어·데이터 통합 |
FTP 는 데이터 채널에 별도 포트를 쓰기 때문에 능동·수동 모드 방화벽 설정이 번거롭습니다. SFTP 는 SSH 세션 하나로 인증부터 파일 전송까지 처리해 방화벽 정책이 단순합니다. 대외 기관 연계 신규 건은 SFTP 를 우선 검토하고, FTP 는 상대가 이미 그렇게 운영 중일 때만 그대로 맞춥니다.
기관 간 연계는 코드로 합의하지 않고 문서(규격서)로 합의합니다. 규격서가 애매하면 운영 중 분쟁이 생기므로, 최소한 아래 항목은 명시돼야 합니다.
/upload, /download 같은 절대 경로YYYYMMDD_기관코드_순번.dat이 중 파일명 규칙과 완료 신호가 실제 코드에 직접 반영되는 부분입니다. 나머지는 운영 협의로 풀리지만, 이 둘은 배치 로직에 그대로 들어갑니다.
파일 시스템에는 "다 쓸 때까지 안 보이게" 하는 기능이 없습니다. 그래서 임시 이름으로 올린 뒤 완료되면 정식 이름으로 바꾸는 방식을 씁니다. rename 은 같은 폴더 안에서 원자적으로 처리되므로, 수신 측이 폴링하는 순간에 절반만 쓰인 파일이 보일 일이 없습니다.
절차는 다음과 같습니다. 먼저 파일명.dat.tmp 로 업로드하고, 로컬 크기와 원격 크기를 비교해 전송이 온전한지 확인합니다. 그다음 .tmp 를 떼고 정식 이름으로 rename 하고, 마지막으로 파일명.dat.ok 같은 빈 마커 파일을 하나 더 올립니다. 수신 측은 데이터 파일이 아니라 마커 파일의 존재를 기준으로 처리 여부를 판단합니다.
마커 대신 파일명 확장자 자체를 바꾸는 방식(.uploading → .dat)도 같은 원리입니다. 어느 쪽이든 핵심은 "정식 이름으로 보이는 순간에는 이미 전송이 끝나 있어야 한다"는 것입니다.
수신 측 배치는 폴더를 주기적으로 스캔하되, 마커가 있는 파일만 처리 대상으로 삼습니다. 처리한 파일은 원본 폴더에 그대로 두지 않고 processed/ 로 옮겨서, 다음 폴링에서 같은 파일을 다시 보지 않게 합니다.
그래도 네트워크 지연이나 재전송으로 같은 파일명이 다시 올라올 수 있습니다. 이 경우를 대비해 처리 이력을 별도로 남깁니다. 실무에서는 DB 테이블(파일명, 처리 시각, 결과)로 관리하고, 이 레슨의 데모 코드에서는 Set<String> 으로 단순화했습니다. 같은 파일명이 다시 오면 정책에 따라 건너뛰거나, 내용이 다르면 오류로 알립니다.
전송 실패는 원인에 따라 대응이 달라야 합니다. 무조건 재시도하면 인증 실패 같은 영구적 문제도 계속 재시도하면서 알림만 반복됩니다.
| 실패 종류 | 원인 예 | 대응 |
|---|---|---|
| CONNECT | 네트워크 단절, 상대 서버 다운 | 지수 백오프 재시도 |
| AUTH | 비밀번호·키 만료 | 즉시 알림, 재시도 무의미 |
| PERMISSION | 계정 권한 부족 | 즉시 알림, 담당자 확인 |
| IO | 디스크 풀, 중간 끊김 | 정리 후 재시도 |
이 레슨의 FileTransfer.TransferException 은 Kind 열거형으로 이 네 가지를 구분합니다. 재시도 로직은 Kind 를 보고 CONNECT·IO 는 재시도하고, AUTH·PERMISSION 은 즉시 알림으로 넘기는 식으로 분기합니다. 알림은 11 외부 API 연동, 19 메일 발송 레슨의 방식을 그대로 씁니다.
SFTP 인증은 비밀번호와 키 두 가지입니다. 배치 서버 간 연계는 대부분 키 인증(~/.ssh/id_ed25519)을 씁니다. 비밀번호가 스크립트나 설정 파일에 평문으로 남는 것을 피할 수 있기 때문입니다.
키 인증을 쓰더라도 known_hosts 검증(StrictHostKeyChecking)을 끄면 안 됩니다. 이 설정을 끄면 중간자 공격으로 다른 서버에 파일을 올려도 알아채지 못합니다. 계정은 전용 계정을 발급받아 chroot 로 지정된 폴더 밖을 볼 수 없게 하고, 업로드 대상 폴더에만 쓰기 권한을 줍니다.
대외 기관 회선은 사내 네트워크보다 느리고 불안정한 경우가 많습니다. 연결·읽기 타임아웃을 넉넉히 잡되 무한 대기는 피하고, 유휴 연결이 끊기지 않도록 keepalive 를 설정합니다.
대용량 파일은 전체를 메모리에 올리지 말고 스트림으로 흘려보냅니다. 이 레슨의 LocalTransfer.upload 도 4096바이트 버퍼로 읽고 쓰는 스트리밍 구조라, 파일 크기가 커져도 힙 사용량이 늘지 않습니다.
배치 실행 시각은 대외 기관이 정한 시간 창(윈도) 안에 맞춰야 합니다. 09 스케줄 레슨의 cron 표현식으로 이 시간 창을 관리합니다. 너무 이르면 상대 서버가 파일을 아직 준비하지 못했고, 너무 늦으면 그날 정산에서 누락됩니다.
감사 로그는 "누가 언제 무엇을 몇 바이트 전송했는지"를 한 줄로 남깁니다. 사고가 났을 때 이 로그가 유일한 증거인 경우가 많습니다.
폐쇄망 프로젝트는 SFTP 클라이언트 jar 를 미리 반입해야 합니다. JSch 원본은 유지보수가 멈춰서, 포크판인 com.github.mwiede:jsch 0.2.x 를 권장합니다. FTP 만 쓰면 Apache Commons Net 으로 충분하고, Spring Integration SFTP 는 이 정도 규모 연계에는 과합니다.
배치를 붙이기 전에 리눅스 sftp·scp·rsync 명령으로 먼저 손으로 접속해 봅니다. 계정 권한, 경로, 방화벽이 맞는지 확인하는 가장 빠른 방법입니다. 명령 형식은 리눅스 명령어 06 레슨을 참고합니다.
배치 로직(원자적 전송, 폴링, 중복 방지)은 실제 SFTP 서버 없이도 검증할 수 있습니다. 이 레슨처럼 로컬 폴더를 원격 루트로 삼는 구현체로 단위 테스트를 돌리고, 실제 서버 연동은 스테이징 환경에서 별도로 확인합니다. 프로토콜 자체를 테스트하는 게 아니라 "임시 이름 업로드 → 크기 확인 → rename → 마커" 순서가 지켜지는지를 테스트하는 것이 목적입니다.