들어가며
나는 jsch 라이브러리를 사용하여 파일을 SFTP 를 통하여 다른 서버로 전송을 해야했다.
ex) prod-server -> file-server
필자는 gradle 을 사용하여 아래 의존성을 추가하여 라이브러리를 사용하였다.
implementation group: 'com.jcraft', name: 'jsch', version: '0.1.55'
간단하게 내 상황에 대하여 설명을 해보자면
프론트에서 User 가 파일 업로드를 여러개 하였고 그 파일들을 서버로 전송 받아서
위 파일들을 SFTP 세션을 생성해 파일 서버로 보내는 작업을 하고 있었다.
위 상황에서 추가적인 요구사항이 들어왔다.
User 가 파일업로드 후 서버로 파일들을 전송할 때, User 가 업로드 한 파일이 아닌
다른 서버에 존재하는 파일을 pdf 로 변환하여 파일 서버로 보내야 하는 요구사항이 있다.
즉, 업로드 한 파일들 + 타 서버에 존재하는 파일을 pdf 로 가공한 파일
위 2가지를 파일 서버로 전송을 해야 했다.
그리고 위 요구 사항을 잘 해결하였지만, 퍼포먼스가 잘 나오지는 않았다.
원래 I/O 작업이 시간이 오래걸리는 것은 알고있었지만 평균 15초 정도는 용납할 수 없었다..
그래서 위 문제를 해결하기 위해 비동기(=@Async) 를 사용하여 해결하기로 하였다.
하지만 여기서 문제가 발생하였다.
com.jcraft.jsch.SftpException: java.io.IOException: Pipe closed
-> SFTP 세션이 파일 처리 중 닫히는 문제가 발생하였다.
한번 위 문제를 해결 해보자
본론
이 문제를 해결하기 위해 구글링도 해보고 지피티도 이용해봤지만 별다른 성과가 없었다.
하지만 해답은 생각보다 가까운 곳에 있었다.
내가 생각했던 비동기 플로우는 아래와 같다.
- Multipart 로 파일을 받아서 파일서버로 그 파일들을 보낼 때 SFTP 세션을 Open 한다.
- SFTP 세션이 Open 되어 있을 때 그 세션을 사용하여 기존에 있던 파일 또한 가공하여 보내려고 한다.
- 그리고 I/O 작업이 오래걸리는 걸 막기 위해 비동기를 적용해 다른 스레드에서 작업을 하게 한다.
뭔가 될 것 같아서 계속 시도를 해봤지만 위 에러를 계속 만나게 되었다.
자세한 설명을 위해 코드를 보자.
private List<Files> uploadMultipartFiles(List<MultipartFile> files,) throws Exception {
List<Files> fileList = new LinkedList<>();
ChannelSftp sftp = null;
try {
sftp = fileService.createSftp(); //sftp 생성
updateService.updateFiles(sftp,path);
for (int i = 0; i < files.size(); i++) {
MultipartFile document = fileList.get(i);
String fileName = StringUtils.defaultString(document.getOriginalFilename());
sftp.put(document.getInputStream(), fileName, ChannelSftp.OVERWRITE);
sftp.chmod(0_660, fileName);
}
} finally {
fileService.disconnect(sftp);
}
return fileList;
}
위 로직을 통해 SFTP 세션을 생성하고 파일 서버로 MultiPart 파일을 보냈다.
그리고 내가 생각한 로직은 updateService.updateFiles(sftp,path);
이 로직에서 위에 생성된 sftp 를 재사용하여 파일을 변환하는 방법을 사용하려고 했다.
아래 코드가 문제가 되는 코드 이다.
@Async
public void updateFiles (ChannelSftp sftp, String path) throws Exception {
ByteArrayOutputStream pdfOutputStream = docxToPdf(docxOutputStream.toByteArray());
transferPdfToFileServer(sftp, pdfOutputStream, path);
}
}
위 로직을 통해서 open 되어있는 sftp 세션을 재사용 하여 파일 전송을 하려했지만 실패했다
왜 일까? 고민을 하루 동안 해보았고 그 결과 Console 에 있는 로그들을 보며 해답을 찾았다.
해답은 @Async 처리를 하는 순간 내가 만든 메소드는 별도의 스레드에서 작업이 된다.
그 뜻은 내가 기존에 만들 어둔 메소드는 Main 스레드에서 동작이 되지만, 내가 비동기 작업을 건 메소드는 다른 스레드에서 동작을 한다.
‘즉 실행되는 스레드가 달라 세션이 살아 있어도 사용을 할 수가 없던 것이였다.’
생각해보면 당연한 건데, 위 내용을 잊고 있었다.
그러므로 위 문제를 해결하기 위해서는 비동기 메소드에서도 sftp 세션을 생성해야 했다.
@Async
public void updateFiles (ChannelSftp sftp, String path) throws Exception {
ByteArrayOutputStream pdfOutputStream = convertDocxToPdf(docxOutputStream.toByteArray());
ChannelSftp sftp = null;
try {
sftp = fileService.createSftp();
transferPdfToFileServer(sftp, pdfOutputStream, path);
} finally {
Objects.requireNonNull(sftp).disconnect();
}
}
}
위 처럼 코드를 작성하여 sftp 세션을 비동기 스레드에 생성을 해주었더니 에러가 발생하지 않고 실행이 잘되었다….
정리를 해보자면
일반적으로 SFTP 세션은 스레드 간에 안전하게 공유되지 않는 특성을 가지고 있다.
이는 SFTP 세션이 내부적으로 네트워크 소켓이나 버퍼와 같은 리소스를 유지하기 때문이며
다른 스레드에서 기존 세션을 사용할 경우 연결 불안정 또는 접근 문제로 인해 예외가 발생할 수 있다는 점을 인지했어야 했다.
- Ex) java.io.IOException: Pipe closed
즉 메인 스레드에서 생성한 SFTP 세션을 비동기 스레드에서 사용하는 것이 애초에 불가능한 플로우였다.
프로토콜 특성을 조금 딥하게 알았다면 아주 쉽게 해결할 수 있었을 문제였다.
다음에 위 문제가 다시 발생한다면 이제는 빠르게 해결할 수 있을 것 같다.
결론
언제나 개발을 하다가 딥하게 파다가 밑바닥에서 내리는 결론은 'CS 지식' 의 중요성 이다.
즉 CS 지식이 많으면 많을 수록 에러 해결또한 비교적 쉽고 문제에 대한 접근을 다양하게 할 수 있다.
오늘의 결론, CS 공부를 많이 하자.