Notice
Recent Posts
Recent Comments
Link
«   2026/09   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Archives
Today
Total
관리 메뉴

hilims 님의 블로그

Spring Boot에서 SFTP 파일 업로드 & 다운로드 구현 (JSch 기반) 본문

JAVA/Spring

Spring Boot에서 SFTP 파일 업로드 & 다운로드 구현 (JSch 기반)

hilims 2025. 10. 28. 09:57

 

개요

Spring Boot 프로젝트에서 SFTP 서버와 파일을 주고받기 위한 서비스 로직을 구현했습니다.
이를 통해 손쉽게 파일을 업로드, 다운로드, 삭제할 수 있으며, 연결 관리 및 디렉토리 자동 생성 기능도 포함되어 있습니다.


⚙️ 의존성 추가

build.gradle 또는 pom.xml에 아래 의존성을 추가합니다.

implementation 'com.jcraft:jsch:0.1.55'

🧱 설정 파일 (application.yml)

sftp:
  upload:
    connection:
      host: sftp.server.address
    credentials:
      user: sftpUser
      password: sftpPassword

💡 주요 기능

  • ✅ SFTP 연결 및 해제 관리
  • ✅ 디렉토리 자동 생성 (mkdir -p 역할)
  • ✅ 파일 업로드 / 다운로드 / 삭제
  • ✅ 파일 존재 여부 확인
  • ✅ Spring MultipartFile 직접 업로드 지원

🧑‍💻 SftpService.java

package com.swinnus.reefer.service.common.sftp;

import com.jcraft.jsch.*;
import com.swinnus.reefer.dto.common.FileVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.Objects;

@Slf4j
@Service
@RequiredArgsConstructor
public class SftpService {

    @Value("${sftp.upload.connection.host}")
    private String host;
    @Value("${sftp.upload.credentials.user}")
    private String user;
    @Value("${sftp.upload.credentials.password}")
    private String password;

    private final int port = 22;
    private final JSch jsch = new JSch();

    /** SFTP 연결 */
    private ChannelSftp openSftp() {
        try {
            Session session = jsch.getSession(user, host, port);
            session.setPassword(password);
            session.setConfig("StrictHostKeyChecking", "no");
            session.connect();

            ChannelSftp sftp = (ChannelSftp) session.openChannel("sftp");
            sftp.connect();
            sftp.setFilenameEncoding("UTF-8");

            log.debug("SFTP connected: {}@{}", user, host);
            return sftp;
        } catch (JSchException e) {
            throw new RuntimeException("SFTP connection failure: " + e.getMessage(), e);
        } catch (SftpException e) {
            throw new RuntimeException(e);
        }
    }

    /** 연결 종료 */
    private void closeSftp(ChannelSftp sftp) {
        if (Objects.isNull(sftp)) return;
        try {
            Session session = sftp.getSession();
            sftp.disconnect();
            if (session != null && session.isConnected()) session.disconnect();
            log.debug("SFTP disconnected: {}@{}", user, host);
        } catch (Exception e) {
            log.warn("SFTP Closing failure", e);
        }
    }

    /** 디렉토리 자동 생성 */
    private void createDirectories(ChannelSftp sftp, String remoteDir) throws SftpException {
        String[] folders = remoteDir.replace("\\", "/").split("/");
        String path = "";
        for (String folder : folders) {
            if (folder == null || folder.trim().isEmpty()) continue;
            path += "/" + folder;
            try {
                sftp.cd(path);
            } catch (SftpException e) {
                sftp.mkdir(path);
                sftp.cd(path);
            }
        }
    }

    /** 파일 다운로드 */
    public byte[] downloadToBytes(String remotePath) {
        ChannelSftp sftp = openSftp();
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            sftp.get(remotePath.replace("\\", "/"), baos);
            return baos.toByteArray();
        } catch (SftpException | java.io.IOException e) {
            throw new RuntimeException("File download failure: " + remotePath, e);
        } finally {
            closeSftp(sftp);
        }
    }

    /** 파일 존재 여부 확인 */
    public boolean exists(String remotePath) {
        ChannelSftp sftp = openSftp();
        try {
            sftp.stat(remotePath.replace("\\", "/"));
            return true;
        } catch (SftpException e) {
            if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) return false;
            throw new RuntimeException("Failure to confirm whether the file exists: " + remotePath, e);
        } finally {
            closeSftp(sftp);
        }
    }

    /** 파일 업로드 */
    public FileVO uploadFile(MultipartFile file, String basePath, String directoryPath) {
        ChannelSftp sftp = openSftp();
        try (InputStream inputStream = file.getInputStream()) {
            String remoteDir = (basePath + "/" + directoryPath).replace("\\", "/");

            createDirectories(sftp, remoteDir);

            String remotePath = remoteDir + "/" + file.getOriginalFilename();
            sftp.put(inputStream, remotePath);

            return FileVO.builder()
                    .filePath(directoryPath.replace("\\", "/"))
                    .fileName(file.getOriginalFilename())
                    .fileSize(file.getSize())
                    .storagePath(remotePath)
                    .build();
        } catch (Exception e) {
            throw new RuntimeException("File upload failure: " + file.getOriginalFilename(), e);
        } finally {
            closeSftp(sftp);
        }
    }

    /** 파일 삭제 */
    public void deleteFile(String remotePath) {
        ChannelSftp sftp = openSftp();
        try {
            sftp.rm(remotePath.replace("\\", "/"));
        } catch (SftpException e) {
            throw new RuntimeException("File deletion failure: " + remotePath, e);
        } finally {
            closeSftp(sftp);
        }
    }
}

📂 FileVO 예시

@Builder
public record FileVO(
    String filePath,
    String fileName,
    long fileSize,
    String storagePath
) {}

🧠 포인트 정리

기능 설명
openSftp() SFTP 연결 생성
closeSftp() 연결 및 세션 종료
createDirectories() 상위 디렉토리 자동 생성
uploadFile() MultipartFile 업로드 및 경로 리턴
downloadToBytes() 파일을 byte[] 형태로 다운로드
exists() 파일 존재 여부 확인
deleteFile() 파일 삭제 처리

🧾 예외 처리 팁

  • JSchException: Auth fail → 사용자명 / 비밀번호 / 포트 확인
  • Connection timed out → 방화벽 혹은 네트워크 설정 문제
  • SSH_FX_NO_SUCH_FILE → 경로 오타 또는 권한 문제