FTP 동시 파일 접근 시 연결 수 초과 문제 해결

FTP 서버에서 여러 스레드가 동시에 파일 내용을 읽어들이는 상황에서, 일정 시간이 지나면 연결 수가 급증하여 서버 측 제한에 걸리는 현상이 발생했다. 이 글에서는 문제의 원인과 해결 방안을 정리한다.

문제 상황

다중 스레드 환경에서 FTP 파일을 메모리로 직접 로드하는 방식을 사용했다. 파일을 로컬 디스크에 저장하지 않고 바이트 배열로 변환하여 처리하는 구조였다.

환경 정보

  • OS: CentOS 7
  • JDK: 1.8
  • Apache Commons Net FTPClient

원인 분석

초기 구현 코드는 다음과 같았다:

InputStream dataStream = null;
ByteArrayOutputStream memoryBuffer = null;

try {
    dataStream = ftpSession.retrieveFileStream(targetPath);
    memoryBuffer = new ByteArrayOutputStream();
    
    byte[] chunk = new byte[4096];
    int bytesRead;
    
    while ((bytesRead = dataStream.read(chunk)) != -1) {
        memoryBuffer.write(chunk, 0, bytesRead);
    }
} finally {
    if (dataStream != null) {
        dataStream.close();
    }
    if (memoryBuffer != null) {
        memoryBuffer.close();
    }
}

스트림을 닫는 것만으로는 FTP 프로토콜 레벨의 명령 시퀀스가 완료되지 않는다. retrieveFileStream, storeFileStream 등의 메서드는 중간 응답을 받은 후 추가 조치가 필요한 불완전한 명령이다. 스트림 처리 후 completePendingCommand()를 호출하여 서버의 완료 응답을 수신하고 전체 트랜잭션을 마무리해야 한다.

이 호출이 누락되면 FTP 연결이 정상 종료되지 않고 세션이 계속 점유된다. 시간이 지나면서 연결 풀이 고갈되고 새 연결을 생성하려다 서버 제한에 도달하게 된다.

해결 방법

방법 1: 공식 문서 기준 구현

FileInputStream localIn = new FileInputStream("source.txt");
OutputStream remoteOut = ftpClient.storeFileStream("remote.txt");

if (!FTPReply.isPositiveIntermediate(ftpClient.getReplyCode())) {
    localIn.close();
    remoteOut.close();
    ftpClient.logout();
    ftpClient.disconnect();
    throw new IOException("업로드 초기화 실패");
}

IOUtils.copy(localIn, remoteOut);
localIn.close();
remoteOut.close();

// 반드시 명령 완료 처리 필요
if (!ftpClient.completePendingCommand()) {
    ftpClient.logout();
    ftpClient.disconnect();
    throw new IOException("업로드 완료 확인 실패");
}

방법 2: 메모리 로딩 방식 개선

바이트 배열로 직접 로드하는 경우 다음과 같이 수정한다:

InputStream dataStream = null;
ByteArrayOutputStream memoryBuffer = null;
boolean commandCompleted = false;

try {
    dataStream = ftpSession.retrieveFileStream(targetPath);
    memoryBuffer = new ByteArrayOutputStream();
    
    byte[] chunk = new byte[8192];
    int bytesRead;
    
    while ((bytesRead = dataStream.read(chunk)) != -1) {
        memoryBuffer.write(chunk, 0, bytesRead);
    }
    
    // 스트림 완전 소진 후 명령 완료
    dataStream.close();
    dataStream = null;
    
    if (!ftpSession.completePendingCommand()) {
        throw new IOException("파일 수신 완료 실패");
    }
    commandCompleted = true;
    
    return memoryBuffer.toByteArray();
    
} finally {
    if (dataStream != null) {
        try { dataStream.close(); } catch (IOException ignored) {}
    }
    if (memoryBuffer != null) {
        try { memoryBuffer.close(); } catch (IOException ignored) {}
    }
    
    // 예외 발생 시에도 명령 완료 시도
    if (!commandCompleted && ftpSession != null) {
        try {
            ftpSession.completePendingCommand();
        } catch (IOException cleanupEx) {
            // 로깅만 수행
        }
    }
}

검증 결과

ss -tn | grep ':21' | wc -l 명령으로 연결 수를 모니터링했다. 수정 전에는 시간 경과에 따라 ESTABLISHED 상태의 연결이 누적되었으나, 수정 후에는 연결이 안정적으로 유지되었다. 1시간 이상 지속 실행 시에도 연결 수 급증 현상이 재현되지 않았다.

추가 고려사항

  • completePendingCommand()는 한 번만 호출해야 한다. 중복 호출은 예외를 발생시킨다.
  • 스트림을 닫은 후, 연결을 반환하기 전에 반드시 호출해야 한다.
  • 연결 풀을 사용하는 경우 풀 반환 로직에 이 검증을 포함해야 한다.

태그: FTP Apache Commons Net connection pool Java IO Network Programming

7월 15일 05:50에 게시됨