MyStory/Technology_Preview

SFTP 사용해서 서버에 이미지 저장

LupyLaon 2025. 3. 26. 17:16

관리자 페이지에서 등록 부분에서 이미지를 업로드하여 등록하는 부분이 있었다.

이때 이미지를 로컬에 생성하는 것이 아닌, 적절한 이름으로 원격 서버에 저장하는 방식이 좋을거라고 생각했다.

원래는 클라우드에 저장하는 것이 제일 쉬운 방식이라고 생각하지만, 유지보수적인 부분에서 원격 서버에 저장하는 것을 선택했다.

 

아래는 구현 설계 순서도이다.

 

 

주요 메서드는 saveFiles()에서 이루어진다.

기존에 드라이브에 저장을 하는 방식을 생각하면 비슷하다.

1. 저장할 경로를 만들어준다.

2. 저장한 경로를 난 새롭게 만들어지는 디렉토리 내에 넣을거다. -> 새로운 폴더를 만든다.

3. 각 파일의 유효성을 검사한다.

4. 파일명을 만들어준다.

5. 파일을 업로드한다.

 

꽤 간단한 방식으로 구현했다.

하지만 구현전에 먼저 SFTP에 대한 이해가 필요했다.

 

SFTP(Secure File Transfer Protocol)

- SFTP는 SSH(Secure Shell) 프로토콜을 통해 파일을 안전하게 전송하는 네트워크 프로토콜

 

SFTP의 특징

1. 보안성

- SSH 터널을 통해 데이터 전송

- 데이터 암호화

- 중간자 공격 방지

- 강력한 인증 메커니즘

2. 주요 기능

- 파일 업로드/ 다운로드

- 원격 파일 시스템 탐색

- 파일/ 디렉토리 생성, 삭제, 권한 변경

- 단일 포트(기본 22번) 사용

 

프로토콜 작동 원리

1. Client -> SFTP 서버 : SSH 연결 요청

2. SFTP서버 -> Client : 연결 수락

3. Client -> SFTP 서버 : 사용자 인증 (비밀번호/키)

4. SFTP서버 -> Client : 인증 승인

5. Client -> SFTP 서버 : 파일 전송 명령

6. SFTP -> Client : 파일 전송 완료

 

SFTP 구현 핵심

- 연결 관리

 

ChannelSftp는 JSch 라이브러리에서 제공하는 SFTP 채널 클래스이다.

SFTP 프로토콜을 통한 파일 전송 작업을 수행한다.

private ChannelSftp createNewConnection() throws JSchException {
    JSch jsch = new JSch();
    Session session = jsch.getSession(user, host, port);
    session.setPassword(password);

    // 보안 설정
    session.setConfig("StrictHostKeyChecking", "no");
    session.setConfig("PreferredAuthentications", "password");
    
    // 타임아웃 및 keepalive 설정
    session.setServerAliveInterval(15000);
    session.setServerAliveCountMax(3);

    session.connect(CONNECTION_TIMEOUT);

    Channel channel = session.openChannel("sftp");
    channel.connect(CHANNEL_TIMEOUT);

    return (ChannelSftp) channel;
}

 

 

- 파일 업로드

 

MultipartFile은 Spring Framework에서 제공하는 인터페이스다.

웹 어플리케이션에서 파일 업로드를 처리하는 데 사용한다.

private void uploadFile(ChannelSftp channelSftp, MultipartFile file, 
                        String savePath, String newFileName) throws SftpException, IOException {
    // 입력 스트림을 통해 파일 전송
    channelSftp.put(file.getInputStream(), newFileName);
}

 

 

public String saveFiles(List<MultipartFile> files, String fileType) throws IOException {
    // 저장 경로 생성
    String savePath = createSavePath(fileType);
    
    try (SftpSession sftpSession = new SftpSession()) {
        ChannelSftp channelSftp = sftpSession.getChannel();
        // 원격 디렉토리 생성
        createDirectoryIfNotExists(channelSftp, savePath);

        // 각 파일 처리
        for (MultipartFile file : files) {
            // 파일 유효성 검사
            if (!isValidFile(file)) continue;

            // 고유 파일명 생성
            String newFileName = generateFileName(fileType, file);
            // 파일 업로드
            uploadFile(channelSftp, file, savePath, newFileName, paths);
        }

        // 저장된 파일 경로 반환
        return String.join(" ", paths);
    }
}

 

검증

 

자신이 만들고 있는 프로젝트의 특성에 맞게 파일을 검증하는 과정은 필요하다.

난 파일 존재 여부, 파일 크기, 확장자, 파일 시그니처를 검증했다.

private boolean isValidFile(MultipartFile file) {
    // 검증 항목:
    // 1. 파일이 비어있지 않음
    // 2. 파일 크기 제한 (10MB)
    // 3. 허용된 확장자 (.jpg, .jpeg, .png, .pdf)
    // 4. 파일 시그니처 검증
}

 

파일 이름 및 경로 생성

 

이미지를 불러와서 사용하는 쪽에 용이하도록 이름을 재설정해줬다.

적절하게 저장하려는 객체에 맞는 이름으로 접두사를 사용해서 붙여줬다.

또한 이름이 중복되지 않도록 하였다.

private String generateFileName(String fileType, MultipartFile file) {
    // 고유 파일명 생성:
    // - 파일 유형에 따른 접두사 (poster_, notice_ 등)
    // - 랜덤 UUID 조각
    // - 원본 파일 확장자
    return prefix + UUID.randomUUID().toString().substring(0, 8) + extension;
}

 

트러블 슈팅

 

처음 구현했을 때 지속적인 연결문제가 있었다.

문제를 계속 찾아보았지만, 무엇이 문제인지 모르겠었다.

일단 연결이 중간에 끊기는 현상이지 않을까 생각해서 연결 실패 시 재시도 하는 재시도 메커니즘을 추가했다.

private ChannelSftp getConnection() throws JSchException {
    for (int i = 0; i < RETRY_COUNT; i++) {
        try {
            // 연결 시도
            // 실패 시 대기 후 재시도
        } catch (Exception e) {
            if (i < RETRY_COUNT - 1) {
                Thread.sleep(RETRY_DELAY);
            }
        }
    }
}

 


결과적으로는 이것도 문제가 아니었다. 

확인결과.....application.yml에 입력한 sftp 설정값의 비밀번호가 틀렸다..

수정해주니 완벽해결!