내가 담당하는 업무는 쇼핑몰 백오피스 운영이고 고객에게 노출될 이미지, 동영상 등 파일들을 업로드 하는 로직이 여러 메뉴에 존재한다.
예를 들면, 기획전 배너 이미지, 광고 배너, 공지 배너 등등... 다양한 종류의 이미지들을 업로드 하고 있으며
이 이미지들은 홈쇼핑의 FTP 서버에 업로드 하고 있다. 업로드 타겟이 되는 FTP 서버의 유형은 2가지이다.
이미지와 동영상만 언급했지만, 업로드 대상이 되는 파일의 유형은 PDF파일이 될 수도 있으며 HTML 컨텐츠를 업로드 할 수도 있다.
FTP에 연결하고 업로드 하는 핵심 로직은 특정 클래스(아래 코드의 SftpOperationExt)에 구현되어 있으며,
업로드가 필요한 각 메뉴별 서비스에서 대략 아래와 같은 메서드를 만들고, 실제 이미지를 업로드 하는 메서드 내에서 호출하고 있다.
private void uplaodImageBySftp(InputStream is, String imageName, String remotePath) throws Exception {
SftpOperationExt sftp = null;
try {
sftp = (SftpOperationExt)sftpOperationFactory.connect(); // get SFTP connection
sftp.uploadAsStream(is, imageName, new File(remotePath)).disconnect();
} catch(Exception e) {
throw e;
} finally {
if (sftp != null && sftp.isConnected()) {
try {
sftp.disconnect();
} catch (Exception e) {
// Logging exception message
}
}
}
}
사실 위의 예시 코드는 그 동안 별 문제 없이 새로운 기능 혹은 메뉴가 추가될 때 마다 관행처럼 복붙하던 코드였으나, 연내 파일 업로드 타겟이 FTP 서버에서 AWS S3로 전환될 예정으로 이슈가 되는 코드로 바뀌게 되었다. 위와 같은 코드를 모두 찾아 바꿔야 했기 때문이다.
그리하여 FTP 업로드를 수행하는 코드들에 대한 전수조사 중, 아래와 같은 추가적인 문제를 발견하게 되었다.
위의 내용들을 종합하여 최종적으로 정리한 현재 애플리케이션 내 FTP 처리 로직의 문제점은 아래와 같았다.
그래서 나는 아래와 같은 점을 핵심으로 삼아 개선을 진행하기로 했다.
위와 같은 개선 방안으로 최종 개선한 내역은 아래와 같다.
interface FileOperation {
void upload(Category category, InputStream is);
void download(Category category, String destination);
void copy(String origin, String target); // 예외적으로 Category를 따르지 않고 위치를 직접 지정
void delete(Category category, String filename);
String getUploadPath(Category category, Map<String, String> pathParameter);
}
기존 애플리케이션에서 발생하는 파일 오퍼레이션 4종류에 대해 인터페이스 내에 정의하고, 추가적인 코드 중 대표적인 사례였던 파일 오퍼레이션 경로 반환 또한 파일 오퍼레이션 구현체에서 수행하도록 했다.
Category는 파일 오퍼레이션이 발생하는 메뉴 및 파일 유형에 대한 정의이며, Enum이다. 파일 오퍼레이션이 발생하는 로직 내에서는 직접 파일 오퍼레이션 경로를 만드는 과정을 코드로 작성하지 않고(파일 복사의 경우 예외), 어느 영역에서 발생하는 파일 오퍼레이션인지에 대한 정보만 전달한다.
enum Category {
DEAL_IMAGE(FileType.IMAGE),
DEAL_VIDEO(FileType.VIDEO),
ADVERTISE_BANNER(FileType.IMAGE),
...
}
파일 오퍼레이션이 이루어지는 영역에 대한 정의이다. 각 서비스 로직에서는 파일 오퍼레이션 시 Category에 정의된 이름을 통해 요청한다. 해당 영역에 대한 파일 유형 등 환경별로 달라지지 않는 값은 위와 같이 함께 정의한다.
enum FtpCategory {
DEAL_IMAGE(ServerType.IMAGE, "/deal/image"),
DEAL_VIDEO(ServerType.VIDEO, "/deal/video"),
ADVERTISE_BANNER(ServerType.IMAGE, "/banner/{YYYYMM}"),
...
}
enum S3Category {
DEAL_IMAGE("/IMG/deal/image"),
DEAL_VIDEO("/VOD/deal/video"),
ADVERTISE_BANNER("/IMG/banner/{YYYYMM}"),
...
}
Category에 대한 상세 내용을 정의한 enum이다. 파일 오퍼레이션 경로, 오퍼레이션 대상 FTP 서버 등 환경에 따라 달라질 수 있는 값들을 정의하기 위한 것이다. 위 2개의 enum을 보면 서로 경로가 다른 것을 볼 수 있을 것이다. 파일 오퍼레이션 인터페이스 구현체에서는 Category에 정의된 이름을 이용하여 각 환경에 맞는 enum에 정의된 이름의 값을 찾아 작업을 수행한다.
@Primary
public class FtpFileOperation implements FileOperation {
@Override
public void upload(Category category, InputStream is) {
FtpCategory categoryDetail = FtpCategory.valueOf(category.name()); // FtpCategory 참조
... // FTP 파일 업로드 로직
}
}
개선 이전에도 사용하던 로직 기반으로 파일 오퍼레이션 구현체를 만든다. 단, 기존에는 각 서비스 소스 상황에 따라 조금씩 다를 수 있었지만 이제 공통적으로 사용 가능하도록 코드를 작성한다. 환경에 따라 알맞는 Category 상세 enum을 참조한다.
(FtpFileOperation은 FtpCategory를 참조, S3FileOperation은 S3Category를 참조)
@Autowired
private FileOperation fileOperation;
...
// 업로드 수행 - DEAL_IMAGE 유형에 file 업로드 시
fileOperation.upload(Category.DEAL_IMAGE, file.getInputStream);
...
// 결과 처리 시 업로드 경로가 필요하다면 아래와 같이 획득, 경로 내 파라미터가 없으므로 null 전달
fileOperation.getUploadPath(Category.DEAL_IMAGE, null);
...
이제 각 서비스 소스 내에서는 이 글의 초반에 언급했던 예시 코드와 같은 코드를 작성할 필요가 없다. 경로를 만들어내는 코드도 작성하지 않아도 되며, 환경별 프로퍼티 파일에 경로를 기입할 필요도 없다. 필요한 오퍼레이션에 대한 메서드를 호출하는 코드를 넣기만 하면 된다. 다만, 새로운 카테고리가 생긴다면 Category와 환경별 Category 상세 enum에 정의해주면 된다.
사실 상황에 의해서 반강제로 하게 된 것이기도 하지만... 개선 후 실질적으로 느낀 효과는 아래와 같았다.
그밖에 아쉬운 점과 우려도 약간은 남아있었는데...
사실 간단하게 보면 기존에 흩어져 있던 코드들을 모아서 한곳에 모아서 그것을 가져다 쓰도록 한 것에 불과하지만, 실제로는 파일 오퍼레이션 환경에 대한 대응도 어렵지 않게 되었고 파일 오퍼레이션의 발생에 대한 포인트를 한 곳에서 관리함으로써 운영하는 입장에서는 더 편리해진 느낌이 들었다. 또한, 생각없이 복사-붙여넣기 하던 코드들이 더 이상 필요하지 않게 되었으며 이에 따라 불필요하게 중복되는 코드들이 제거되어 각 서비스는 자신의 비즈니스 로직에만 집중할 수 있게 되었다.
물론 이번과 같은 경우는 꼭 변화가 이루어져야만 하는 상황이긴 했지만, 이번 작업을 진행하면서 당연하게 생각하던 것에 대해 가끔은 의구심을 갖고 바라보는 것의 중요성과, 그것에 대해 가볍게나마 개선해 나가는 것이 업무 효율에 큰 영향을 미친다는 것을 다시 한 번 깨닫게 되었다.